diff --git a/.github/native-tests.json b/.github/native-tests.json index 51c1808bf56ca9..08fb441174abfc 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -21,7 +21,7 @@ { "category": "Data3", "timeout": 70, - "test-modules": "flyway, hibernate-orm-panache, hibernate-orm-panache-kotlin, hibernate-orm-envers, liquibase", + "test-modules": "flyway, hibernate-orm-panache, hibernate-orm-panache-kotlin, hibernate-orm-envers, liquibase, liquibase-mongodb", "os-name": "ubuntu-latest" }, { diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index 375315315fe8bd..c5011fc09bcec6 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -178,9 +178,10 @@ jobs: uses: actions/upload-artifact@v2 if: ${{ failure() || cancelled() }} with: - name: "build-reports-JVM Tests - JDK ${{matrix.java.name}}" + name: "build-reports-Initial JDK 11 Build" path: | target/build-report.json + LICENSE.txt retention-days: 2 calculate-test-jobs: @@ -322,6 +323,7 @@ jobs: path: | **/target/*-reports/TEST-*.xml target/build-report.json + LICENSE.txt retention-days: 2 - name: Upload gc.log uses: actions/upload-artifact@v2 @@ -388,6 +390,7 @@ jobs: path: | **/target/*-reports/TEST-*.xml target/build-report.json + LICENSE.txt retention-days: 2 gradle-tests: @@ -447,6 +450,7 @@ jobs: **/build/test-results/test/TEST-*.xml **/target/*-reports/TEST-*.xml target/build-report.json + LICENSE.txt retention-days: 2 devtools-tests: @@ -506,6 +510,7 @@ jobs: path: | **/target/*-reports/TEST-*.xml target/build-report.json + LICENSE.txt retention-days: 2 tcks-test: @@ -564,6 +569,7 @@ jobs: path: | **/target/*-reports/TEST-*.xml target/build-report.json + LICENSE.txt retention-days: 2 native-tests: @@ -643,4 +649,5 @@ jobs: **/target/*-reports/TEST-*.xml **/build/test-results/test/TEST-*.xml target/build-report.json + LICENSE.txt retention-days: 2 diff --git a/bom/application/pom.xml b/bom/application/pom.xml index f1c39ced2103be..7acd0a5f7ff49e 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -17,7 +17,7 @@ 1.69 1.0.2.1 1.0.12.2 - 2.3.1.Final + 2.4.0.Final 4.7.0.Final 0.33.0 1.0.0 @@ -45,15 +45,15 @@ 2.4.4 3.1.1 3.0.1 - 2.1.10 - 1.3.1 + 2.1.12 + 1.3.2 2.0.1 5.2.1 3.2.1 1.2.0 1.0.13 2.6.0 - 2.12.0 + 2.13.0 3.9.1 1.2.1 1.3.5 @@ -109,7 +109,7 @@ 1.16.1.Final 1.8.7.Final 3.4.2.Final - 4.1.2 + 4.1.3 4.5.13 4.4.14 4.1.4 @@ -120,7 +120,7 @@ 2.7.4 8.0.26 7.2.2.jre8 - 21.1.0.0 + 21.3.0.0 10.14.2.0 11.5.6.0 1.2.6 @@ -131,7 +131,7 @@ 12.1.7.Final 4.4.1.Final 2.9.2 - 4.1.65.Final + 4.1.67.Final 1.0.3 3.4.2.Final 1.0.0 @@ -145,7 +145,7 @@ 3.10.0 1.6 2.9.1 - 2.17.29 + 2.17.35 2.40.0 1.4.2 1.5.30 @@ -156,27 +156,28 @@ 4.1.0 1.0.9 5.12.0.202106070339-r - 7.14.0 + 7.15.0 1.0.9 4.4.3 + 4.4.3 1.29 6.0.0 4.3.4 - 4.3.1 + 4.3.2 1.2.1 2.17.0 - 0.33.8 + 0.33.9 3.14.9 5.1.2 0.1.0 - 5.7.0 + 5.7.2 2.2.0 5.2.SP4 2.1.SP2 5.2.Final 2.1.SP1 3.12.4 - 5.3.1 + 5.8.0 4.9.2 1.1.4.Final 14.0.0 @@ -352,6 +353,15 @@ import + + + com.oracle.database.jdbc + ojdbc-bom + ${oracle-jdbc.version} + pom + import + + io.smallrye.reactive @@ -818,6 +828,16 @@ quarkus-liquibase-deployment ${project.version} + + io.quarkus + quarkus-liquibase-mongodb + ${project.version} + + + io.quarkus + quarkus-liquibase-mongodb-deployment + ${project.version} + io.quarkus quarkus-hibernate-orm @@ -1540,13 +1560,6 @@ quarkus-vertx-http-deployment ${project.version} - - io.quarkus - quarkus-vertx-http-deployment - ${project.version} - test-jar - test - io.quarkus quarkus-vertx-web @@ -1607,6 +1620,11 @@ quarkus-mailer ${project.version} + + io.quarkus + quarkus-mailer-deployment + ${project.version} + io.quarkus quarkus-mongodb-client @@ -1924,6 +1942,21 @@ quarkus-amazon-common ${project.version} + + io.quarkus + quarkus-amazon-lambda-event-server + ${project.version} + + + io.quarkus + quarkus-amazon-lambda-http-event-server + ${project.version} + + + io.quarkus + quarkus-amazon-lambda-rest-event-server + ${project.version} + io.quarkus quarkus-amazon-common-deployment @@ -4488,11 +4521,6 @@ mssql-jdbc ${mssql-jdbc.version} - - com.oracle.database.jdbc - ojdbc11 - ${oracle-jdbc.version} - org.elasticsearch.client elasticsearch-rest-client @@ -5061,6 +5089,11 @@ + + org.liquibase.ext + liquibase-mongodb + ${liquibase-mongodb.version} + org.yaml snakeyaml diff --git a/build-parent/pom.xml b/build-parent/pom.xml index e7c6e7f70fe40c..605cba756dbd33 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -34,7 +34,7 @@ ${version.surefire.plugin} - 1.1.1 + 1.2.0 + + io.quarkus + quarkus-apache-httpclient + test + + + + + + + + diff --git a/extensions/amazon-lambda-http/http-event-server/src/main/java/io/quarkus/amazon/lambda/runtime/MockHttpEventServer.java b/extensions/amazon-lambda-http/http-event-server/src/main/java/io/quarkus/amazon/lambda/runtime/MockHttpEventServer.java new file mode 100644 index 00000000000000..c5f255c3ccebf4 --- /dev/null +++ b/extensions/amazon-lambda-http/http-event-server/src/main/java/io/quarkus/amazon/lambda/runtime/MockHttpEventServer.java @@ -0,0 +1,149 @@ +package io.quarkus.amazon.lambda.runtime; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; + +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.RoutingContext; + +public class MockHttpEventServer extends MockEventServer { + + private final ObjectMapper objectMapper; + private final ObjectWriter eventWriter; + private final ObjectReader responseReader; + + public MockHttpEventServer() { + objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); + eventWriter = objectMapper.writerFor(APIGatewayV2HTTPEvent.class); + responseReader = objectMapper.readerFor(APIGatewayV2HTTPResponse.class); + } + + @Override + protected void defaultHanderSetup() { + router.route().handler(this::handleHttpRequests); + } + + public void handleHttpRequests(RoutingContext ctx) { + String requestId = ctx.request().getHeader(AmazonLambdaApi.LAMBDA_RUNTIME_AWS_REQUEST_ID); + if (requestId == null) { + requestId = UUID.randomUUID().toString(); + } + ctx.put(AmazonLambdaApi.LAMBDA_RUNTIME_AWS_REQUEST_ID, requestId); + String traceId = ctx.request().getHeader(AmazonLambdaApi.LAMBDA_RUNTIME_AWS_REQUEST_ID); + if (traceId == null) { + traceId = UUID.randomUUID().toString(); + } + ctx.put(AmazonLambdaApi.LAMBDA_TRACE_HEADER_KEY, traceId); + Buffer body = ctx.getBody(); + + APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent(); + event.setRequestContext(new APIGatewayV2HTTPEvent.RequestContext()); + event.getRequestContext().setHttp(new APIGatewayV2HTTPEvent.RequestContext.Http()); + event.getRequestContext().getHttp().setMethod(ctx.request().method().name()); + event.setRawPath(ctx.request().path()); + event.setRawQueryString(ctx.request().query()); + for (String header : ctx.request().headers().names()) { + if (event.getHeaders() == null) + event.setHeaders(new HashMap<>()); + List values = ctx.request().headers().getAll(header); + String value = String.join(",", values); + event.getHeaders().put(header, value); + } + if (body != null) { + String ct = ctx.request().getHeader("content-type"); + if (ct == null || isBinary(ct)) { + String encoded = Base64.getMimeEncoder().encodeToString(body.getBytes()); + event.setBody(encoded); + event.setIsBase64Encoded(true); + } else { + event.setBody(new String(body.getBytes(), StandardCharsets.UTF_8)); + } + } + + try { + byte[] mEvent = eventWriter.writeValueAsBytes(event); + ctx.put(APIGatewayV2HTTPEvent.class.getName(), mEvent); + queue.put(ctx); + } catch (Exception e) { + log.error("Publish failure", e); + ctx.fail(500); + } + } + + @Override + protected String getEventContentType(RoutingContext request) { + if (request.get(APIGatewayV2HTTPEvent.class.getName()) != null) + return "application/json"; + return super.getEventContentType(request); + } + + @Override + protected Buffer processEventBody(RoutingContext request) { + byte[] buf = request.get(APIGatewayV2HTTPEvent.class.getName()); + if (buf != null) { + return Buffer.buffer(buf); + } + return super.processEventBody(request); + } + + @Override + public void processResponse(RoutingContext ctx, RoutingContext pending, Buffer buffer) { + if (pending.get(APIGatewayV2HTTPEvent.class.getName()) != null) { + try { + APIGatewayV2HTTPResponse res = responseReader.readValue(buffer.getBytes()); + HttpServerResponse response = pending.response(); + if (res.getHeaders() != null) { + for (Map.Entry header : res.getHeaders().entrySet()) { + for (String val : header.getValue().split(",")) { + response.headers().add(header.getKey(), val); + } + } + } + response.setStatusCode(res.getStatusCode()); + String body = res.getBody(); + if (body != null) { + if (res.getIsBase64Encoded()) { + byte[] bytes = Base64.getDecoder().decode(body); + response.end(Buffer.buffer(bytes)); + } else { + response.end(body); + } + } else { + response.end(); + } + + } catch (IOException e) { + log.error("Publish failure", e); + pending.fail(500); + } + } else { + super.processResponse(ctx, pending, buffer); + } + } + + private boolean isBinary(String contentType) { + if (contentType != null) { + String ct = contentType.toLowerCase(Locale.ROOT); + return !(ct.startsWith("text") || ct.contains("json") || ct.contains("xml") || ct.contains("yaml")); + } + return false; + } + +} diff --git a/extensions/amazon-lambda-http/http-event-server/src/test/java/io/quarkus/amazon/lambda/runtime/EventServerTest.java b/extensions/amazon-lambda-http/http-event-server/src/test/java/io/quarkus/amazon/lambda/runtime/EventServerTest.java new file mode 100644 index 00000000000000..831049128008b5 --- /dev/null +++ b/extensions/amazon-lambda-http/http-event-server/src/test/java/io/quarkus/amazon/lambda/runtime/EventServerTest.java @@ -0,0 +1,82 @@ +package io.quarkus.amazon.lambda.runtime; + +import java.util.HashMap; +import java.util.concurrent.Future; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; + +public class EventServerTest { + + static MockEventServer server; + static ObjectMapper mapper; + static ObjectReader eventReader; + static ObjectWriter resWriter; + + @BeforeAll + public static void start() { + server = new MockHttpEventServer(); + server.start(); + mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); + eventReader = mapper.readerFor(APIGatewayV2HTTPEvent.class); + resWriter = mapper.writerFor(APIGatewayV2HTTPResponse.class); + } + + @AfterAll + public static void end() throws Exception { + server.close(); + } + + @Test + public void testServer() throws Exception { + Client client = ClientBuilder.newBuilder().build(); + WebTarget base = client.target("http://localhost:" + MockEventServer.DEFAULT_PORT); + Future lambdaInvoke = base.request().async() + .post(Entity.text("Hello World")); + + Response next = base.path(MockEventServer.NEXT_INVOCATION).request().get(); + Assertions.assertEquals(200, next.getStatus()); + String requestId = next.getHeaderString(AmazonLambdaApi.LAMBDA_RUNTIME_AWS_REQUEST_ID); + String traceId = next.getHeaderString(AmazonLambdaApi.LAMBDA_TRACE_HEADER_KEY); + Assertions.assertNotNull(requestId); + Assertions.assertNotNull(traceId); + String json = next.readEntity(String.class); + APIGatewayV2HTTPEvent event = eventReader.readValue(json); + Assertions.assertEquals("text/plain", event.getHeaders().get("Content-Type")); + Assertions.assertEquals("Hello World", event.getBody()); + next.close(); + + APIGatewayV2HTTPResponse res = new APIGatewayV2HTTPResponse(); + res.setStatusCode(201); + res.setHeaders(new HashMap()); + res.getHeaders().put("Content-Type", "text/plain"); + res.setBody("Hi"); + Response sendResponse = base.path(MockEventServer.INVOCATION).path(requestId).path("response") + .request().post(Entity.json(resWriter.writeValueAsString(res))); + Assertions.assertEquals(204, sendResponse.getStatus()); + sendResponse.close(); + + Response lambdaResponse = lambdaInvoke.get(); + Assertions.assertEquals(201, lambdaResponse.getStatus()); + Assertions.assertEquals("Hi", lambdaResponse.readEntity(String.class)); + lambdaResponse.close(); + } +} diff --git a/extensions/amazon-lambda-http/pom.xml b/extensions/amazon-lambda-http/pom.xml index 61c49706a6b979..78c20a2fb26b2d 100644 --- a/extensions/amazon-lambda-http/pom.xml +++ b/extensions/amazon-lambda-http/pom.xml @@ -17,7 +17,9 @@ runtime + http-event-server deployment + maven-archetype diff --git a/extensions/amazon-lambda-http/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-lambda-http/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 3ee6b0b861cd66..295b2b80f286c1 100644 --- a/extensions/amazon-lambda-http/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-lambda-http/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -11,4 +11,4 @@ metadata: categories: - "cloud" guide: "https://quarkus.io/guides/amazon-lambda-http" - status: "preview" + status: "stable" diff --git a/extensions/amazon-lambda-rest/deployment/pom.xml b/extensions/amazon-lambda-rest/deployment/pom.xml index 32362c7132335c..97aa14362fcb06 100644 --- a/extensions/amazon-lambda-rest/deployment/pom.xml +++ b/extensions/amazon-lambda-rest/deployment/pom.xml @@ -39,6 +39,10 @@ io.quarkus quarkus-amazon-lambda-rest + + io.quarkus + quarkus-amazon-lambda-rest-event-server + diff --git a/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java b/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java index 68242ca8e9f806..822459c06fe63c 100644 --- a/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java +++ b/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java @@ -20,12 +20,10 @@ import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; -import io.quarkus.runtime.LaunchMode; import io.quarkus.vertx.http.deployment.RequireVirtualHttpBuildItem; import io.vertx.core.file.impl.FileResolver; @@ -46,8 +44,8 @@ public void setupSecurity(BuildProducer additionalBeans } @BuildStep - public RequireVirtualHttpBuildItem requestVirtualHttp(LaunchModeBuildItem launchMode) { - return launchMode.getLaunchMode() == LaunchMode.NORMAL ? RequireVirtualHttpBuildItem.MARKER : null; + public RequireVirtualHttpBuildItem requestVirtualHttp() { + return RequireVirtualHttpBuildItem.ALWAYS_VIRTUAL; } @BuildStep diff --git a/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/DevServicesRestLambdaProcessor.java b/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/DevServicesRestLambdaProcessor.java new file mode 100644 index 00000000000000..5e9f952f55badf --- /dev/null +++ b/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/DevServicesRestLambdaProcessor.java @@ -0,0 +1,14 @@ +package io.quarkus.amazon.lambda.http.deployment; + +import io.quarkus.amazon.lambda.deployment.EventServerOverrideBuildItem; +import io.quarkus.amazon.lambda.runtime.MockRestEventServer; +import io.quarkus.deployment.annotations.BuildStep; + +public class DevServicesRestLambdaProcessor { + + @BuildStep + public EventServerOverrideBuildItem overrideEventServer() { + return new EventServerOverrideBuildItem( + () -> new MockRestEventServer()); + } +} diff --git a/extensions/amazon-lambda-rest/pom.xml b/extensions/amazon-lambda-rest/pom.xml index 34cb1cf262363d..3e194bce78c241 100644 --- a/extensions/amazon-lambda-rest/pom.xml +++ b/extensions/amazon-lambda-rest/pom.xml @@ -17,6 +17,7 @@ runtime + rest-event-server deployment maven-archetype diff --git a/extensions/amazon-lambda-rest/rest-event-server/pom.xml b/extensions/amazon-lambda-rest/rest-event-server/pom.xml new file mode 100644 index 00000000000000..81b5fd7b97743b --- /dev/null +++ b/extensions/amazon-lambda-rest/rest-event-server/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + + io.quarkus + quarkus-amazon-lambda-rest-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-amazon-lambda-rest-event-server + Quarkus - Amazon Lambda REST Event Server + AWS Lambda REST Mock Lambda event server for testing and dev mode + + + + io.quarkus + quarkus-amazon-lambda-event-server + + + io.quarkus + quarkus-amazon-lambda-rest + + + org.junit.jupiter + junit-jupiter + test + + + org.jboss.resteasy + resteasy-client + test + + + commons-logging + commons-logging + + + jakarta.activation + jakarta.activation-api + + + + + + io.quarkus + quarkus-apache-httpclient + test + + + + + + + + diff --git a/extensions/amazon-lambda-rest/rest-event-server/src/main/java/io/quarkus/amazon/lambda/runtime/MockRestEventServer.java b/extensions/amazon-lambda-rest/rest-event-server/src/main/java/io/quarkus/amazon/lambda/runtime/MockRestEventServer.java new file mode 100644 index 00000000000000..4a3665eaf21889 --- /dev/null +++ b/extensions/amazon-lambda-rest/rest-event-server/src/main/java/io/quarkus/amazon/lambda/runtime/MockRestEventServer.java @@ -0,0 +1,173 @@ +package io.quarkus.amazon.lambda.runtime; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; + +import io.quarkus.amazon.lambda.http.model.AwsProxyRequest; +import io.quarkus.amazon.lambda.http.model.AwsProxyRequestContext; +import io.quarkus.amazon.lambda.http.model.AwsProxyResponse; +import io.quarkus.amazon.lambda.http.model.Headers; +import io.quarkus.amazon.lambda.http.model.MultiValuedTreeMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.RoutingContext; + +public class MockRestEventServer extends MockEventServer { + + private final ObjectMapper objectMapper; + private final ObjectWriter eventWriter; + private final ObjectReader responseReader; + + public MockRestEventServer() { + objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); + eventWriter = objectMapper.writerFor(AwsProxyRequest.class); + responseReader = objectMapper.readerFor(AwsProxyResponse.class); + } + + @Override + protected void defaultHanderSetup() { + router.route().handler(this::handleHttpRequests); + } + + public void handleHttpRequests(RoutingContext ctx) { + String requestId = ctx.request().getHeader(AmazonLambdaApi.LAMBDA_RUNTIME_AWS_REQUEST_ID); + if (requestId == null) { + requestId = UUID.randomUUID().toString(); + } + ctx.put(AmazonLambdaApi.LAMBDA_RUNTIME_AWS_REQUEST_ID, requestId); + String traceId = ctx.request().getHeader(AmazonLambdaApi.LAMBDA_RUNTIME_AWS_REQUEST_ID); + if (traceId == null) { + traceId = UUID.randomUUID().toString(); + } + ctx.put(AmazonLambdaApi.LAMBDA_TRACE_HEADER_KEY, traceId); + Buffer body = ctx.getBody(); + + AwsProxyRequest event = new AwsProxyRequest(); + event.setRequestContext(new AwsProxyRequestContext()); + event.getRequestContext().setRequestId(requestId); + event.getRequestContext().setHttpMethod(ctx.request().method().name()); + event.setHttpMethod(ctx.request().method().name()); + event.setPath(ctx.request().path()); + if (ctx.request().query() != null) { + event.setMultiValueQueryStringParameters(new MultiValuedTreeMap<>()); + String[] params = ctx.request().query().split("&"); + for (String param : params) { + if (param.contains("=")) { + String[] keyval = param.split("="); + try { + event.getMultiValueQueryStringParameters().add( + URLDecoder.decode(keyval[0], StandardCharsets.UTF_8.name()), + URLDecoder.decode(keyval[1], StandardCharsets.UTF_8.name())); + } catch (UnsupportedEncodingException e) { + log.error("Failed to parse query string", e); + ctx.response().setStatusCode(400).end(); + return; + } + } + } + + } + if (ctx.request().headers() != null) { + event.setMultiValueHeaders(new Headers()); + for (String header : ctx.request().headers().names()) { + List values = ctx.request().headers().getAll(header); + for (String val : values) + event.getMultiValueHeaders().add(header, val); + } + } + if (body != null) { + String ct = ctx.request().getHeader("content-type"); + if (ct == null || isBinary(ct)) { + String encoded = Base64.getMimeEncoder().encodeToString(body.getBytes()); + event.setBody(encoded); + event.setIsBase64Encoded(true); + } else { + event.setBody(new String(body.getBytes(), StandardCharsets.UTF_8)); + } + } + + try { + byte[] mEvent = eventWriter.writeValueAsBytes(event); + ctx.put(AwsProxyRequest.class.getName(), mEvent); + queue.put(ctx); + } catch (Exception e) { + log.error("Publish failure", e); + ctx.fail(500); + } + } + + @Override + protected String getEventContentType(RoutingContext request) { + if (request.get(AwsProxyRequest.class.getName()) != null) + return "application/json"; + return super.getEventContentType(request); + } + + @Override + protected Buffer processEventBody(RoutingContext request) { + byte[] buf = request.get(AwsProxyRequest.class.getName()); + if (buf != null) { + return Buffer.buffer(buf); + } + return super.processEventBody(request); + } + + @Override + public void processResponse(RoutingContext ctx, RoutingContext pending, Buffer buffer) { + if (pending.get(AwsProxyRequest.class.getName()) != null) { + try { + AwsProxyResponse res = responseReader.readValue(buffer.getBytes()); + HttpServerResponse response = pending.response(); + if (res.getMultiValueHeaders() != null) { + for (Map.Entry> header : res.getMultiValueHeaders().entrySet()) { + for (String val : header.getValue()) { + response.headers().add(header.getKey(), val); + } + } + } + response.setStatusCode(res.getStatusCode()); + String body = res.getBody(); + if (body != null) { + if (res.isBase64Encoded()) { + byte[] bytes = Base64.getDecoder().decode(body); + response.end(Buffer.buffer(bytes)); + } else { + response.end(body); + } + } else { + response.end(); + } + + } catch (IOException e) { + log.error("Publish failure", e); + pending.fail(500); + } + } else { + super.processResponse(ctx, pending, buffer); + } + } + + private boolean isBinary(String contentType) { + if (contentType != null) { + String ct = contentType.toLowerCase(Locale.ROOT); + return !(ct.startsWith("text") || ct.contains("json") || ct.contains("xml") || ct.contains("yaml")); + } + return false; + } + +} diff --git a/extensions/amazon-lambda-rest/rest-event-server/src/test/java/io/quarkus/amazon/lambda/runtime/EventServerTest.java b/extensions/amazon-lambda-rest/rest-event-server/src/test/java/io/quarkus/amazon/lambda/runtime/EventServerTest.java new file mode 100644 index 00000000000000..508ea7e4bbabb6 --- /dev/null +++ b/extensions/amazon-lambda-rest/rest-event-server/src/test/java/io/quarkus/amazon/lambda/runtime/EventServerTest.java @@ -0,0 +1,83 @@ +package io.quarkus.amazon.lambda.runtime; + +import java.util.HashMap; +import java.util.concurrent.Future; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; + +import io.quarkus.amazon.lambda.http.model.AwsProxyRequest; +import io.quarkus.amazon.lambda.http.model.AwsProxyResponse; + +public class EventServerTest { + + static MockEventServer server; + static ObjectMapper mapper; + static ObjectReader eventReader; + static ObjectWriter resWriter; + + @BeforeAll + public static void start() { + server = new MockRestEventServer(); + server.start(); + mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); + eventReader = mapper.readerFor(AwsProxyRequest.class); + resWriter = mapper.writerFor(AwsProxyResponse.class); + } + + @AfterAll + public static void end() throws Exception { + server.close(); + } + + @Test + public void testServer() throws Exception { + Client client = ClientBuilder.newBuilder().build(); + WebTarget base = client.target("http://localhost:" + MockEventServer.DEFAULT_PORT); + Future lambdaInvoke = base.request().async() + .post(Entity.text("Hello World")); + + Response next = base.path(MockEventServer.NEXT_INVOCATION).request().get(); + Assertions.assertEquals(200, next.getStatus()); + String requestId = next.getHeaderString(AmazonLambdaApi.LAMBDA_RUNTIME_AWS_REQUEST_ID); + String traceId = next.getHeaderString(AmazonLambdaApi.LAMBDA_TRACE_HEADER_KEY); + Assertions.assertNotNull(requestId); + Assertions.assertNotNull(traceId); + String json = next.readEntity(String.class); + AwsProxyRequest event = eventReader.readValue(json); + Assertions.assertEquals("text/plain", event.getMultiValueHeaders().getFirst("Content-Type")); + Assertions.assertEquals("Hello World", event.getBody()); + next.close(); + + AwsProxyResponse res = new AwsProxyResponse(); + res.setStatusCode(201); + res.setHeaders(new HashMap()); + res.getHeaders().put("Content-Type", "text/plain"); + res.setBody("Hi"); + Response sendResponse = base.path(MockEventServer.INVOCATION).path(requestId).path("response") + .request().post(Entity.json(resWriter.writeValueAsString(res))); + Assertions.assertEquals(204, sendResponse.getStatus()); + sendResponse.close(); + + Response lambdaResponse = lambdaInvoke.get(); + Assertions.assertEquals(201, lambdaResponse.getStatus()); + Assertions.assertEquals("Hi", lambdaResponse.readEntity(String.class)); + lambdaResponse.close(); + } +} diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/graal/LambdaContainerHandlerSubstitution.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/graal/LambdaContainerHandlerSubstitution.java index e2f2ede57953ab..d1c9ffebdd1960 100644 --- a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/graal/LambdaContainerHandlerSubstitution.java +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/graal/LambdaContainerHandlerSubstitution.java @@ -5,7 +5,7 @@ import com.oracle.svm.core.annotate.TargetClass; @TargetClass(LambdaContainerHandler.class) -public class LambdaContainerHandlerSubstitution { +public final class LambdaContainerHandlerSubstitution { // afterburner does not work in native mode, so let's ensure it's never registered @Substitute diff --git a/extensions/amazon-lambda-rest/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-lambda-rest/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 8636a6f8832a44..992fa650deacb1 100644 --- a/extensions/amazon-lambda-rest/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-lambda-rest/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -11,6 +11,6 @@ metadata: categories: - "cloud" guide: "https://quarkus.io/guides/amazon-lambda-http" - status: "preview" + status: "stable" config: - "quarkus.lambda-http." diff --git a/extensions/amazon-lambda-xray/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-lambda-xray/runtime/src/main/resources/META-INF/quarkus-extension.yaml index b68cb8deb0e6ad..64721449014f47 100644 --- a/extensions/amazon-lambda-xray/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-lambda-xray/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -10,4 +10,4 @@ metadata: guide: "https://quarkus.io/guides/amazon-lambda#tracing-with-aws-xray-and-graalvm" categories: - "cloud" - status: "preview" \ No newline at end of file + status: "stable" \ No newline at end of file diff --git a/extensions/amazon-lambda/common-deployment/pom.xml b/extensions/amazon-lambda/common-deployment/pom.xml index f2686d8d098c60..9e34d9282e0ce9 100644 --- a/extensions/amazon-lambda/common-deployment/pom.xml +++ b/extensions/amazon-lambda/common-deployment/pom.xml @@ -35,6 +35,10 @@ io.quarkus quarkus-amazon-lambda-common + + io.quarkus + quarkus-amazon-lambda-event-server + io.quarkus quarkus-junit5-internal diff --git a/extensions/amazon-lambda/common-deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaCommonProcessor.java b/extensions/amazon-lambda/common-deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaCommonProcessor.java index df692697ed573a..eafa77114b062e 100644 --- a/extensions/amazon-lambda/common-deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaCommonProcessor.java +++ b/extensions/amazon-lambda/common-deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaCommonProcessor.java @@ -5,7 +5,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.amazon.lambda.runtime.AmazonLambdaMapperRecorder; -import io.quarkus.amazon.lambda.runtime.LambdaBuildTimeConfig; import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.deployment.annotations.BuildProducer; @@ -75,12 +74,11 @@ public void initContextReaders(AmazonLambdaMapperRecorder recorder, @BuildStep @Record(value = ExecutionTime.STATIC_INIT) - void initContextReaders(LambdaBuildTimeConfig config, - AmazonLambdaMapperRecorder recorder, + void initContextReaders(AmazonLambdaMapperRecorder recorder, LambdaObjectMapperInitializedBuildItem dependency, LaunchModeBuildItem launchModeBuildItem) { LaunchMode mode = launchModeBuildItem.getLaunchMode(); - if (config.enablePollingJvmMode && mode.isDevOrTest()) { + if (mode.isDevOrTest()) { // only need context readers in native or dev or test mode recorder.initContextReaders(); } diff --git a/extensions/amazon-lambda/common-deployment/src/main/java/io/quarkus/amazon/lambda/deployment/DevServicesLambdaProcessor.java b/extensions/amazon-lambda/common-deployment/src/main/java/io/quarkus/amazon/lambda/deployment/DevServicesLambdaProcessor.java new file mode 100644 index 00000000000000..6b41e5100b0c96 --- /dev/null +++ b/extensions/amazon-lambda/common-deployment/src/main/java/io/quarkus/amazon/lambda/deployment/DevServicesLambdaProcessor.java @@ -0,0 +1,95 @@ +package io.quarkus.amazon.lambda.deployment; + +import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; + +import java.util.Optional; +import java.util.function.Supplier; + +import org.jboss.logging.Logger; + +import io.quarkus.amazon.lambda.runtime.AmazonLambdaApi; +import io.quarkus.amazon.lambda.runtime.LambdaHotReplacementRecorder; +import io.quarkus.amazon.lambda.runtime.MockEventServer; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.DevServicesNativeConfigResultBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.builditem.ServiceStartBuildItem; +import io.quarkus.runtime.LaunchMode; + +public class DevServicesLambdaProcessor { + private static final Logger log = Logger.getLogger(DevServicesLambdaProcessor.class); + + static MockEventServer server; + static LaunchMode startMode; + + @BuildStep(onlyIfNot = IsNormal.class) + @Record(STATIC_INIT) + public void enableHotReplacementChecker(LaunchModeBuildItem launchMode, + LambdaHotReplacementRecorder recorder, + LambdaObjectMapperInitializedBuildItem dependency) { + if (launchMode.getLaunchMode().isDevOrTest()) { + if (!legacyTestingEnabled()) { + recorder.enable(); + } + } + } + + private boolean legacyTestingEnabled() { + try { + Class.forName("io.quarkus.amazon.lambda.test.LambdaClient"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + @BuildStep(onlyIfNot = IsNormal.class) + public void startEventServer(LaunchModeBuildItem launchMode, + LambdaConfig config, + Optional override, + BuildProducer devServicePropertiesProducer, + BuildProducer serviceStartBuildItemBuildProducer) throws Exception { + if (!launchMode.getLaunchMode().isDevOrTest()) + return; + if (legacyTestingEnabled()) + return; + if (server != null) { + return; + } + Supplier supplier = null; + if (override.isPresent()) { + supplier = override.get().getServer(); + } else { + supplier = () -> new MockEventServer(); + } + + server = supplier.get(); + int port = launchMode.getLaunchMode() == LaunchMode.TEST ? config.mockEventServer.testPort + : config.mockEventServer.devPort; + startMode = launchMode.getLaunchMode(); + server.start(port); + String baseUrl = "localhost:" + port + MockEventServer.BASE_PATH; + System.setProperty(AmazonLambdaApi.QUARKUS_INTERNAL_AWS_LAMBDA_TEST_API, baseUrl); + devServicePropertiesProducer.produce( + new DevServicesNativeConfigResultBuildItem(AmazonLambdaApi.QUARKUS_INTERNAL_AWS_LAMBDA_TEST_API, baseUrl)); + Runnable closeTask = () -> { + if (server != null) { + try { + server.close(); + } catch (Throwable e) { + log.error("Failed to stop the Lambda Mock Event Server", e); + } finally { + server = null; + } + } + startMode = null; + server = null; + }; + QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread().getContextClassLoader(); + ((QuarkusClassLoader) cl.parent()).addCloseTask(closeTask); + } +} diff --git a/extensions/amazon-lambda/common-deployment/src/main/java/io/quarkus/amazon/lambda/deployment/EventServerOverrideBuildItem.java b/extensions/amazon-lambda/common-deployment/src/main/java/io/quarkus/amazon/lambda/deployment/EventServerOverrideBuildItem.java new file mode 100644 index 00000000000000..29644cf78b8b02 --- /dev/null +++ b/extensions/amazon-lambda/common-deployment/src/main/java/io/quarkus/amazon/lambda/deployment/EventServerOverrideBuildItem.java @@ -0,0 +1,18 @@ +package io.quarkus.amazon.lambda.deployment; + +import java.util.function.Supplier; + +import io.quarkus.amazon.lambda.runtime.MockEventServer; +import io.quarkus.builder.item.SimpleBuildItem; + +public final class EventServerOverrideBuildItem extends SimpleBuildItem { + private Supplier server; + + public EventServerOverrideBuildItem(Supplier server) { + this.server = server; + } + + public Supplier getServer() { + return server; + } +} diff --git a/extensions/amazon-lambda/common-deployment/src/main/java/io/quarkus/amazon/lambda/deployment/LambdaConfig.java b/extensions/amazon-lambda/common-deployment/src/main/java/io/quarkus/amazon/lambda/deployment/LambdaConfig.java new file mode 100644 index 00000000000000..e9dcc8e4c3e61f --- /dev/null +++ b/extensions/amazon-lambda/common-deployment/src/main/java/io/quarkus/amazon/lambda/deployment/LambdaConfig.java @@ -0,0 +1,14 @@ +package io.quarkus.amazon.lambda.deployment; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.BUILD_TIME) +public class LambdaConfig { + + /** + * Configuration for the mock event server that is run + * in dev mode and test mode + */ + MockEventServerConfig mockEventServer; +} diff --git a/extensions/amazon-lambda/common-deployment/src/main/java/io/quarkus/amazon/lambda/deployment/MockEventServerConfig.java b/extensions/amazon-lambda/common-deployment/src/main/java/io/quarkus/amazon/lambda/deployment/MockEventServerConfig.java new file mode 100644 index 00000000000000..8722ba40f47f62 --- /dev/null +++ b/extensions/amazon-lambda/common-deployment/src/main/java/io/quarkus/amazon/lambda/deployment/MockEventServerConfig.java @@ -0,0 +1,23 @@ +package io.quarkus.amazon.lambda.deployment; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +/** + * Configuration for the mock event server that is run + * in dev mode and test mode + */ +@ConfigGroup +public class MockEventServerConfig { + /** + * Port to access mock event server in dev mode + */ + @ConfigItem(defaultValue = "8080") + public int devPort; + + /** + * Port to access mock event server in dev mode + */ + @ConfigItem(defaultValue = "8081") + public int testPort; +} diff --git a/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AbstractLambdaPollLoop.java b/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AbstractLambdaPollLoop.java index db662c1139144b..1d7052ccc766df 100644 --- a/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AbstractLambdaPollLoop.java +++ b/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AbstractLambdaPollLoop.java @@ -33,23 +33,42 @@ public AbstractLambdaPollLoop(ObjectMapper objectMapper, ObjectReader cognitoIdR protected abstract boolean isStream(); - public void startPollLoop(ShutdownContext context) { + protected HttpURLConnection requestConnection = null; + public void startPollLoop(ShutdownContext context) { final AtomicBoolean running = new AtomicBoolean(true); + // flag to check whether to interrupt. + final AtomicBoolean shouldInterrupt = new AtomicBoolean(true); + String baseUrl = AmazonLambdaApi.baseUrl(); final Thread pollingThread = new Thread(new Runnable() { @SuppressWarnings("unchecked") @Override public void run() { - try { - checkQuarkusBootstrapped(); - URL requestUrl = AmazonLambdaApi.invocationNext(); + if (!LambdaHotReplacementRecorder.enabled) { + // when running with continuous testing, this method fails + // because currentApplication is not set when running as an + // auxiliary application. So, just skip it if hot replacement enabled. + // this is only needed in lambda JVM mode anyways to make sure + // quarkus has started. + checkQuarkusBootstrapped(); + } + URL requestUrl = AmazonLambdaApi.invocationNext(baseUrl); + if (AmazonLambdaApi.isTestMode()) { + // FYI: This log is required as native test runner + // looks for "Listening on" in log to ensure native executable booted + log.info("Listening on: " + requestUrl.toString()); + } while (running.get()) { - HttpURLConnection requestConnection = null; try { requestConnection = (HttpURLConnection) requestUrl.openConnection(); } catch (IOException e) { + if (!running.get()) { + // just return gracefully as we were probably shut down by + // shutdown task + return; + } if (abortGracefully(e)) { return; } @@ -57,10 +76,31 @@ public void run() { } try { String requestId = requestConnection.getHeaderField(AmazonLambdaApi.LAMBDA_RUNTIME_AWS_REQUEST_ID); + if (requestConnection.getResponseCode() != 200) { + // connection should be closed by finally clause + continue; + } try { + if (LambdaHotReplacementRecorder.enabled) { + try { + // do not interrupt during a hot replacement + // as shutdown will abort and do nasty things. + shouldInterrupt.set(false); + if (LambdaHotReplacementRecorder.checkHotReplacement()) { + // hot replacement happened in dev mode + // so we requeue the request as quarkus will restart + // and the message will not be processed + // FYI: this requeue endpoint is something only the mock event server implements + requeue(baseUrl, requestId); + return; + } + } finally { + shouldInterrupt.set(true); + } + } String traceId = requestConnection.getHeaderField(AmazonLambdaApi.LAMBDA_TRACE_HEADER_KEY); TraceId.setTraceId(traceId); - URL url = AmazonLambdaApi.invocationResponse(requestId); + URL url = AmazonLambdaApi.invocationResponse(baseUrl, requestId); if (isStream()) { HttpURLConnection responseConnection = responseStream(url); if (running.get()) { @@ -87,7 +127,7 @@ public void run() { } log.error("Failed to run lambda", e); - postError(AmazonLambdaApi.invocationError(requestId), + postError(AmazonLambdaApi.invocationError(baseUrl, requestId), new FunctionError(e.getClass().getName(), e.getMessage())); continue; } @@ -111,25 +151,34 @@ public void run() { } catch (Exception e) { try { log.error("Lambda init error", e); - postError(AmazonLambdaApi.initError(), new FunctionError(e.getClass().getName(), e.getMessage())); + postError(AmazonLambdaApi.initError(baseUrl), + new FunctionError(e.getClass().getName(), e.getMessage())); } catch (Exception ex) { log.error("Failed to report init error", ex); } finally { // our main loop is done, time to shutdown Application app = Application.currentApplication(); if (app != null) { + log.error("Shutting down Quarkus application because of error"); app.stop(); } } + } finally { + log.info("Lambda polling thread complete"); } } }, "Lambda Thread"); pollingThread.setDaemon(true); context.addShutdownTask(() -> { running.set(false); - //note that this does not seem to be 100% reliable in unblocking the thread - //which is why it is a daemon. - pollingThread.interrupt(); + try { + //note that interrupting does not seem to be 100% reliable in unblocking the thread + requestConnection.disconnect(); + } catch (Exception ignore) { + } + if (shouldInterrupt.get()) { + pollingThread.interrupt(); + } }); pollingThread.start(); @@ -167,10 +216,24 @@ private void checkQuarkusBootstrapped() { protected void postResponse(URL url, Object response) throws IOException { HttpURLConnection responseConnection = (HttpURLConnection) url.openConnection(); + if (response != null) { + getOutputWriter().writeHeaders(responseConnection); + } responseConnection.setDoOutput(true); responseConnection.setRequestMethod("POST"); - if (response != null) + if (response != null) { getOutputWriter().writeValue(responseConnection.getOutputStream(), response); + } + while (responseConnection.getInputStream().read() != -1) { + // Read data + } + } + + protected void requeue(String baseUrl, String requestId) throws IOException { + URL url = AmazonLambdaApi.requeue(baseUrl, requestId); + HttpURLConnection responseConnection = (HttpURLConnection) url.openConnection(); + responseConnection.setDoOutput(true); + responseConnection.setRequestMethod("POST"); while (responseConnection.getInputStream().read() != -1) { // Read data } @@ -178,6 +241,7 @@ protected void postResponse(URL url, Object response) throws IOException { protected void postError(URL url, Object response) throws IOException { HttpURLConnection responseConnection = (HttpURLConnection) url.openConnection(); + responseConnection.setRequestProperty("Content-Type", "application/json"); responseConnection.setDoOutput(true); responseConnection.setRequestMethod("POST"); objectMapper.writeValue(responseConnection.getOutputStream(), response); @@ -202,7 +266,7 @@ boolean abortGracefully(Exception ex) { || (ex instanceof UnknownHostException && !lambdaEnv); if (graceful) - log.warn("Aborting lambda poll loop: " + (!lambdaEnv ? "no lambda container found" : "test mode")); + log.warn("Aborting lambda poll loop: " + (lambdaEnv ? "no lambda container found" : "ending dev/test mode")); return graceful; } diff --git a/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AmazonLambdaApi.java b/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AmazonLambdaApi.java index a768c158bc3899..ec1e528ab38753 100644 --- a/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AmazonLambdaApi.java +++ b/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AmazonLambdaApi.java @@ -28,20 +28,35 @@ public class AmazonLambdaApi { public static final String API_PATH_ERROR = "/error"; public static final String API_PATH_RESPONSE = "/response"; - static URL invocationNext() throws MalformedURLException { - return new URL(API_PROTOCOL + runtimeApi() + API_PATH_INVOCATION_NEXT); + // Only available in dev/test mode and points to path for mock even tserver + public static final String API_BASE_PATH_TEST = "/_lambda_"; + public static final String POST_EVENT = API_BASE_PATH_TEST; + + // this is quarkus specific endpoint for dev mode + public static final String API_PATH_REQUEUE = "/requeue"; + + static String baseUrl() { + return API_PROTOCOL + runtimeApi(); + } + + static URL invocationNext(String baseUrl) throws MalformedURLException { + return new URL(baseUrl + API_PATH_INVOCATION_NEXT); } - static URL invocationError(String requestId) throws MalformedURLException { - return new URL(API_PROTOCOL + runtimeApi() + API_PATH_INVOCATION + requestId + API_PATH_ERROR); + static URL invocationError(String baseUrl, String requestId) throws MalformedURLException { + return new URL(baseUrl + API_PATH_INVOCATION + requestId + API_PATH_ERROR); } - static URL invocationResponse(String requestId) throws MalformedURLException { - return new URL(API_PROTOCOL + runtimeApi() + API_PATH_INVOCATION + requestId + API_PATH_RESPONSE); + static URL invocationResponse(String baseUrl, String requestId) throws MalformedURLException { + return new URL(baseUrl + API_PATH_INVOCATION + requestId + API_PATH_RESPONSE); } - static URL initError() throws MalformedURLException { - return new URL(API_PROTOCOL + runtimeApi() + API_PATH_INIT_ERROR); + static URL requeue(String baseUrl, String requestId) throws MalformedURLException { + return new URL(baseUrl + API_PATH_INVOCATION + requestId + API_PATH_REQUEUE); + } + + static URL initError(String baseUrl) throws MalformedURLException { + return new URL(baseUrl + API_PATH_INIT_ERROR); } static String logGroupName() { @@ -64,6 +79,10 @@ static String functionVersion() { return System.getenv("AWS_LAMBDA_FUNCTION_VERSION"); } + public static boolean isTestMode() { + return System.getProperty(AmazonLambdaApi.QUARKUS_INTERNAL_AWS_LAMBDA_TEST_API) != null; + } + private static String runtimeApi() { String testApi = System.getProperty(QUARKUS_INTERNAL_AWS_LAMBDA_TEST_API); if (testApi != null) { diff --git a/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/JacksonOutputWriter.java b/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/JacksonOutputWriter.java index d950a57f741cf5..137e8c75461059 100644 --- a/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/JacksonOutputWriter.java +++ b/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/JacksonOutputWriter.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.OutputStream; +import java.net.HttpURLConnection; import com.fasterxml.jackson.databind.ObjectWriter; @@ -16,4 +17,10 @@ public JacksonOutputWriter(ObjectWriter writer) { public void writeValue(OutputStream os, Object obj) throws IOException { writer.writeValue(os, obj); } + + @Override + public void writeHeaders(HttpURLConnection conn) { + conn.setRequestProperty("Content-Type", "application/json"); + } + } diff --git a/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaBuildTimeConfig.java b/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaBuildTimeConfig.java deleted file mode 100644 index 3af310748aa107..00000000000000 --- a/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaBuildTimeConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.quarkus.amazon.lambda.runtime; - -import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.runtime.annotations.ConfigPhase; -import io.quarkus.runtime.annotations.ConfigRoot; - -/** - * - */ -@ConfigRoot(phase = ConfigPhase.BUILD_TIME) -public class LambdaBuildTimeConfig { - - /** - * If true, this will enable the aws event poll loop within a Quarkus test run. This loop normally only runs in native - * image. This option is strictly for testing purposes. - * - */ - @ConfigItem - public boolean enablePollingJvmMode; -} diff --git a/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaHotReplacementRecorder.java b/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaHotReplacementRecorder.java new file mode 100644 index 00000000000000..8e187d637adad4 --- /dev/null +++ b/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaHotReplacementRecorder.java @@ -0,0 +1,49 @@ +package io.quarkus.amazon.lambda.runtime; + +import org.jboss.logging.Logger; + +import io.quarkus.dev.console.DevConsoleManager; +import io.quarkus.dev.spi.HotReplacementContext; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class LambdaHotReplacementRecorder { + private static final Logger log = Logger.getLogger(LambdaHotReplacementRecorder.class); + + static volatile long nextUpdate; + public static volatile boolean enabled; + private static final long HOT_REPLACEMENT_INTERVAL = 2000; + + static Object syncLock = new Object(); + + public static boolean checkHotReplacement() throws Exception { + if (!enabled) + return false; + HotReplacementContext hotReplacementContext = DevConsoleManager.getHotReplacementContext(); + if (hotReplacementContext == null) + return false; + + if ((nextUpdate > System.currentTimeMillis() && !hotReplacementContext.isTest())) { + if (hotReplacementContext.getDeploymentProblem() != null) { + throw new Exception("Hot Replacement Deployment issue", hotReplacementContext.getDeploymentProblem()); + } + return false; + } + synchronized (syncLock) { + if (nextUpdate < System.currentTimeMillis() || hotReplacementContext.isTest()) { + nextUpdate = System.currentTimeMillis() + HOT_REPLACEMENT_INTERVAL; + try { + boolean restart = hotReplacementContext.doScan(true); + return restart; + } catch (Exception e) { + throw new IllegalStateException("Unable to perform live reload scanning", e); + } + } + } + return false; + } + + public void enable() { + enabled = true; + } +} diff --git a/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaOutputWriter.java b/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaOutputWriter.java index d9de5be4e9ff08..a0895ab5302277 100644 --- a/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaOutputWriter.java +++ b/extensions/amazon-lambda/common-runtime/src/main/java/io/quarkus/amazon/lambda/runtime/LambdaOutputWriter.java @@ -2,7 +2,11 @@ import java.io.IOException; import java.io.OutputStream; +import java.net.HttpURLConnection; public interface LambdaOutputWriter { void writeValue(OutputStream os, Object obj) throws IOException; + + default void writeHeaders(HttpURLConnection conn) { + } } diff --git a/extensions/amazon-lambda/deployment/pom.xml b/extensions/amazon-lambda/deployment/pom.xml index 6748ee9112fa6e..8e5df28f1b6d4b 100644 --- a/extensions/amazon-lambda/deployment/pom.xml +++ b/extensions/amazon-lambda/deployment/pom.xml @@ -35,6 +35,11 @@ quarkus-junit5-internal test + + io.quarkus + quarkus-junit5 + test + diff --git a/extensions/amazon-lambda/deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaProcessor.java b/extensions/amazon-lambda/deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaProcessor.java index 128109ea225234..b755f0aae7f7b2 100644 --- a/extensions/amazon-lambda/deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaProcessor.java +++ b/extensions/amazon-lambda/deployment/src/main/java/io/quarkus/amazon/lambda/deployment/AmazonLambdaProcessor.java @@ -24,7 +24,6 @@ import io.quarkus.amazon.lambda.runtime.AmazonLambdaRecorder; import io.quarkus.amazon.lambda.runtime.FunctionError; -import io.quarkus.amazon.lambda.runtime.LambdaBuildTimeConfig; import io.quarkus.amazon.lambda.runtime.LambdaConfig; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; @@ -248,13 +247,12 @@ void startPoolLoop(AmazonLambdaRecorder recorder, @BuildStep @Record(value = ExecutionTime.RUNTIME_INIT) - void enableNativeEventLoop(LambdaBuildTimeConfig config, - AmazonLambdaRecorder recorder, + void startPoolLoopDevOrTest(AmazonLambdaRecorder recorder, List orderServicesFirst, // force some ordering of recorders ShutdownContextBuildItem shutdownContextBuildItem, LaunchModeBuildItem launchModeBuildItem) { LaunchMode mode = launchModeBuildItem.getLaunchMode(); - if (config.enablePollingJvmMode && mode.isDevOrTest()) { + if (mode.isDevOrTest()) { recorder.startPollLoop(shutdownContextBuildItem); } } diff --git a/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/GreetingLambda.java b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/GreetingLambda.java new file mode 100644 index 00000000000000..f62a7ef7eef955 --- /dev/null +++ b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/GreetingLambda.java @@ -0,0 +1,12 @@ +package io.quarkus.amazon.lambda.deployment.testing; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +public class GreetingLambda implements RequestHandler { + + @Override + public String handleRequest(Person input, Context context) { + return "Hey " + input.getName(); + } +} diff --git a/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/LambdaDevServicesContinuousTestingTestCase.java b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/LambdaDevServicesContinuousTestingTestCase.java new file mode 100644 index 00000000000000..1565016006b699 --- /dev/null +++ b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/LambdaDevServicesContinuousTestingTestCase.java @@ -0,0 +1,49 @@ +package io.quarkus.amazon.lambda.deployment.testing; + +import java.util.function.Supplier; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.ContinuousTestingTestUtils; +import io.quarkus.test.QuarkusDevModeTest; + +public class LambdaDevServicesContinuousTestingTestCase { + @RegisterExtension + public static QuarkusDevModeTest test = new QuarkusDevModeTest() + .setArchiveProducer(new Supplier() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(GreetingLambda.class, Person.class) + .addAsResource(new StringAsset(ContinuousTestingTestUtils.appProperties("")), + "application.properties"); + } + }).setTestArchiveProducer(new Supplier() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class).addClass(LambdaHandlerET.class); + } + }); + + @Test + public void testLambda() throws Exception { + ContinuousTestingTestUtils utils = new ContinuousTestingTestUtils(); + var result = utils.waitForNextCompletion(); + Assertions.assertEquals(1, result.getTotalTestsPassed()); + Assertions.assertEquals(0, result.getTotalTestsFailed()); + test.modifySourceFile(GreetingLambda.class, s -> s.replace("Hey", "Yo")); + result = utils.waitForNextCompletion(); + Assertions.assertEquals(0, result.getTotalTestsPassed()); + Assertions.assertEquals(1, result.getTotalTestsFailed()); + test.modifyTestSourceFile(LambdaHandlerET.class, s -> s.replace("Hey", "Yo")); + result = utils.waitForNextCompletion(); + Assertions.assertEquals(1, result.getTotalTestsPassed()); + Assertions.assertEquals(0, result.getTotalTestsFailed()); + + } +} diff --git a/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/LambdaHandlerET.java b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/LambdaHandlerET.java new file mode 100644 index 00000000000000..e37e209b3c221f --- /dev/null +++ b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/LambdaHandlerET.java @@ -0,0 +1,31 @@ +package io.quarkus.amazon.lambda.deployment.testing; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.containsString; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class LambdaHandlerET { + + @Test + public void testSimpleLambdaSuccess() throws Exception { + // you test your lambas by invoking on http://localhost:8081 + // this works in dev mode too + + Person in = new Person(); + in.setName("Stu"); + given() + .contentType("application/json") + .accept("application/json") + .body(in) + .when() + .post() + .then() + .statusCode(200) + .body(containsString("Hey Stu")); + } + +} diff --git a/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/Person.java b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/Person.java new file mode 100644 index 00000000000000..d2a4066a77dc29 --- /dev/null +++ b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/Person.java @@ -0,0 +1,15 @@ +package io.quarkus.amazon.lambda.deployment.testing; + +public class Person { + + private String name; + + public String getName() { + return name; + } + + public Person setName(String name) { + this.name = name; + return this; + } +} diff --git a/extensions/amazon-lambda/event-server/pom.xml b/extensions/amazon-lambda/event-server/pom.xml new file mode 100644 index 00000000000000..9c2b5daf911e1b --- /dev/null +++ b/extensions/amazon-lambda/event-server/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + + io.quarkus + quarkus-amazon-lambda-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-amazon-lambda-event-server + Quarkus - Amazon Lambda Event Server + Mock Lambda event server for testing and dev mode + + + + io.vertx + vertx-web + + + com.fasterxml.jackson.core + jackson-databind + + + + + io.quarkus + quarkus-amazon-lambda-common + + + org.junit.jupiter + junit-jupiter + test + + + org.jboss.resteasy + resteasy-client + test + + + commons-logging + commons-logging + + + jakarta.activation + jakarta.activation-api + + + + + + io.quarkus + quarkus-apache-httpclient + test + + + io.rest-assured + rest-assured + test + + + + + + + + diff --git a/extensions/amazon-lambda/event-server/src/main/java/io/quarkus/amazon/lambda/runtime/MockEventServer.java b/extensions/amazon-lambda/event-server/src/main/java/io/quarkus/amazon/lambda/runtime/MockEventServer.java new file mode 100644 index 00000000000000..cea6dfd37e2315 --- /dev/null +++ b/extensions/amazon-lambda/event-server/src/main/java/io/quarkus/amazon/lambda/runtime/MockEventServer.java @@ -0,0 +1,240 @@ +package io.quarkus.amazon.lambda.runtime; + +import java.io.Closeable; +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.jboss.logging.Logger; + +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpServer; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; + +public class MockEventServer implements Closeable { + protected static final Logger log = Logger.getLogger(MockEventServer.class); + public static final int DEFAULT_PORT = 8081; + + public void start() { + int port = DEFAULT_PORT; + start(port); + } + + public void start(int port) { + this.port = port; + vertx = Vertx.vertx(); + httpServer = vertx.createHttpServer(); + router = Router.router(vertx); + setupRoutes(); + httpServer.requestHandler(router).listen(port).result(); + log.info("Mock Lambda Event Server Started"); + } + + private Vertx vertx; + private int port; + protected HttpServer httpServer; + protected Router router; + protected BlockingQueue queue; + protected ConcurrentHashMap responsePending = new ConcurrentHashMap<>(); + protected ExecutorService blockingPool = Executors.newCachedThreadPool(); + public static final String BASE_PATH = AmazonLambdaApi.API_BASE_PATH_TEST; + public static final String INVOCATION = BASE_PATH + AmazonLambdaApi.API_PATH_INVOCATION; + public static final String NEXT_INVOCATION = BASE_PATH + AmazonLambdaApi.API_PATH_INVOCATION_NEXT; + public static final String POST_EVENT = BASE_PATH; + + public MockEventServer() { + queue = new LinkedBlockingQueue<>(); + } + + public HttpServer getHttpServer() { + return httpServer; + } + + public void setupRoutes() { + router.route().handler(BodyHandler.create()); + router.post(POST_EVENT).handler(this::postEvent); + router.route(NEXT_INVOCATION).blockingHandler(this::nextEvent); + router.route(INVOCATION + ":requestId" + AmazonLambdaApi.API_PATH_REQUEUE).handler(this::handleRequeue); + router.route(INVOCATION + ":requestId" + AmazonLambdaApi.API_PATH_RESPONSE).handler(this::handleResponse); + router.route(INVOCATION + ":requestId" + AmazonLambdaApi.API_PATH_ERROR).handler(this::handleError); + defaultHanderSetup(); + } + + protected void defaultHanderSetup() { + router.post().handler(this::postEvent); + } + + public void postEvent(RoutingContext ctx) { + String requestId = ctx.request().getHeader(AmazonLambdaApi.LAMBDA_RUNTIME_AWS_REQUEST_ID); + if (requestId == null) { + requestId = UUID.randomUUID().toString(); + } + ctx.put(AmazonLambdaApi.LAMBDA_RUNTIME_AWS_REQUEST_ID, requestId); + String traceId = ctx.request().getHeader(AmazonLambdaApi.LAMBDA_RUNTIME_AWS_REQUEST_ID); + if (traceId == null) { + traceId = UUID.randomUUID().toString(); + } + ctx.put(AmazonLambdaApi.LAMBDA_TRACE_HEADER_KEY, traceId); + try { + queue.put(ctx); + } catch (InterruptedException e) { + log.error("Publish interrupted"); + ctx.fail(500); + } + } + + private RoutingContext pollNextEvent() throws InterruptedException { + for (;;) { + RoutingContext request = queue.poll(10, TimeUnit.MILLISECONDS); + if (request != null) + return request; + + } + } + + public void nextEvent(RoutingContext ctx) { + // Vert.x barfs if you block too long so we have our own executor + blockingPool.execute(() -> { + final AtomicBoolean closed = new AtomicBoolean(false); + ctx.response().closeHandler((v) -> closed.set(true)); + RoutingContext request = null; + try { + for (;;) { + request = queue.poll(10, TimeUnit.MILLISECONDS); + if (request != null) { + if (closed.get()) { + queue.put(request); + return; + } else { + break; + } + } else if (closed.get()) { + return; + } + } + } catch (InterruptedException e) { + log.error("nextEvent interrupted"); + ctx.fail(500); + } + + String contentType = getEventContentType(request); + if (contentType != null) { + ctx.response().putHeader("content-type", contentType); + } + String traceId = request.get(AmazonLambdaApi.LAMBDA_TRACE_HEADER_KEY); + if (traceId != null) { + ctx.response().putHeader(AmazonLambdaApi.LAMBDA_TRACE_HEADER_KEY, traceId); + } + String requestId = request.get(AmazonLambdaApi.LAMBDA_RUNTIME_AWS_REQUEST_ID); + responsePending.put(requestId, request); + ctx.response().putHeader(AmazonLambdaApi.LAMBDA_RUNTIME_AWS_REQUEST_ID, requestId); + Buffer body = processEventBody(request); + if (body != null) { + ctx.response().setStatusCode(200).end(body); + } else { + ctx.response().setStatusCode(200).end(); + } + }); + } + + protected String getEventContentType(RoutingContext request) { + return request.request().getHeader("content-type"); + } + + protected Buffer processEventBody(RoutingContext request) { + return request.getBody(); + } + + public void handleResponse(RoutingContext ctx) { + String requestId = ctx.pathParam("requestId"); + RoutingContext pending = responsePending.remove(requestId); + if (pending == null) { + log.error("Unknown lambda request: " + requestId); + ctx.fail(404); + return; + } + Buffer buffer = ctx.getBody(); + processResponse(ctx, pending, buffer); + ctx.response().setStatusCode(204); + ctx.end(); + } + + public void handleRequeue(RoutingContext ctx) { + String requestId = ctx.pathParam("requestId"); + RoutingContext pending = responsePending.remove(requestId); + if (pending == null) { + log.error("Unknown lambda request: " + requestId); + ctx.fail(404); + return; + } + try { + queue.put(pending); + } catch (InterruptedException e) { + log.error("Publish interrupted"); + ctx.fail(500); + } + ctx.response().setStatusCode(204); + ctx.end(); + } + + public void processResponse(RoutingContext ctx, RoutingContext pending, Buffer buffer) { + if (buffer != null) { + if (ctx.request().getHeader("Content-Type") != null) { + pending.response().putHeader("Content-Type", ctx.request().getHeader("Content-Type")); + } + pending.response() + .setStatusCode(200) + .end(buffer); + } else { + pending.response() + .setStatusCode(204) + .end(); + } + } + + public void handleError(RoutingContext ctx) { + String requestId = ctx.pathParam("requestId"); + RoutingContext pending = responsePending.remove(requestId); + if (pending == null) { + log.error("Unknown lambda request: " + requestId); + ctx.fail(404); + return; + } + Buffer buffer = ctx.getBody(); + processError(ctx, pending, buffer); + ctx.response().setStatusCode(204); + ctx.end(); + } + + public void processError(RoutingContext ctx, RoutingContext pending, Buffer buffer) { + if (buffer != null) { + if (ctx.request().getHeader("Content-Type") != null) { + pending.response().putHeader("Content-Type", ctx.request().getHeader("Content-Type")); + } + pending.response() + .setStatusCode(500) + .end(buffer); + } else { + pending.response() + .setStatusCode(500) + .end(); + } + } + + @Override + public void close() throws IOException { + log.info("Stopping Mock Lambda Event Server"); + httpServer.close().result(); + vertx.close().result(); + blockingPool.shutdown(); + } +} diff --git a/extensions/amazon-lambda/event-server/src/test/java/io/quarkus/amazon/lambda/runtime/EventServerTest.java b/extensions/amazon-lambda/event-server/src/test/java/io/quarkus/amazon/lambda/runtime/EventServerTest.java new file mode 100644 index 00000000000000..d6f98929b281d9 --- /dev/null +++ b/extensions/amazon-lambda/event-server/src/test/java/io/quarkus/amazon/lambda/runtime/EventServerTest.java @@ -0,0 +1,66 @@ +package io.quarkus.amazon.lambda.runtime; + +import java.util.concurrent.Future; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class EventServerTest { + + static MockEventServer server; + + @BeforeAll + public static void start() { + server = new MockEventServer(); + server.start(); + } + + @AfterAll + public static void end() throws Exception { + server.close(); + } + + @Test + public void testServer() throws Exception { + Client client = ClientBuilder.newBuilder().build(); + WebTarget base = client.target("http://localhost:" + MockEventServer.DEFAULT_PORT); + WebTarget postEvent = base.path(MockEventServer.POST_EVENT); + // test posting event works at '/_lambda' + invokeTest(base, postEvent); + // make sure posting event works on '/' + invokeTest(base, base); + } + + private void invokeTest(WebTarget base, WebTarget postEvent) + throws InterruptedException, java.util.concurrent.ExecutionException { + Future lambdaInvoke = postEvent.request().async().post(Entity.json("\"hello\"")); + + Response next = base.path(MockEventServer.NEXT_INVOCATION).request().get(); + Assertions.assertEquals(200, next.getStatus()); + String requestId = next.getHeaderString(AmazonLambdaApi.LAMBDA_RUNTIME_AWS_REQUEST_ID); + String traceId = next.getHeaderString(AmazonLambdaApi.LAMBDA_TRACE_HEADER_KEY); + Assertions.assertNotNull(requestId); + Assertions.assertNotNull(traceId); + String json = next.readEntity(String.class); + Assertions.assertEquals("\"hello\"", json); + next.close(); + + Response sendResponse = base.path(MockEventServer.INVOCATION).path(requestId).path("response") + .request().post(Entity.json("\"good day\"")); + Assertions.assertEquals(204, sendResponse.getStatus()); + sendResponse.close(); + + Response lambdaResponse = lambdaInvoke.get(); + Assertions.assertEquals(200, lambdaResponse.getStatus()); + Assertions.assertEquals("\"good day\"", lambdaResponse.readEntity(String.class)); + lambdaResponse.close(); + } +} diff --git a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/build.gradle b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/build.gradle index a003f1174cfd5a..0609366f7346e9 100644 --- a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/build.gradle +++ b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/build.gradle @@ -10,10 +10,8 @@ repositories { dependencies { implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") - implementation 'io.quarkus:quarkus-resteasy' implementation 'io.quarkus:quarkus-amazon-lambda' - testImplementation "io.quarkus:quarkus-test-amazon-lambda" testImplementation 'io.quarkus:quarkus-junit5' testImplementation 'io.rest-assured:rest-assured' } diff --git a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml index b617576bb157ab..732ec60ca31e72 100644 --- a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml +++ b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/pom.xml @@ -36,8 +36,8 @@ quarkus-amazon-lambda - io.quarkus - quarkus-test-amazon-lambda + io.rest-assured + rest-assured test diff --git a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/src/test/java/LambdaHandlerTest.java b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/src/test/java/LambdaHandlerTest.java index 4f89d941f2b53b..69dc8f5452ba7c 100644 --- a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/src/test/java/LambdaHandlerTest.java +++ b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/src/test/java/LambdaHandlerTest.java @@ -1,22 +1,31 @@ package ${package}; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import io.quarkus.amazon.lambda.test.LambdaClient; import io.quarkus.test.junit.QuarkusTest; +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.containsString; + @QuarkusTest public class LambdaHandlerTest { @Test public void testSimpleLambdaSuccess() throws Exception { - InputObject in = new InputObject(); - in.setGreeting("Hello"); + // you test your lambas by invoking on http://localhost:8081 + // this works in dev mode too + + Person in = new Person(); in.setName("Stu"); - OutputObject out = LambdaClient.invoke(OutputObject.class, in); - Assertions.assertEquals("Hello Stu", out.getResult()); - Assertions.assertTrue(out.getRequestId().matches("aws-request-\\d"), "Expected requestId as 'aws-request-'"); + given() + .contentType("application/json") + .accept("application/json") + .body(in) + .when() + .post() + .then() + .statusCode(200) + .body(containsString("Hello Stu")); } } diff --git a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/src/test/resources/application.properties b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/src/test/resources/application.properties index 7c37ee7b060a83..6be7ba734f7956 100644 --- a/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/src/test/resources/application.properties +++ b/extensions/amazon-lambda/maven-archetype/src/main/resources/archetype-resources/src/test/resources/application.properties @@ -1,5 +1,2 @@ quarkus.lambda.handler=test -quarkus.lambda.enable-polling-jvm-mode=true - - diff --git a/extensions/amazon-lambda/pom.xml b/extensions/amazon-lambda/pom.xml index 71ccac1880aee3..ee3640b9fbf348 100644 --- a/extensions/amazon-lambda/pom.xml +++ b/extensions/amazon-lambda/pom.xml @@ -17,6 +17,7 @@ common-runtime + event-server common-deployment runtime deployment diff --git a/extensions/amazon-lambda/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-lambda/runtime/src/main/resources/META-INF/quarkus-extension.yaml index c19be1cae40cb5..495288d1d6b539 100644 --- a/extensions/amazon-lambda/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-lambda/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -8,7 +8,7 @@ metadata: categories: - "cloud" guide: "https://quarkus.io/guides/amazon-lambda" - status: "preview" + status: "stable" codestart: name: "amazon-lambda" kind: "example" diff --git a/extensions/amazon-services/common/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/common/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 4ea5a1d78f630b..86c58a06796c58 100644 --- a/extensions/amazon-services/common/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/common/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -8,5 +8,5 @@ metadata: - "amazon" categories: - "data" - status: "preview" + status: "stable" unlisted: true \ No newline at end of file diff --git a/extensions/amazon-services/dynamodb/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/dynamodb/runtime/src/main/resources/META-INF/quarkus-extension.yaml index ace051cb576d94..597bec9e4fb7bc 100644 --- a/extensions/amazon-services/dynamodb/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/dynamodb/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -10,6 +10,6 @@ metadata: guide: "https://quarkus.io/guides/amazon-dynamodb" categories: - "data" - status: "preview" + status: "stable" config: - "quarkus.dynamodb." diff --git a/extensions/amazon-services/iam/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/iam/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 5eb16f520dd059..0046f605a490ea 100644 --- a/extensions/amazon-services/iam/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/iam/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,6 +9,6 @@ metadata: guide: "https://quarkus.io/guides/amazon-iam" categories: - "data" - status: "preview" + status: "stable" config: - "quarkus.iam." diff --git a/extensions/amazon-services/kms/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/kms/runtime/src/main/resources/META-INF/quarkus-extension.yaml index c7cb65aa89ff8a..e6bc35fc57f554 100644 --- a/extensions/amazon-services/kms/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/kms/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,6 +9,6 @@ metadata: guide: "https://quarkus.io/guides/amazon-kms" categories: - "data" - status: "preview" + status: "stable" config: - "quarkus.kms." diff --git a/extensions/amazon-services/s3/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/s3/runtime/src/main/resources/META-INF/quarkus-extension.yaml index ed8bc6d64adc54..cfff54043b1190 100644 --- a/extensions/amazon-services/s3/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/s3/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,6 +9,6 @@ metadata: guide: "https://quarkus.io/guides/amazon-s3" categories: - "data" - status: "preview" + status: "stable" config: - "quarkus.s3." diff --git a/extensions/amazon-services/ses/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/ses/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 7133851af1215a..7b8c825793fe74 100644 --- a/extensions/amazon-services/ses/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/ses/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,6 +9,6 @@ metadata: guide: "https://quarkus.io/guides/amazon-ses" categories: - "data" - status: "preview" + status: "stable" config: - "quarkus.ses." diff --git a/extensions/amazon-services/sns/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/sns/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 87cab1d1233cfe..5df4b079013f87 100644 --- a/extensions/amazon-services/sns/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/sns/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,6 +9,6 @@ metadata: guide: "https://quarkus.io/guides/amazon-sns" categories: - "data" - status: "preview" + status: "stable" config: - "quarkus.sns." diff --git a/extensions/amazon-services/sqs/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/sqs/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 58366e56f9dc92..35623c807f85bb 100644 --- a/extensions/amazon-services/sqs/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/sqs/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,6 +9,6 @@ metadata: guide: "https://quarkus.io/guides/amazon-sqs" categories: - "data" - status: "preview" + status: "stable" config: - "quarkus.sqs." diff --git a/extensions/amazon-services/ssm/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/amazon-services/ssm/runtime/src/main/resources/META-INF/quarkus-extension.yaml index b3024a2dfb0878..ec7c7fbba307c1 100644 --- a/extensions/amazon-services/ssm/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/amazon-services/ssm/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,6 +9,6 @@ metadata: guide: "https://quarkus.io/guides/amazon-ssm" categories: - "data" - status: "preview" + status: "stable" config: - "quarkus.ssm." diff --git a/extensions/apicurio-registry-avro/deployment/src/main/java/io/quarkus/apicurio/registry/avro/DevServicesApicurioRegistryProcessor.java b/extensions/apicurio-registry-avro/deployment/src/main/java/io/quarkus/apicurio/registry/avro/DevServicesApicurioRegistryProcessor.java index 1147572ecc9bf9..b644e4a860a0a7 100644 --- a/extensions/apicurio-registry-avro/deployment/src/main/java/io/quarkus/apicurio/registry/avro/DevServicesApicurioRegistryProcessor.java +++ b/extensions/apicurio-registry-avro/deployment/src/main/java/io/quarkus/apicurio/registry/avro/DevServicesApicurioRegistryProcessor.java @@ -20,7 +20,10 @@ import io.quarkus.deployment.builditem.DevServicesConfigResultBuildItem; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.console.ConsoleInstalledBuildItem; +import io.quarkus.deployment.console.StartupLogCompressor; import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.deployment.logging.LoggingSetupBuildItem; import io.quarkus.devservices.common.ContainerLocator; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.configuration.ConfigUtils; @@ -57,7 +60,9 @@ public class DevServicesApicurioRegistryProcessor { public void startApicurioRegistryDevService(LaunchModeBuildItem launchMode, ApicurioRegistryDevServicesBuildTimeConfig apicurioRegistryDevServices, Optional devServicesSharedNetworkBuildItem, - BuildProducer devServicesConfiguration) { + BuildProducer devServicesConfiguration, + Optional consoleInstalledBuildItem, + LoggingSetupBuildItem loggingSetupBuildItem) { ApicurioRegistryDevServiceCfg configuration = getConfiguration(apicurioRegistryDevServices); @@ -69,11 +74,20 @@ public void startApicurioRegistryDevService(LaunchModeBuildItem launchMode, shutdownApicurioRegistry(); cfg = null; } - - ApicurioRegistry apicurioRegistry = startApicurioRegistry(configuration, launchMode, - devServicesSharedNetworkBuildItem.isPresent()); - if (apicurioRegistry == null) { - return; + ApicurioRegistry apicurioRegistry; + StartupLogCompressor compressor = new StartupLogCompressor( + (launchMode.isTest() ? "(test) " : "") + "Apicurio Registry Dev Services Starting:", + consoleInstalledBuildItem, loggingSetupBuildItem); + try { + apicurioRegistry = startApicurioRegistry(configuration, launchMode, + devServicesSharedNetworkBuildItem.isPresent()); + if (apicurioRegistry == null) { + return; + } + compressor.close(); + } catch (Throwable t) { + compressor.closeAndDumpCaptured(); + throw new RuntimeException(t); } cfg = configuration; diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchivePredicateBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchivePredicateBuildItem.java new file mode 100644 index 00000000000000..26aabbf1797463 --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchivePredicateBuildItem.java @@ -0,0 +1,25 @@ +package io.quarkus.arc.deployment; + +import java.util.function.Predicate; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.deployment.ApplicationArchive; + +/** + * + * By default, only explict/implicit bean archives (as defined by the spec) are considered during the bean discovery. However, + * extensions can register a logic to identify additional bean archives. + */ +public final class BeanArchivePredicateBuildItem extends MultiBuildItem { + + private final Predicate predicate; + + public BeanArchivePredicateBuildItem(Predicate predicate) { + this.predicate = predicate; + } + + public Predicate getPredicate() { + return predicate; + } + +} diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java index 2bb662fd5a7b04..8133de96db552a 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java @@ -1,6 +1,11 @@ package io.quarkus.arc.deployment; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import org.jboss.jandex.AnnotationInstance; @@ -35,12 +40,13 @@ public BeanArchiveIndexBuildItem build(ArcConfig config, ApplicationArchivesBuil List additionalBeanDefiningAnnotations, List additionalBeans, List generatedBeans, LiveReloadBuildItem liveReloadBuildItem, BuildProducer generatedClass, - CustomScopeAnnotationsBuildItem customScopes, List excludeDependencyBuildItems) + CustomScopeAnnotationsBuildItem customScopes, List excludeDependencyBuildItems, + List beanArchivePredicates) throws Exception { // First build an index from application archives IndexView applicationIndex = buildApplicationIndex(config, applicationArchivesBuildItem, - additionalBeanDefiningAnnotations, customScopes, excludeDependencyBuildItems); + additionalBeanDefiningAnnotations, customScopes, excludeDependencyBuildItems, beanArchivePredicates); // Then build additional index for beans added by extensions Indexer additionalBeanIndexer = new Indexer(); @@ -81,7 +87,8 @@ public BeanArchiveIndexBuildItem build(ArcConfig config, ApplicationArchivesBuil private IndexView buildApplicationIndex(ArcConfig config, ApplicationArchivesBuildItem applicationArchivesBuildItem, List additionalBeanDefiningAnnotations, - CustomScopeAnnotationsBuildItem customScopes, List excludeDependencyBuildItems) { + CustomScopeAnnotationsBuildItem customScopes, List excludeDependencyBuildItems, + List beanArchivePredicates) { Set archives = applicationArchivesBuildItem.getAllApplicationArchives(); @@ -116,10 +123,8 @@ private IndexView buildApplicationIndex(ArcConfig config, ApplicationArchivesBui continue; } IndexView index = archive.getIndex(); - // NOTE: Implicit bean archive without beans.xml contains one or more bean classes with a bean defining annotation and no extension - if (archive.getChildPath("META-INF/beans.xml") != null || archive.getChildPath("WEB-INF/beans.xml") != null - || (index.getAllKnownImplementors(DotNames.EXTENSION).isEmpty() - && containsBeanDefiningAnnotation(index, beanDefiningAnnotations))) { + if (isExplicitBeanArchive(archive) || isImplicitBeanArchive(index, beanDefiningAnnotations) + || isAdditionalBeanArchive(archive, beanArchivePredicates)) { indexes.add(index); } } @@ -127,6 +132,26 @@ && containsBeanDefiningAnnotation(index, beanDefiningAnnotations))) { return CompositeIndex.create(indexes); } + private boolean isExplicitBeanArchive(ApplicationArchive archive) { + return archive.getChildPath("META-INF/beans.xml") != null || archive.getChildPath("WEB-INF/beans.xml") != null; + } + + private boolean isImplicitBeanArchive(IndexView index, Set beanDefiningAnnotations) { + // NOTE: Implicit bean archive without beans.xml contains one or more bean classes with a bean defining annotation and no extension + return index.getAllKnownImplementors(DotNames.EXTENSION).isEmpty() + && containsBeanDefiningAnnotation(index, beanDefiningAnnotations); + } + + private boolean isAdditionalBeanArchive(ApplicationArchive archive, + List beanArchivePredicates) { + for (BeanArchivePredicateBuildItem p : beanArchivePredicates) { + if (p.getPredicate().test(archive)) { + return true; + } + } + return false; + } + private boolean isApplicationArchiveExcluded(ArcConfig config, List excludeDependencyBuildItems, ApplicationArchive archive) { if (archive.getArtifactKey() != null) { diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java index ee60809ab577f7..a0d855f1ce38ff 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigBuildStep.java @@ -1,12 +1,17 @@ package io.quarkus.arc.deployment; +import static io.quarkus.arc.deployment.ConfigClassBuildItem.Type.MAPPING; +import static io.quarkus.arc.deployment.ConfigClassBuildItem.Type.PROPERTIES; import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; -import static io.quarkus.deployment.configuration.ConfigMappingUtils.CONFIG_MAPPING_NAME; import static io.smallrye.config.ConfigMappings.ConfigClassWithPrefix.configClassWithPrefix; +import static java.util.Collections.emptySet; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; +import static org.eclipse.microprofile.config.inject.ConfigProperties.UNCONFIGURED_PREFIX; import static org.jboss.jandex.AnnotationInstance.create; import static org.jboss.jandex.AnnotationTarget.Kind.CLASS; +import static org.jboss.jandex.AnnotationTarget.Kind.FIELD; +import static org.jboss.jandex.AnnotationTarget.Kind.METHOD_PARAMETER; import static org.jboss.jandex.AnnotationValue.createStringValue; import java.util.ArrayList; @@ -14,6 +19,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Stream; @@ -25,8 +31,10 @@ import org.eclipse.microprofile.config.inject.ConfigProperties; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; import org.jboss.jandex.IndexView; @@ -46,15 +54,16 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; -import io.quarkus.deployment.builditem.ConfigClassBuildItem; import io.quarkus.deployment.builditem.ConfigurationBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.quarkus.deployment.configuration.ConfigMappingUtils; import io.quarkus.deployment.configuration.definition.RootDefinition; import io.quarkus.deployment.recording.RecorderContext; import io.quarkus.gizmo.ResultHandle; import io.quarkus.runtime.annotations.ConfigPhase; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.ConfigMappingLoader; +import io.smallrye.config.ConfigMappingMetadata; import io.smallrye.config.ConfigMappings.ConfigClassWithPrefix; import io.smallrye.config.inject.ConfigProducer; @@ -66,6 +75,7 @@ public class ConfigBuildStep { private static final DotName MP_CONFIG_PROPERTIES_NAME = DotName.createSimple(ConfigProperties.class.getName()); private static final DotName MP_CONFIG_VALUE_NAME = DotName.createSimple(ConfigValue.class.getName()); + private static final DotName CONFIG_MAPPING_NAME = DotName.createSimple(ConfigMapping.class.getName()); private static final DotName MAP_NAME = DotName.createSimple(Map.class.getName()); private static final DotName SET_NAME = DotName.createSimple(Set.class.getName()); private static final DotName LIST_NAME = DotName.createSimple(List.class.getName()); @@ -242,11 +252,79 @@ void generateConfigClasses( BuildProducer reflectiveClasses, BuildProducer configClasses) { - // TODO - Generation of Mapping interface classes can be done in core because they don't require CDI - ConfigMappingUtils.generateConfigClasses(combinedIndex, generatedClasses, reflectiveClasses, configClasses, - CONFIG_MAPPING_NAME); - ConfigMappingUtils.generateConfigClasses(combinedIndex, generatedClasses, reflectiveClasses, configClasses, - MP_CONFIG_PROPERTIES_NAME); + List mappingAnnotations = new ArrayList<>(); + mappingAnnotations.addAll(combinedIndex.getIndex().getAnnotations(CONFIG_MAPPING_NAME)); + mappingAnnotations.addAll(combinedIndex.getIndex().getAnnotations(MP_CONFIG_PROPERTIES_NAME)); + + for (AnnotationInstance instance : mappingAnnotations) { + AnnotationTarget target = instance.target(); + AnnotationValue annotationPrefix = instance.value("prefix"); + + if (target.kind().equals(FIELD)) { + if (annotationPrefix != null && !annotationPrefix.asString().equals(UNCONFIGURED_PREFIX)) { + configClasses.produce( + toConfigClassBuildItem(instance, toClass(target.asField().type().name()), + annotationPrefix.asString())); + continue; + } + } + + if (target.kind().equals(METHOD_PARAMETER)) { + if (annotationPrefix != null && !annotationPrefix.asString().equals(UNCONFIGURED_PREFIX)) { + ClassType classType = target.asMethodParameter().method().parameters() + .get(target.asMethodParameter().position()).asClassType(); + configClasses + .produce(toConfigClassBuildItem(instance, toClass(classType.name()), annotationPrefix.asString())); + continue; + } + } + + if (!target.kind().equals(CLASS)) { + continue; + } + + Class configClass = toClass(target.asClass().name()); + String prefix = Optional.ofNullable(annotationPrefix).map(AnnotationValue::asString).orElse(""); + + List configMappingsMetadata = ConfigMappingLoader.getConfigMappingsMetadata(configClass); + Set generatedClassesNames = new HashSet<>(); + Set mappingsInfo = new HashSet<>(); + configMappingsMetadata.forEach(mappingMetadata -> { + generatedClasses.produce( + new GeneratedClassBuildItem(true, mappingMetadata.getClassName(), mappingMetadata.getClassBytes())); + reflectiveClasses + .produce(ReflectiveClassBuildItem.builder(mappingMetadata.getInterfaceType()).methods(true).build()); + reflectiveClasses + .produce(ReflectiveClassBuildItem.builder(mappingMetadata.getClassName()).constructors(true).build()); + + for (Class parent : getHierarchy(mappingMetadata.getInterfaceType())) { + reflectiveClasses.produce(ReflectiveClassBuildItem.builder(parent).methods(true).build()); + } + + generatedClassesNames.add(mappingMetadata.getClassName()); + + ClassInfo mappingInfo = combinedIndex.getIndex() + .getClassByName(DotName.createSimple(mappingMetadata.getInterfaceType().getName())); + if (mappingInfo != null) { + mappingsInfo.add(mappingInfo); + } + }); + + // Search and register possible classes for implicit Converter methods + for (ClassInfo classInfo : mappingsInfo) { + for (MethodInfo method : classInfo.methods()) { + if (!isHandledByProducers(method.returnType()) && + mappingsInfo.stream() + .map(ClassInfo::name) + .noneMatch(name -> name.equals(method.returnType().name()))) { + reflectiveClasses + .produce(new ReflectiveClassBuildItem(true, false, method.returnType().name().toString())); + } + } + } + + configClasses.produce(toConfigClassBuildItem(instance, configClass, generatedClassesNames, prefix)); + } } @BuildStep @@ -327,6 +405,12 @@ void registerConfigClasses( configClassWithPrefix -> Stream.of(configClassWithPrefix.getKlass(), configClassWithPrefix.getPrefix()) .collect(toList())); + recorder.registerConfigMappings( + configClasses.stream() + .filter(ConfigClassBuildItem::isMapping) + .map(configMapping -> configClassWithPrefix(configMapping.getConfigClass(), configMapping.getPrefix())) + .collect(toSet())); + recorder.registerConfigProperties( configClasses.stream() .filter(ConfigClassBuildItem::isProperties) @@ -335,6 +419,45 @@ void registerConfigClasses( .collect(toSet())); } + private static Class toClass(DotName dotName) { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + try { + return classLoader.loadClass(dotName.toString()); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("The class (" + dotName.toString() + ") cannot be created during deployment.", e); + } + } + + private static ConfigClassBuildItem toConfigClassBuildItem( + AnnotationInstance instance, + Class configClass, + String prefix) { + return toConfigClassBuildItem(instance, configClass, emptySet(), prefix); + } + + private static ConfigClassBuildItem toConfigClassBuildItem( + AnnotationInstance instance, + Class configClass, + Set generatedClasses, + String prefix) { + if (instance.name().equals(CONFIG_MAPPING_NAME)) { + return new ConfigClassBuildItem(configClass, generatedClasses, prefix, MAPPING); + } else if (instance.name().equals(MP_CONFIG_PROPERTIES_NAME)) { + return new ConfigClassBuildItem(configClass, generatedClasses, prefix, PROPERTIES); + } else { + throw new IllegalArgumentException(); + } + } + + private static List> getHierarchy(Class mapping) { + List> interfaces = new ArrayList<>(); + for (Class i : mapping.getInterfaces()) { + interfaces.add(i); + interfaces.addAll(getHierarchy(i)); + } + return interfaces; + } + private String getPropertyName(String name, ClassInfo declaringClass) { StringBuilder builder = new StringBuilder(); if (declaringClass.enclosingClass() == null) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/ConfigClassBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigClassBuildItem.java similarity index 94% rename from core/deployment/src/main/java/io/quarkus/deployment/builditem/ConfigClassBuildItem.java rename to extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigClassBuildItem.java index fb3e80e17225e2..1d011ddbe5824d 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/ConfigClassBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ConfigClassBuildItem.java @@ -1,4 +1,4 @@ -package io.quarkus.deployment.builditem; +package io.quarkus.arc.deployment; import java.util.Set; @@ -48,6 +48,6 @@ public boolean isProperties() { public enum Type { MAPPING, - PROPERTIES + PROPERTIES; } } diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/config/ConfigMappingTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/config/ConfigMappingTest.java index 7c19fb0eba2f2e..019633add73fb9 100644 --- a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/config/ConfigMappingTest.java +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/config/ConfigMappingTest.java @@ -39,6 +39,7 @@ public class ConfigMappingTest { static final QuarkusUnitTest TEST = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addAsResource(new StringAsset("config.my.prop=1234\n" + + "config.override.my.prop=5678\n" + "group.host=localhost\n" + "group.port=8080\n" + "types.int=9\n" + @@ -354,15 +355,22 @@ void hierarchy() { @Dependent public static class ConstructorInjection { private String myProp; + private String overrideProp; @Inject - public ConstructorInjection(@ConfigMapping(prefix = "config") MyConfigMapping myConfigMapping) { + public ConstructorInjection(@ConfigMapping(prefix = "config") MyConfigMapping myConfigMapping, + @ConfigMapping(prefix = "config.override") MyConfigMapping override) { this.myProp = myConfigMapping.myProp(); + this.overrideProp = override.myProp(); } public String getMyProp() { return myProp; } + + public String getOverrideProp() { + return overrideProp; + } } @Inject @@ -371,5 +379,6 @@ public String getMyProp() { @Test void constructorInjection() { assertEquals("1234", constructorInjection.getMyProp()); + assertEquals("5678", constructorInjection.getOverrideProp()); } } diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java index e2afb940d65e58..dba24339b49038 100644 --- a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java +++ b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java @@ -136,7 +136,7 @@ private String createContainerImage(ContainerImageConfig containerImageConfig, D PackageConfig packageConfig) { DockerfilePaths dockerfilePaths = getDockerfilePaths(dockerConfig, forNative, packageConfig, out); - String[] dockerArgs = getDockerArgs(containerImageInfo.getImage(), dockerfilePaths, dockerConfig); + String[] dockerArgs = getDockerArgs(containerImageInfo.getImage(), dockerfilePaths, containerImageConfig, dockerConfig); log.infof("Executing the following command to build docker image: '%s %s'", dockerConfig.executableName, String.join(" ", dockerArgs)); boolean buildSuccessful = ExecUtil.exec(out.getOutputDirectory().toFile(), reader, dockerConfig.executableName, @@ -178,12 +178,16 @@ private String createContainerImage(ContainerImageConfig containerImageConfig, D return containerImageInfo.getImage(); } - private String[] getDockerArgs(String image, DockerfilePaths dockerfilePaths, DockerConfig dockerConfig) { + private String[] getDockerArgs(String image, DockerfilePaths dockerfilePaths, ContainerImageConfig containerImageConfig, + DockerConfig dockerConfig) { List dockerArgs = new ArrayList<>(6 + dockerConfig.buildArgs.size()); dockerArgs.addAll(Arrays.asList("build", "-f", dockerfilePaths.getDockerfilePath().toAbsolutePath().toString())); for (Map.Entry entry : dockerConfig.buildArgs.entrySet()) { dockerArgs.addAll(Arrays.asList("--build-arg", entry.getKey() + "=" + entry.getValue())); } + for (Map.Entry entry : containerImageConfig.labels.entrySet()) { + dockerArgs.addAll(Arrays.asList("--label", String.format("%s=%s", entry.getKey(), entry.getValue()))); + } if (dockerConfig.cacheFrom.isPresent()) { List cacheFrom = dockerConfig.cacheFrom.get(); if (!cacheFrom.isEmpty()) { diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibConfig.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibConfig.java index c1d476e7e52d99..f8f74492a59795 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibConfig.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibConfig.java @@ -85,8 +85,11 @@ public class JibConfig { /** * Custom labels to add to the generated image + * + * @deprecated Use 'quarkus.container-image.labels' instead */ @ConfigItem + @Deprecated public Map labels; /** diff --git a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java index 83e01cdd95028e..910bc79d776cca 100644 --- a/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java +++ b/extensions/container-image/container-image-jib/deployment/src/main/java/io/quarkus/container/image/jib/deployment/JibProcessor.java @@ -119,10 +119,11 @@ public void buildFromJar(ContainerImageConfig containerImageConfig, JibConfig ji JibContainerBuilder jibContainerBuilder; String packageType = packageConfig.type; if (packageConfig.isLegacyJar() || packageType.equalsIgnoreCase(PackageConfig.UBER_JAR)) { - jibContainerBuilder = createContainerBuilderFromLegacyJar(jibConfig, + jibContainerBuilder = createContainerBuilderFromLegacyJar(jibConfig, containerImageConfig, sourceJar, outputTarget, mainClass, containerImageLabels); } else if (packageConfig.isFastJar()) { - jibContainerBuilder = createContainerBuilderFromFastJar(jibConfig, sourceJar, curateOutcome, containerImageLabels, + jibContainerBuilder = createContainerBuilderFromFastJar(jibConfig, containerImageConfig, sourceJar, curateOutcome, + containerImageLabels, appCDSResult); } else { throw new IllegalArgumentException( @@ -161,7 +162,7 @@ public void buildFromNative(ContainerImageConfig containerImageConfig, JibConfig "The native binary produced by the build is not a Linux binary and therefore cannot be used in a Linux container image. Consider adding \"quarkus.native.container-build=true\" to your configuration"); } - JibContainerBuilder jibContainerBuilder = createContainerBuilderFromNative(containerImageConfig, jibConfig, + JibContainerBuilder jibContainerBuilder = createContainerBuilderFromNative(jibConfig, containerImageConfig, nativeImage, containerImageLabels); setUser(jibConfig, jibContainerBuilder); setPlatforms(jibConfig, jibContainerBuilder); @@ -280,6 +281,7 @@ private Logger.Level toJBossLoggingLevel(LogEvent.Level level) { * */ private JibContainerBuilder createContainerBuilderFromFastJar(JibConfig jibConfig, + ContainerImageConfig containerImageConfig, JarBuildItem sourceJarBuildItem, CurateOutcomeBuildItem curateOutcome, List containerImageLabels, Optional appCDSResult) { @@ -427,7 +429,7 @@ private JibContainerBuilder createContainerBuilderFromFastJar(JibConfig jibConfi .setWorkingDirectory(workDirInContainer) .setEntrypoint(entrypoint) .setEnvironment(getEnvironmentVariables(jibConfig)) - .setLabels(allLabels(jibConfig, containerImageLabels)) + .setLabels(allLabels(jibConfig, containerImageConfig, containerImageLabels)) .setCreationTime(Instant.now()); for (int port : jibConfig.ports) { jibContainerBuilder.addExposedPort(Port.tcp(port)); @@ -468,6 +470,7 @@ private void setPlatforms(JibConfig jibConfig, JibContainerBuilder jibContainerB } private JibContainerBuilder createContainerBuilderFromLegacyJar(JibConfig jibConfig, + ContainerImageConfig containerImageConfig, JarBuildItem sourceJarBuildItem, OutputTargetBuildItem outputTargetBuildItem, MainClassBuildItem mainClassBuildItem, @@ -502,7 +505,7 @@ private JibContainerBuilder createContainerBuilderFromLegacyJar(JibConfig jibCon JibContainerBuilder jibContainerBuilder = javaContainerBuilder.toContainerBuilder() .setEnvironment(getEnvironmentVariables(jibConfig)) - .setLabels(allLabels(jibConfig, containerImageLabels)) + .setLabels(allLabels(jibConfig, containerImageConfig, containerImageLabels)) .setCreationTime(Instant.now()); if (jibConfig.jvmEntrypoint.isPresent()) { @@ -517,7 +520,7 @@ private JibContainerBuilder createContainerBuilderFromLegacyJar(JibConfig jibCon } } - private JibContainerBuilder createContainerBuilderFromNative(ContainerImageConfig containerImageConfig, JibConfig jibConfig, + private JibContainerBuilder createContainerBuilderFromNative(JibConfig jibConfig, ContainerImageConfig containerImageConfig, NativeImageBuildItem nativeImageBuildItem, List containerImageLabels) { List entrypoint; @@ -532,8 +535,8 @@ private JibContainerBuilder createContainerBuilderFromNative(ContainerImageConfi try { AbsoluteUnixPath workDirInContainer = AbsoluteUnixPath.get("/work"); JibContainerBuilder jibContainerBuilder = Jib - .from(toRegistryImage(ImageReference.parse(jibConfig.baseNativeImage), containerImageConfig.username, - containerImageConfig.password)) + .from(toRegistryImage(ImageReference.parse(jibConfig.baseNativeImage), jibConfig.baseRegistryUsername, + jibConfig.baseRegistryPassword)) .addFileEntriesLayer(FileEntriesLayer.builder() .addEntry(nativeImageBuildItem.getPath(), workDirInContainer.resolve(BINARY_NAME_IN_CONTAINER), FilePermissions.fromOctalString("775")) @@ -541,7 +544,7 @@ private JibContainerBuilder createContainerBuilderFromNative(ContainerImageConfi .setWorkingDirectory(workDirInContainer) .setEntrypoint(entrypoint) .setEnvironment(getEnvironmentVariables(jibConfig)) - .setLabels(allLabels(jibConfig, containerImageLabels)) + .setLabels(allLabels(jibConfig, containerImageConfig, containerImageLabels)) .setCreationTime(Instant.now()); for (int port : jibConfig.ports) { jibContainerBuilder.addExposedPort(Port.tcp(port)); @@ -604,12 +607,14 @@ private void handleExtraFiles(OutputTargetBuildItem outputTarget, JibContainerBu } } - private Map allLabels(JibConfig jibConfig, List containerImageLabels) { + private Map allLabels(JibConfig jibConfig, ContainerImageConfig containerImageConfig, + List containerImageLabels) { if (jibConfig.labels.isEmpty() && containerImageLabels.isEmpty()) { return Collections.emptyMap(); } final Map allLabels = new HashMap<>(jibConfig.labels); + allLabels.putAll(containerImageConfig.labels); for (ContainerImageLabelBuildItem containerImageLabel : containerImageLabels) { // we want the user supplied labels to take precedence so the user can override labels generated from other extensions if desired allLabels.putIfAbsent(containerImageLabel.getName(), containerImageLabel.getValue()); diff --git a/extensions/container-image/container-image-openshift/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/container-image/container-image-openshift/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 5f6b78609781c9..9c1ee15c7c8bb0 100644 --- a/extensions/container-image/container-image-openshift/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/container-image/container-image-openshift/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -8,4 +8,4 @@ metadata: - "image" categories: - "cloud" - status: "preview" \ No newline at end of file + status: "stable" diff --git a/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageConfig.java b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageConfig.java index 87547b9f92a7b8..986326cfe50c79 100644 --- a/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageConfig.java +++ b/extensions/container-image/deployment/src/main/java/io/quarkus/container/image/deployment/ContainerImageConfig.java @@ -1,10 +1,13 @@ package io.quarkus.container.image.deployment; import java.util.List; +import java.util.Map; import java.util.Optional; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; +import io.quarkus.runtime.annotations.ConvertWith; +import io.quarkus.runtime.configuration.TrimmedStringConverter; @ConfigRoot public class ContainerImageConfig { @@ -13,12 +16,14 @@ public class ContainerImageConfig { * The group the container image will be part of */ @ConfigItem(defaultValue = "${user.name}") + @ConvertWith(TrimmedStringConverter.class) public Optional group; /** * The name of the container image. If not set defaults to the application name */ @ConfigItem(defaultValue = "${quarkus.application.name:unset}") + @ConvertWith(TrimmedStringConverter.class) public Optional name; /** @@ -33,6 +38,12 @@ public class ContainerImageConfig { @ConfigItem public Optional> additionalTags; + /** + * Custom labels to add to the generated image. + */ + @ConfigItem + public Map labels; + /** * The container registry to use */ @@ -95,6 +106,9 @@ public Optional getEffectiveGroup() { if (group.isPresent()) { String originalGroup = group.get(); if (originalGroup.equals(System.getProperty("user.name"))) { + if (originalGroup.isEmpty()) { + return Optional.empty(); + } return Optional.of(originalGroup.toLowerCase().replace(' ', '-')); } } diff --git a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java index 0881c79369314e..537dbbec2327c1 100644 --- a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java +++ b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java @@ -27,7 +27,10 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesConfigResultBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.console.ConsoleInstalledBuildItem; +import io.quarkus.deployment.console.StartupLogCompressor; import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.deployment.logging.LoggingSetupBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; import io.quarkus.runtime.LaunchMode; @@ -50,7 +53,9 @@ DevServicesDatasourceResultBuildItem launchDatabases(CurateOutcomeBuildItem cura DataSourcesBuildTimeConfig dataSourceBuildTimeConfig, LaunchModeBuildItem launchMode, List configurationHandlerBuildItems, - BuildProducer devServicesResultBuildItemBuildProducer) { + BuildProducer devServicesResultBuildItemBuildProducer, + Optional consoleInstalledBuildItem, + LoggingSetupBuildItem loggingSetupBuildItem) { //figure out if we need to shut down and restart existing databases //if not and the DB's have already started we just return if (databases != null) { @@ -111,60 +116,70 @@ DevServicesDatasourceResultBuildItem launchDatabases(CurateOutcomeBuildItem cura Map devDBProviderMap = devDBProviders.stream() .collect(Collectors.toMap(DevServicesDatasourceProviderBuildItem::getDatabase, DevServicesDatasourceProviderBuildItem::getDevServicesProvider)); - - defaultResult = startDevDb(null, curateOutcomeBuildItem, installedDrivers, - !dataSourceBuildTimeConfig.namedDataSources.isEmpty(), - devDBProviderMap, - dataSourceBuildTimeConfig.defaultDataSource, - configHandlersByDbType, propertiesMap, closeableList, launchMode.getLaunchMode()); - List dbConfig = new ArrayList<>(); - if (defaultResult != null) { - for (Map.Entry i : defaultResult.getConfigProperties().entrySet()) { - dbConfig.add(new DevServicesConfigResultBuildItem(i.getKey(), i.getValue())); - } - } - for (Map.Entry entry : dataSourceBuildTimeConfig.namedDataSources.entrySet()) { - DevServicesDatasourceResultBuildItem.DbResult result = startDevDb(entry.getKey(), curateOutcomeBuildItem, - installedDrivers, true, - devDBProviderMap, entry.getValue(), configHandlersByDbType, propertiesMap, closeableList, - launchMode.getLaunchMode()); - if (result != null) { - namedResults.put(entry.getKey(), result); - for (Map.Entry i : result.getConfigProperties().entrySet()) { + StartupLogCompressor compressor = new StartupLogCompressor( + (launchMode.isTest() ? "(test) " : "") + "Database Starting:", + consoleInstalledBuildItem, + loggingSetupBuildItem); + try { + defaultResult = startDevDb(null, curateOutcomeBuildItem, installedDrivers, + !dataSourceBuildTimeConfig.namedDataSources.isEmpty(), + devDBProviderMap, + dataSourceBuildTimeConfig.defaultDataSource, + configHandlersByDbType, propertiesMap, closeableList, launchMode.getLaunchMode()); + List dbConfig = new ArrayList<>(); + if (defaultResult != null) { + for (Map.Entry i : defaultResult.getConfigProperties().entrySet()) { dbConfig.add(new DevServicesConfigResultBuildItem(i.getKey(), i.getValue())); } } - } - for (DevServicesConfigResultBuildItem i : dbConfig) { - devServicesResultBuildItemBuildProducer - .produce(i); - } + for (Map.Entry entry : dataSourceBuildTimeConfig.namedDataSources.entrySet()) { + DevServicesDatasourceResultBuildItem.DbResult result = startDevDb(entry.getKey(), curateOutcomeBuildItem, + installedDrivers, true, + devDBProviderMap, entry.getValue(), configHandlersByDbType, propertiesMap, closeableList, + launchMode.getLaunchMode()); + if (result != null) { + namedResults.put(entry.getKey(), result); + for (Map.Entry i : result.getConfigProperties().entrySet()) { + dbConfig.add(new DevServicesConfigResultBuildItem(i.getKey(), i.getValue())); + } + } + } + for (DevServicesConfigResultBuildItem i : dbConfig) { + devServicesResultBuildItemBuildProducer + .produce(i); + } - if (first) { - first = false; - Runnable closeTask = new Runnable() { - @Override - public void run() { - if (databases != null) { - for (Closeable i : databases) { - try { - i.close(); - } catch (Throwable t) { - log.error("Failed to stop database", t); + if (first) { + first = false; + Runnable closeTask = new Runnable() { + @Override + public void run() { + if (databases != null) { + for (Closeable i : databases) { + try { + i.close(); + } catch (Throwable t) { + log.error("Failed to stop database", t); + } } } + first = true; + databases = null; + cachedProperties = null; } - first = true; - databases = null; - cachedProperties = null; - } - }; - QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread().getContextClassLoader(); - ((QuarkusClassLoader) cl.parent()).addCloseTask(closeTask); + }; + QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread().getContextClassLoader(); + ((QuarkusClassLoader) cl.parent()).addCloseTask(closeTask); + } + databases = closeableList; + cachedProperties = propertiesMap; + compressor.close(); + log.info("Dev Services for datasources started."); + return new DevServicesDatasourceResultBuildItem(defaultResult, namedResults); + } catch (Throwable t) { + compressor.closeAndDumpCaptured(); + throw new RuntimeException(t); } - databases = closeableList; - cachedProperties = propertiesMap; - return new DevServicesDatasourceResultBuildItem(defaultResult, namedResults); } private DevServicesDatasourceResultBuildItem.DbResult startDevDb(String dbName, diff --git a/extensions/elytron-security-jdbc/deployment/pom.xml b/extensions/elytron-security-jdbc/deployment/pom.xml index 0e71187e66be69..44c3c8f921931b 100644 --- a/extensions/elytron-security-jdbc/deployment/pom.xml +++ b/extensions/elytron-security-jdbc/deployment/pom.xml @@ -46,12 +46,6 @@ quarkus-resteasy-deployment test - - io.quarkus - quarkus-vertx-http-deployment - test-jar - test - io.quarkus quarkus-junit5-internal diff --git a/extensions/elytron-security-jdbc/deployment/src/test/java/io/quarkus/elytron/security/jdbc/CustomRoleDecoderDevModeTest.java b/extensions/elytron-security-jdbc/deployment/src/test/java/io/quarkus/elytron/security/jdbc/CustomRoleDecoderDevModeTest.java index b55a8c75daf900..772ebd49aeecf4 100644 --- a/extensions/elytron-security-jdbc/deployment/src/test/java/io/quarkus/elytron/security/jdbc/CustomRoleDecoderDevModeTest.java +++ b/extensions/elytron-security-jdbc/deployment/src/test/java/io/quarkus/elytron/security/jdbc/CustomRoleDecoderDevModeTest.java @@ -13,9 +13,9 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.deployment.util.FileUtil; +import io.quarkus.test.ContinuousTestingTestUtils; +import io.quarkus.test.ContinuousTestingTestUtils.TestStatus; import io.quarkus.test.QuarkusDevModeTest; -import io.quarkus.vertx.http.deployment.devmode.tests.TestStatus; -import io.quarkus.vertx.http.testrunner.ContinuousTestingTestUtils; import io.restassured.RestAssured; //see https://github.com/quarkusio/quarkus/issues/9296 diff --git a/extensions/funqy/funqy-amazon-lambda/deployment/src/main/java/io/quarkus/funqy/deployment/bindings/FunqyLambdaBuildStep.java b/extensions/funqy/funqy-amazon-lambda/deployment/src/main/java/io/quarkus/funqy/deployment/bindings/FunqyLambdaBuildStep.java index f6b374d81e519b..82887800d171c2 100644 --- a/extensions/funqy/funqy-amazon-lambda/deployment/src/main/java/io/quarkus/funqy/deployment/bindings/FunqyLambdaBuildStep.java +++ b/extensions/funqy/funqy-amazon-lambda/deployment/src/main/java/io/quarkus/funqy/deployment/bindings/FunqyLambdaBuildStep.java @@ -7,7 +7,6 @@ import java.util.Optional; import io.quarkus.amazon.lambda.deployment.LambdaObjectMapperInitializedBuildItem; -import io.quarkus.amazon.lambda.runtime.LambdaBuildTimeConfig; import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.builder.item.SimpleBuildItem; import io.quarkus.deployment.annotations.BuildProducer; @@ -67,14 +66,13 @@ public void startPoolLoop(FunqyLambdaBindingRecorder recorder, @BuildStep @Record(RUNTIME_INIT) - public void enableNativeEventLoop(LambdaBuildTimeConfig config, - RuntimeComplete ignored, + public void startPoolLoopDevOrTest(RuntimeComplete ignored, FunqyLambdaBindingRecorder recorder, List orderServicesFirst, // force some ordering of recorders ShutdownContextBuildItem shutdownContextBuildItem, LaunchModeBuildItem launchModeBuildItem) { LaunchMode mode = launchModeBuildItem.getLaunchMode(); - if (config.enablePollingJvmMode && mode.isDevOrTest()) { + if (mode.isDevOrTest()) { recorder.startPollLoop(shutdownContextBuildItem); } } diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java index dab50d36a386ca..ec726faa57c9ff 100644 --- a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java +++ b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServerProcessor.java @@ -40,6 +40,7 @@ import io.grpc.internal.ServerImpl; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; +import io.quarkus.arc.deployment.BeanArchivePredicateBuildItem; import io.quarkus.arc.deployment.CustomScopeAnnotationsBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; @@ -49,6 +50,7 @@ import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.arc.processor.BeanInfo; import io.quarkus.arc.processor.BuiltinScope; +import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; @@ -546,6 +548,18 @@ void configureMetrics(GrpcBuildTimeConfig configuration, Optional() { + + @Override + public boolean test(ApplicationArchive archive) { + // Every archive that contains a generated implementor of MutinyBean is considered a bean archive + return !archive.getIndex().getKnownDirectImplementors(GrpcDotNames.MUTINY_BEAN).isEmpty(); + } + }); + } + private static class GrpcInterceptors { final Set globalInterceptors; final Set nonGlobalInterceptors; diff --git a/extensions/grpc/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/grpc/runtime/src/main/resources/META-INF/quarkus-extension.yaml index ba8a04ffba04bc..2a2f8152e8fe75 100644 --- a/extensions/grpc/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/grpc/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -8,7 +8,7 @@ metadata: - "web" - "serialization" - "reactive" - status: "experimental" + status: "stable" codestart: name: "grpc" languages: diff --git a/extensions/hibernate-orm/deployment/pom.xml b/extensions/hibernate-orm/deployment/pom.xml index 75cea02da2efea..f1cd0a90c9730d 100644 --- a/extensions/hibernate-orm/deployment/pom.xml +++ b/extensions/hibernate-orm/deployment/pom.xml @@ -87,12 +87,6 @@ quarkus-smallrye-metrics-deployment test - - io.quarkus - quarkus-vertx-http-deployment - test - test-jar - org.awaitility awaitility diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java index 214456677d76fc..b51cb54fb66412 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java @@ -41,6 +41,7 @@ import javax.persistence.spi.PersistenceUnitTransactionType; import javax.transaction.TransactionManager; +import org.eclipse.microprofile.config.ConfigProvider; import org.hibernate.MultiTenancyStrategy; import org.hibernate.boot.archive.scan.spi.ClassDescriptor; import org.hibernate.boot.archive.scan.spi.PackageDescriptor; @@ -87,12 +88,14 @@ import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.DevServicesLauncherConfigResultBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.LiveReloadBuildItem; import io.quarkus.deployment.builditem.LogCategoryBuildItem; +import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; import io.quarkus.deployment.builditem.ServiceStartBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; @@ -177,6 +180,55 @@ void includeArchivesHostingEntityPackagesInIndex(HibernateOrmConfig hibernateOrm } } + @BuildStep + void devServicesAutoGenerateByDefault(DevServicesLauncherConfigResultBuildItem devServicesResult, + List schemaReadyBuildItems, + HibernateOrmConfig config, + BuildProducer runTimeConfigurationDefaultBuildItemBuildProducer) { + if (!schemaReadyBuildItems.isEmpty()) { + //we don't want to enable auto generation if somebody else is managing the schema + return; + } + String dsName; + if (config.defaultPersistenceUnit.datasource.isEmpty()) { + dsName = "quarkus.datasource.username"; + } else { + dsName = "quarkus.datasource." + config.defaultPersistenceUnit.datasource.get() + ".username"; + } + + if (ConfigProvider.getConfig().getOptionalValue(dsName, String.class).isEmpty()) { + if (devServicesResult.getConfig().containsKey(dsName)) { + if (ConfigProvider.getConfig().getOptionalValue("quarkus.hibernate-orm.database.generation", String.class) + .isEmpty()) { + LOG.info( + "Setting quarkus.hibernate-orm.database.generation=drop-and-create to initialize Dev Services managed database"); + runTimeConfigurationDefaultBuildItemBuildProducer.produce(new RunTimeConfigurationDefaultBuildItem( + "quarkus.hibernate-orm.database.generation", "drop-and-create")); + } + } + } + + for (Entry entry : config.persistenceUnits.entrySet()) { + + if (entry.getValue().datasource.isEmpty()) { + dsName = "quarkus.datasource.jdbc.url"; + } else { + dsName = "quarkus.datasource." + entry.getValue().datasource.get() + ".username"; + } + if (ConfigProvider.getConfig().getOptionalValue(dsName, String.class).isEmpty()) { + if (devServicesResult.getConfig().containsKey(dsName)) { + String propertyName = "quarkus.hibernate-orm." + entry.getKey() + ".database.generation"; + if (ConfigProvider.getConfig().getOptionalValue(propertyName, String.class).isEmpty()) { + LOG.info("Setting " + propertyName + "=drop-and-create to initialize Dev Services managed database"); + runTimeConfigurationDefaultBuildItemBuildProducer + .produce(new RunTimeConfigurationDefaultBuildItem(propertyName, "drop-and-create")); + } + } + } + } + + } + @BuildStep AdditionalIndexedClassesBuildItem addPersistenceUnitAnnotationToIndex() { return new AdditionalIndexedClassesBuildItem(ClassNames.QUARKUS_PERSISTENCE_UNIT.toString()); diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/HibernateHotReloadTestCase.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/HibernateHotReloadTestCase.java index 64a86b95bfe2b1..52c1bb4fb2f018 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/HibernateHotReloadTestCase.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/HibernateHotReloadTestCase.java @@ -11,9 +11,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.test.ContinuousTestingTestUtils; +import io.quarkus.test.ContinuousTestingTestUtils.TestStatus; import io.quarkus.test.QuarkusDevModeTest; -import io.quarkus.vertx.http.deployment.devmode.tests.TestStatus; -import io.quarkus.vertx.http.testrunner.ContinuousTestingTestUtils; import io.restassured.RestAssured; public class HibernateHotReloadTestCase { diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRuntimeConfigPersistenceUnit.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRuntimeConfigPersistenceUnit.java index 3ca13e90a43542..3be50c5a488342 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRuntimeConfigPersistenceUnit.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRuntimeConfigPersistenceUnit.java @@ -72,6 +72,9 @@ public static class HibernateOrmConfigPersistenceUnitDatabaseGeneration { * * `drop-and-create` is awesome in development mode. * + * This defaults to 'none', however if Dev Services is in use and no other extensions that manage the schema are present + * this will default to 'drop-and-create'. + * * Accepted values: `none`, `create`, `drop-and-create`, `drop`, `update`, `validate`. */ @ConfigItem(name = ConfigItem.PARENT, defaultValue = "none") diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/devconsole/HibernateOrmDevConsoleInfoSupplier.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/devconsole/HibernateOrmDevConsoleInfoSupplier.java index 2f16877db3326a..895182ae9e072c 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/devconsole/HibernateOrmDevConsoleInfoSupplier.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/devconsole/HibernateOrmDevConsoleInfoSupplier.java @@ -58,6 +58,12 @@ public static void pushPersistenceUnit(String persistenceUnitName, } } + public static void clearData() { + INSTANCE.persistenceUnits.clear(); + INSTANCE.createDDLs.clear(); + INSTANCE.dropDDLs.clear(); + } + private static String generateDDL(SchemaExport.Action action, Metadata metadata, ServiceRegistry serviceRegistry, String importFiles) { SchemaExport schemaExport = new SchemaExport(); diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/devconsole/HibernateOrmDevConsoleIntegrator.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/devconsole/HibernateOrmDevConsoleIntegrator.java index 59bb961e01c2cf..a7dae284eb97fe 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/devconsole/HibernateOrmDevConsoleIntegrator.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/devconsole/HibernateOrmDevConsoleIntegrator.java @@ -21,6 +21,6 @@ public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactor @Override public void disintegrate(SessionFactoryImplementor sessionFactoryImplementor, SessionFactoryServiceRegistry sessionFactoryServiceRegistry) { - // Nothing to do + HibernateOrmDevConsoleInfoSupplier.clearData(); } } diff --git a/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/HibernateValidatorProcessor.java b/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/HibernateValidatorProcessor.java index b854dcada8f552..aea65fd304db8d 100644 --- a/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/HibernateValidatorProcessor.java +++ b/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/HibernateValidatorProcessor.java @@ -49,6 +49,7 @@ import io.quarkus.arc.deployment.AutoAddScopeBuildItem; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; import io.quarkus.arc.deployment.BeanContainerListenerBuildItem; +import io.quarkus.arc.deployment.ConfigClassBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.arc.processor.BeanInfo; @@ -62,11 +63,11 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; -import io.quarkus.deployment.builditem.ConfigClassBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageConfigBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveFieldBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem; import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; @@ -349,9 +350,13 @@ NativeImageConfigBuildItem nativeImageConfig() { } @BuildStep - ExceptionMapperBuildItem mapper() { - return new ExceptionMapperBuildItem(ResteasyReactiveViolationExceptionMapper.class.getName(), - ValidationException.class.getName(), Priorities.USER + 1, true); + void exceptionMapper(BuildProducer exceptionMapperProducer, + BuildProducer reflectiveClassProducer) { + exceptionMapperProducer.produce(new ExceptionMapperBuildItem(ResteasyReactiveViolationExceptionMapper.class.getName(), + ValidationException.class.getName(), Priorities.USER + 1, true)); + reflectiveClassProducer.produce( + new ReflectiveClassBuildItem(true, true, ResteasyReactiveViolationExceptionMapper.ViolationReport.class, + ResteasyReactiveViolationExceptionMapper.ViolationReport.Violation.class)); } private static void contributeBuiltinConstraints(Set builtinConstraints, diff --git a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/jaxrs/ResteasyReactiveContextLocaleResolver.java b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/jaxrs/ResteasyReactiveContextLocaleResolver.java index 38366401d34e95..9f4023f7186993 100644 --- a/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/jaxrs/ResteasyReactiveContextLocaleResolver.java +++ b/extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/jaxrs/ResteasyReactiveContextLocaleResolver.java @@ -18,6 +18,11 @@ public ResteasyReactiveContextLocaleResolver(HttpHeaders headers) { @Override protected HttpHeaders getHeaders() { - return headers; + try { + headers.getLength(); // this forces the creation of the actual object which will fail if there is no request in flight + return headers; + } catch (IllegalStateException e) { + return null; + } } } diff --git a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java new file mode 100644 index 00000000000000..3970870f72b534 --- /dev/null +++ b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java @@ -0,0 +1,105 @@ +package io.quarkus.jdbc.oracle.deployment; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.nativeimage.ExcludeConfigBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageAllowIncompleteClasspathBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; + +/** + * The Oracle JDBC driver includes a {@literal META-INF/native-image} which enables a set + * of global flags we need to control better, so to ensure such flags do not interfere + * with requirements of other libraries. + *

+ * For this reason, the {@literal META-INF/native-image/native-image.properties} resource + * is excluded explicitly; then we re-implement the equivalent directives using Quarkus + * build items. + *

+ * Other resources such as {@literal jni-config.json} and {@literal resource-config.json} + * are not excluded, so to ensure we match the recommendations from the Oracle JDBC + * engineering team and make it easier to pick up improvements in these when the driver + * gets updated. + *

+ * Regarding {@literal reflect-config.json}, we also prefer excluding it for the time + * being even though it's strictly not necessary: the reason is that the previous driver + * version had a build-breaking mistake; this was fixed in version 21.3 so should no + * longer be necessary, but the previous driver had been tested more widely and would + * require it, so this would facilitate the option to revert to the older version in + * case of problems. + */ +public final class OracleMetadataOverrides { + + static final String DRIVER_JAR_MATCH_REGEX = ".*com\\.oracle\\.database\\.jdbc.*"; + static final String NATIVE_IMAGE_RESOURCE_MATCH_REGEX = "/META-INF/native-image/(?:native-image\\.properties|reflect-config\\.json)"; + + /** + * Should match the contents of {@literal reflect-config.json} + * + * @param reflectiveClass builItem producer + */ + @BuildStep + void build(BuildProducer reflectiveClass) { + //This is to match the Oracle metadata (which we excluded so that we can apply fixes): + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, false, "oracle.jdbc.internal.ACProxyable")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.jdbc.driver.T4CDriverExtension")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.jdbc.driver.T2CDriverExtension")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.jdbc.driver.ShardingDriverExtension")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.net.ano.Ano")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.net.ano.AuthenticationService")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.net.ano.DataIntegrityService")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.net.ano.EncryptionService")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.net.ano.SupervisorService")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.jdbc.driver.Message11")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, true, "oracle.sql.TypeDescriptor")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.sql.TypeDescriptorFactory")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, false, "oracle.sql.AnyDataFactory")); + reflectiveClass + .produce(new ReflectiveClassBuildItem(true, false, false, "com.sun.rowset.providers.RIOptimisticProvider")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, false, "oracle.jdbc.logging.annotations.Supports")); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, false, "oracle.jdbc.logging.annotations.Feature")); + } + + @BuildStep + void runtimeInitializeDriver(BuildProducer runtimeInitialized) { + //These re-implement all the "--initialize-at-build-time" arguments found in the native-image.properties : + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.OracleDriver")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.OracleDriver")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("java.sql.DriverManager")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.LogicalConnection")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.pool.OraclePooledConnection")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.pool.OracleDataSource")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.datasource.impl.OracleDataSource")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.pool.OracleOCIConnectionPool")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.OracleTimeoutThreadPerVM")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.net.nt.TimeoutInterruptHandler")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.HAManager")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.net.nt.Clock")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.net.nt.TcpMultiplexer")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.net.nt.TcpMultiplexer")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.net.nt.TcpMultiplexer$LazyHolder")); + runtimeInitialized + .produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.BlockSource$ThreadedCachingBlockSource")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem( + "oracle.jdbc.driver.BlockSource$ThreadedCachingBlockSource$BlockReleaser")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.xa.client.OracleXADataSource")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.replay.OracleXADataSourceImpl")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.datasource.OracleXAConnection")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.xa.client.OracleXAConnection")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.xa.client.OracleXAHeteroConnection")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.T4CXAConnection")); + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.security.o5logon.O5Logon")); + } + + @BuildStep + ExcludeConfigBuildItem excludeOracleDirectives() { + // Excludes both native-image.properties and reflect-config.json, which are reimplemented above + return new ExcludeConfigBuildItem(DRIVER_JAR_MATCH_REGEX, NATIVE_IMAGE_RESOURCE_MATCH_REGEX); + } + + @BuildStep + NativeImageAllowIncompleteClasspathBuildItem naughtyDriver() { + return new NativeImageAllowIncompleteClasspathBuildItem("quarkus-jdbc-oracle"); + } + +} diff --git a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleReflections.java b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleReflections.java index 53854430001ff2..118a24d4c1fddb 100644 --- a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleReflections.java +++ b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleReflections.java @@ -3,7 +3,6 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; /** * Registers the {@code oracle.jdbc.driver.OracleDriver} so that it can be loaded @@ -24,22 +23,4 @@ void build(BuildProducer reflectiveClass) { reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, driverName)); } - @BuildStep - void runtimeInitializeDriver(BuildProducer runtimeInitialized) { - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.OracleDriver")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.SQLUtil$XMLFactory")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.NamedTypeAccessor$XMLFactory")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.OracleTimeoutThreadPerVM")); - runtimeInitialized - .produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.BlockSource$ThreadedCachingBlockSource")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.T4CTTIoauthenticate")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.net.nt.TcpMultiplexer$LazyHolder")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.security.o5logon.O5Logon")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem( - "oracle.jdbc.driver.BlockSource$ThreadedCachingBlockSource$BlockReleaser")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.net.nt.TimeoutInterruptHandler")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.net.nt.Clock")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.NoSupportHAManager")); - runtimeInitialized.produce(new RuntimeInitializedClassBuildItem("oracle.jdbc.driver.LogicalConnection")); - } } diff --git a/extensions/jdbc/jdbc-oracle/deployment/src/test/java/io/quarkus/jdbc/oracle/deployment/RegexMatchTest.java b/extensions/jdbc/jdbc-oracle/deployment/src/test/java/io/quarkus/jdbc/oracle/deployment/RegexMatchTest.java new file mode 100644 index 00000000000000..5a66b4d51b26ab --- /dev/null +++ b/extensions/jdbc/jdbc-oracle/deployment/src/test/java/io/quarkus/jdbc/oracle/deployment/RegexMatchTest.java @@ -0,0 +1,45 @@ +package io.quarkus.jdbc.oracle.deployment; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; + +import io.smallrye.common.constraint.Assert; + +/** + * The metadata override facility of GraalVM's native-image + * works with regular expressions. + * We're testing our expressions here to match against the + * constants the compiler is expecting (inferred by debugging + * the compiler) as it's otherwise a bit tricky to assert + * if they have been applied. + */ +public class RegexMatchTest { + + @Test + public void jarRegexIsMatching() { + final String EXAMPLE_CLASSPATH = "/home/sanne/sources/quarkus/integration-tests/jpa-oracle/target/quarkus-integration-test-jpa-oracle-999-SNAPSHOT-native-image-source-jar/lib/com.oracle.database.jdbc.ojdbc11-21.3.0.0.jar"; + final Pattern pattern = Pattern.compile(OracleMetadataOverrides.DRIVER_JAR_MATCH_REGEX); + final Matcher matcher = pattern.matcher(EXAMPLE_CLASSPATH); + Assert.assertTrue(matcher.find()); + } + + @Test + public void resourceRegexIsMatching() { + //We need to exclude both of these: + final String RES1 = "/META-INF/native-image/native-image.properties"; + final String RES2 = "/META-INF/native-image/reflect-config.json"; + final Pattern pattern = Pattern.compile(OracleMetadataOverrides.NATIVE_IMAGE_RESOURCE_MATCH_REGEX); + + Assert.assertTrue(pattern.matcher(RES1).find()); + Assert.assertTrue(pattern.matcher(RES2).find()); + + //While this one should NOT be ignored: + final String RES3 = "/META-INF/native-image/resource-config.json"; + final String RES4 = "/META-INF/native-image/jni-config.json"; + Assert.assertFalse(pattern.matcher(RES3).find()); + Assert.assertFalse(pattern.matcher(RES4).find()); + } + +} diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/DevServicesKafkaProcessor.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/DevServicesKafkaProcessor.java index 25d1eb9a06cc7f..00cc224903b825 100644 --- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/DevServicesKafkaProcessor.java +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/DevServicesKafkaProcessor.java @@ -42,7 +42,10 @@ import io.quarkus.deployment.builditem.DevServicesConfigResultBuildItem; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.console.ConsoleInstalledBuildItem; +import io.quarkus.deployment.console.StartupLogCompressor; import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.deployment.logging.LoggingSetupBuildItem; import io.quarkus.devservices.common.ContainerAddress; import io.quarkus.devservices.common.ContainerLocator; import io.quarkus.runtime.LaunchMode; @@ -76,7 +79,9 @@ public DevServicesKafkaBrokerBuildItem startKafkaDevService( LaunchModeBuildItem launchMode, KafkaBuildTimeConfig kafkaClientBuildTimeConfig, Optional devServicesSharedNetworkBuildItem, - BuildProducer devServicePropertiesProducer) { + BuildProducer devServicePropertiesProducer, + Optional consoleInstalledBuildItem, + LoggingSetupBuildItem loggingSetupBuildItem) { KafkaDevServiceCfg configuration = getConfiguration(kafkaClientBuildTimeConfig); @@ -88,14 +93,25 @@ public DevServicesKafkaBrokerBuildItem startKafkaDevService( shutdownBroker(); cfg = null; } - - KafkaBroker kafkaBroker = startKafka(configuration, launchMode, devServicesSharedNetworkBuildItem.isPresent()); - DevServicesKafkaBrokerBuildItem bootstrapServers = null; - if (kafkaBroker != null) { - closeable = kafkaBroker.getCloseable(); - devServicePropertiesProducer.produce(new DevServicesConfigResultBuildItem( - KAFKA_BOOTSTRAP_SERVERS, kafkaBroker.getBootstrapServers())); - bootstrapServers = new DevServicesKafkaBrokerBuildItem(kafkaBroker.getBootstrapServers()); + KafkaBroker kafkaBroker; + DevServicesKafkaBrokerBuildItem bootstrapServers; + StartupLogCompressor compressor = new StartupLogCompressor( + (launchMode.isTest() ? "(test) " : "") + "Kafka Dev Services Starting:", + consoleInstalledBuildItem, loggingSetupBuildItem); + try { + + kafkaBroker = startKafka(configuration, launchMode, devServicesSharedNetworkBuildItem.isPresent()); + bootstrapServers = null; + if (kafkaBroker != null) { + closeable = kafkaBroker.getCloseable(); + devServicePropertiesProducer.produce(new DevServicesConfigResultBuildItem( + KAFKA_BOOTSTRAP_SERVERS, kafkaBroker.getBootstrapServers())); + bootstrapServers = new DevServicesKafkaBrokerBuildItem(kafkaBroker.getBootstrapServers()); + } + compressor.close(); + } catch (Throwable t) { + compressor.closeAndDumpCaptured(); + throw new RuntimeException(t); } // Configure the watch dog @@ -361,6 +377,7 @@ protected void containerIsStarting(InspectContainerResponse containerInfo, boole // Start and configure the advertised address String command = "#!/bin/bash\n"; command += "/usr/bin/rpk redpanda start --check=false --node-id 0 --smp 1 "; + command += "--memory 1G --overprovisioned --reserve-memory 0M "; command += "--kafka-addr PLAINTEXT://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092 "; command += String.format("--advertise-kafka-addr PLAINTEXT://%s:29092,OUTSIDE://%s:%d", getHostToUse(), getHostToUse(), getPortToUse()); diff --git a/extensions/kubernetes-client/deployment-internal/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientErrorHandler.java b/extensions/kubernetes-client/deployment-internal/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientErrorHandler.java index ed127a722a83bb..58f3eff0b634ef 100644 --- a/extensions/kubernetes-client/deployment-internal/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientErrorHandler.java +++ b/extensions/kubernetes-client/deployment-internal/src/main/java/io/quarkus/kubernetes/client/deployment/KubernetesClientErrorHandler.java @@ -12,11 +12,11 @@ public static void handle(Exception e) { if (e.getCause() instanceof SSLHandshakeException) { LOG.error( "The application could not be deployed to the cluster because the Kubernetes API Server certificates are not trusted. The certificates can be configured using the relevant configuration properties under the 'quarkus.kubernetes-client' config root, or \"quarkus.kubernetes-client.trust-certs=true\" can be set to explicitly trust the certificates (not recommended)"); - if (e instanceof RuntimeException) { - throw (RuntimeException) e; - } else { - throw new RuntimeException(e); - } + } + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else { + throw new RuntimeException(e); } } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesDeployer.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesDeployer.java index c358fb29ea50d5..362b63ae063578 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesDeployer.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesDeployer.java @@ -11,11 +11,12 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Path; -import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -25,6 +26,7 @@ import io.dekorate.utils.Serialization; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.KubernetesList; +import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.openshift.api.model.Route; @@ -42,7 +44,6 @@ import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.kubernetes.client.deployment.KubernetesClientErrorHandler; import io.quarkus.kubernetes.client.spi.KubernetesClientBuildItem; -import io.quarkus.kubernetes.spi.KubernetesDeploymentTargetBuildItem; public class KubernetesDeployer { @@ -60,7 +61,7 @@ public void selectDeploymentTarget(ContainerImageInfoBuildItem containerImageInf Optional activeContainerImageCapability = ContainerImageCapabilitiesUtil .getActiveContainerImageCapability(capabilities); - if (!activeContainerImageCapability.isPresent()) { + if (activeContainerImageCapability.isEmpty()) { // we can't thrown an exception here, because it could prevent the Kubernetes resources from being generated return; } @@ -85,10 +86,10 @@ public void deploy(KubernetesClientBuildItem kubernetesClient, return; } - if (!selectedDeploymentTarget.isPresent()) { + if (selectedDeploymentTarget.isEmpty()) { - if (!ContainerImageCapabilitiesUtil - .getActiveContainerImageCapability(capabilities).isPresent()) { + if (ContainerImageCapabilitiesUtil + .getActiveContainerImageCapability(capabilities).isEmpty()) { throw new RuntimeException( "A Kubernetes deployment was requested but no extension was found to build a container image. Consider adding one of following extensions: " + CONTAINER_IMAGE_EXTENSIONS_STR + "."); @@ -158,11 +159,6 @@ private DeploymentTargetEntry determineDeploymentTarget( return selectedTarget; } - private Optional getOptionalDeploymentTarget( - List deploymentTargets, String name) { - return deploymentTargets.stream().filter(d -> name.equals(d.getName())).findFirst(); - } - private DeploymentResultBuildItem deploy(DeploymentTargetEntry deploymentTarget, KubernetesClient client, Path outputDir, OpenshiftConfig openshiftConfig, ApplicationInfoBuildItem applicationInfo) { @@ -174,12 +170,13 @@ private DeploymentResultBuildItem deploy(DeploymentTargetEntry deploymentTarget, try (FileInputStream fis = new FileInputStream(manifest)) { KubernetesList list = Serialization.unmarshalAsList(fis); - distinct(list.getItems()).forEach(i -> { - if (KNATIVE.equals(deploymentTarget.getName().toLowerCase())) { - client.resource(i).inNamespace(namespace).deletingExisting().createOrReplace(); - } else { - client.resource(i).inNamespace(namespace).createOrReplace(); + list.getItems().stream().filter(distinctByResourceKey()).forEach(i -> { + final var r = client.resource(i).inNamespace(namespace); + if (shouldDeleteExisting(deploymentTarget, i)) { + r.delete(); + r.waitUntilCondition(Objects::isNull, 10, TimeUnit.SECONDS); } + r.createOrReplace(); log.info("Applied: " + i.getKind() + " " + i.getMetadata().getName() + "."); }); @@ -221,13 +218,15 @@ private void printExposeInformation(KubernetesClient client, KubernetesList list } } - public static Predicate distictByResourceKey() { + private static boolean shouldDeleteExisting(DeploymentTargetEntry deploymentTarget, HasMetadata resource) { + return KNATIVE.equalsIgnoreCase(deploymentTarget.getName()) + || resource instanceof Service + || (Objects.equals("v1", resource.getApiVersion()) && Objects.equals("Service", resource.getKind())); + } + + private static Predicate distinctByResourceKey() { Map seen = new ConcurrentHashMap<>(); return t -> seen.putIfAbsent(t.getApiVersion() + "/" + t.getKind() + ":" + t.getMetadata().getName(), Boolean.TRUE) == null; } - - private static Collection distinct(Collection resources) { - return resources.stream().filter(distictByResourceKey()).collect(Collectors.toList()); - } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java index 0f8251b1cc9064..d9b05f5a4123a6 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesProcessor.java @@ -169,7 +169,6 @@ public void build(ApplicationInfoBuildItem applicationInfo, Path targetPath = outputTarget.getOutputDirectory().resolve(KUBERNETES).resolve(fileName); String relativePath = targetPath.toAbsolutePath().toString().replace(root.toAbsolutePath().toString(), ""); - resourceEntry.getKey().replace(root.toAbsolutePath().toString(), KUBERNETES); if (fileName.endsWith(".yml") || fileName.endsWith(".json")) { String target = fileName.substring(0, fileName.lastIndexOf(".")); if (!deploymentTargets.contains(target)) { diff --git a/extensions/liquibase-mongodb/deployment/pom.xml b/extensions/liquibase-mongodb/deployment/pom.xml new file mode 100644 index 00000000000000..6cac71b48c6cd6 --- /dev/null +++ b/extensions/liquibase-mongodb/deployment/pom.xml @@ -0,0 +1,48 @@ + + + + quarkus-liquibase-mongodb-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-liquibase-mongodb-deployment + Quarkus - Liquibase MongoDB - Deployment + + + + io.quarkus + quarkus-liquibase-mongodb + + + io.quarkus + quarkus-mongodb-client-deployment + + + + io.quarkus + quarkus-junit5-internal + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/liquibase-mongodb/deployment/src/main/java/io/quarkus/liquibase/mongodb/deployment/LiquibaseProcessor.java b/extensions/liquibase-mongodb/deployment/src/main/java/io/quarkus/liquibase/mongodb/deployment/LiquibaseProcessor.java new file mode 100644 index 00000000000000..ec9ddc2a910ded --- /dev/null +++ b/extensions/liquibase-mongodb/deployment/src/main/java/io/quarkus/liquibase/mongodb/deployment/LiquibaseProcessor.java @@ -0,0 +1,325 @@ +package io.quarkus.liquibase.mongodb.deployment; + +import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import javax.enterprise.context.ApplicationScoped; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.DotName; +import org.jboss.logging.Logger; + +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem; +import io.quarkus.deployment.Feature; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Consume; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.ServiceStartBuildItem; +import io.quarkus.deployment.builditem.SystemPropertyBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; +import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; +import io.quarkus.deployment.util.ServiceUtil; +import io.quarkus.liquibase.LiquibaseMongodbFactory; +import io.quarkus.liquibase.runtime.LiquibaseMongodbBuildTimeConfig; +import io.quarkus.liquibase.runtime.LiquibaseMongodbConfig; +import io.quarkus.liquibase.runtime.LiquibaseMongodbRecorder; +import io.quarkus.mongodb.runtime.MongodbConfig; +import liquibase.change.Change; +import liquibase.change.DatabaseChangeProperty; +import liquibase.change.core.CreateProcedureChange; +import liquibase.change.core.CreateViewChange; +import liquibase.change.core.LoadDataChange; +import liquibase.change.core.SQLFileChange; +import liquibase.changelog.ChangeLogParameters; +import liquibase.changelog.ChangeSet; +import liquibase.changelog.DatabaseChangeLog; +import liquibase.exception.LiquibaseException; +import liquibase.parser.ChangeLogParser; +import liquibase.parser.ChangeLogParserFactory; +import liquibase.resource.ClassLoaderResourceAccessor; + +class LiquibaseProcessor { + + private static final Logger LOGGER = Logger.getLogger(LiquibaseProcessor.class); + + private static final String LIQUIBASE_BEAN_NAME_PREFIX = "liquibase_"; + + private static final DotName DATABASE_CHANGE_PROPERTY = DotName.createSimple(DatabaseChangeProperty.class.getName()); + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(Feature.LIQUIBASE_MONGODB); + } + + @BuildStep + public SystemPropertyBuildItem disableHub() { + // Don't block app startup with prompt: + // Do you want to see this operation's report in Liquibase Hub, which improves team collaboration? + // If so, enter your email. If not, enter [N] to no longer be prompted, or [S] to skip for now, but ask again next time (default "S"): + return new SystemPropertyBuildItem("liquibase.hub.mode", "off"); + } + + @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) + @Record(STATIC_INIT) + void nativeImageConfiguration( + LiquibaseMongodbRecorder recorder, + LiquibaseMongodbBuildTimeConfig liquibaseBuildConfig, + CombinedIndexBuildItem combinedIndex, + BuildProducer reflective, + BuildProducer resource, + BuildProducer services, + BuildProducer runtimeInitialized, + BuildProducer resourceBundle) { + + runtimeInitialized.produce(new RuntimeInitializedClassBuildItem(liquibase.diff.compare.CompareControl.class.getName())); + + reflective.produce(new ReflectiveClassBuildItem(false, true, false, + liquibase.change.AbstractSQLChange.class.getName(), + liquibase.database.jvm.JdbcConnection.class.getName())); + + reflective.produce(new ReflectiveClassBuildItem(true, true, true, + liquibase.parser.ChangeLogParserCofiguration.class.getName(), + liquibase.hub.HubServiceFactory.class.getName(), + liquibase.logging.core.DefaultLoggerConfiguration.class.getName(), + liquibase.configuration.GlobalConfiguration.class.getName(), + com.datical.liquibase.ext.config.LiquibaseProConfiguration.class.getName(), + liquibase.license.LicenseServiceFactory.class.getName(), + liquibase.executor.ExecutorService.class.getName(), + liquibase.change.ChangeFactory.class.getName(), + liquibase.logging.core.LogServiceFactory.class.getName(), + liquibase.logging.LogFactory.class.getName(), + liquibase.change.ColumnConfig.class.getName(), + liquibase.change.AddColumnConfig.class.getName(), + liquibase.change.core.LoadDataColumnConfig.class.getName(), + liquibase.sql.visitor.PrependSqlVisitor.class.getName(), + liquibase.sql.visitor.ReplaceSqlVisitor.class.getName(), + liquibase.sql.visitor.AppendSqlVisitor.class.getName(), + liquibase.sql.visitor.RegExpReplaceSqlVisitor.class.getName())); + + reflective.produce(new ReflectiveClassBuildItem(false, false, true, + liquibase.change.ConstraintsConfig.class.getName())); + + // register classes marked with @DatabaseChangeProperty for reflection + Set classesMarkedWithDatabaseChangeProperty = new HashSet<>(); + for (AnnotationInstance databaseChangePropertyInstance : combinedIndex.getIndex() + .getAnnotations(DATABASE_CHANGE_PROPERTY)) { + // the annotation is only supported on methods but let's be safe + AnnotationTarget annotationTarget = databaseChangePropertyInstance.target(); + if (annotationTarget.kind() == AnnotationTarget.Kind.METHOD) { + classesMarkedWithDatabaseChangeProperty.add(annotationTarget.asMethod().declaringClass().name().toString()); + } + } + reflective.produce( + new ReflectiveClassBuildItem(true, true, true, classesMarkedWithDatabaseChangeProperty.toArray(new String[0]))); + + resource.produce( + new NativeImageResourceBuildItem(getChangeLogs(liquibaseBuildConfig).toArray(new String[0]))); + + Stream.of(liquibase.change.Change.class, + liquibase.changelog.ChangeLogHistoryService.class, + liquibase.command.LiquibaseCommand.class, + liquibase.database.Database.class, + liquibase.database.DatabaseConnection.class, + liquibase.datatype.LiquibaseDataType.class, + liquibase.diff.compare.DatabaseObjectComparator.class, + liquibase.diff.DiffGenerator.class, + liquibase.diff.output.changelog.ChangeGenerator.class, + liquibase.executor.Executor.class, + liquibase.license.LicenseService.class, + liquibase.lockservice.LockService.class, + liquibase.logging.LogService.class, + liquibase.parser.ChangeLogParser.class, + liquibase.parser.NamespaceDetails.class, + liquibase.parser.SnapshotParser.class, + liquibase.precondition.Precondition.class, + liquibase.serializer.ChangeLogSerializer.class, + liquibase.serializer.SnapshotSerializer.class, + liquibase.servicelocator.ServiceLocator.class, + liquibase.snapshot.SnapshotGenerator.class, + liquibase.sqlgenerator.SqlGenerator.class, + liquibase.structure.DatabaseObject.class, + liquibase.hub.HubService.class) + .forEach(t -> addService(services, reflective, t, false)); + + // Register Precondition services, and the implementation class for reflection while also registering fields for reflection + addService(services, reflective, liquibase.precondition.Precondition.class, true); + + // liquibase XSD + resource.produce(new NativeImageResourceBuildItem( + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.6.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.7.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.9.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.10.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.0.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd", + "www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd", + "www.liquibase.org/xml/ns/pro/liquibase-pro-3.8.xsd", + "www.liquibase.org/xml/ns/pro/liquibase-pro-3.9.xsd", + "www.liquibase.org/xml/ns/pro/liquibase-pro-3.10.xsd", + "www.liquibase.org/xml/ns/pro/liquibase-pro-4.0.xsd", + "www.liquibase.org/xml/ns/pro/liquibase-pro-4.1.xsd", + "liquibase.build.properties")); + + // liquibase resource bundles + resourceBundle.produce(new NativeImageResourceBundleBuildItem("liquibase/i18n/liquibase-core")); + } + + private void addService(BuildProducer services, + BuildProducer reflective, Class serviceClass, + boolean shouldRegisterFieldForReflection) { + try { + String service = "META-INF/services/" + serviceClass.getName(); + Set implementations = ServiceUtil.classNamesNamedIn(Thread.currentThread().getContextClassLoader(), + service); + services.produce(new ServiceProviderBuildItem(serviceClass.getName(), implementations.toArray(new String[0]))); + + reflective.produce(new ReflectiveClassBuildItem(true, true, shouldRegisterFieldForReflection, + implementations.toArray(new String[0]))); + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + void createBeans(LiquibaseMongodbRecorder recorder, + LiquibaseMongodbConfig liquibaseMongodbConfig, + LiquibaseMongodbBuildTimeConfig liquibaseMongodbBuildTimeConfig, + MongodbConfig mongodbConfig, + BuildProducer syntheticBeanBuildItemBuildProducer) { + + SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem + .configure(LiquibaseMongodbFactory.class) + .scope(ApplicationScoped.class) // this is what the existing code does, but it doesn't seem reasonable + .setRuntimeInit() + .unremovable() + .supplier(recorder.liquibaseSupplier(liquibaseMongodbConfig, liquibaseMongodbBuildTimeConfig, mongodbConfig)); + + syntheticBeanBuildItemBuildProducer.produce(configurator.done()); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + @Consume(SyntheticBeansRuntimeInitBuildItem.class) + ServiceStartBuildItem startLiquibase(LiquibaseMongodbRecorder recorder) { + // will actually run the actions at runtime + recorder.doStartActions(); + + return new ServiceStartBuildItem("liquibase-mongodb"); + } + + /** + * Collect the configured changeLog file for the default and all named datasources. + *

+ * A {@link LinkedHashSet} is used to avoid duplications. + */ + private List getChangeLogs(LiquibaseMongodbBuildTimeConfig liquibaseBuildConfig) { + ChangeLogParameters changeLogParameters = new ChangeLogParameters(); + ClassLoaderResourceAccessor classLoaderResourceAccessor = new ClassLoaderResourceAccessor( + Thread.currentThread().getContextClassLoader()); + + ChangeLogParserFactory changeLogParserFactory = ChangeLogParserFactory.getInstance(); + + Set resources = new LinkedHashSet<>(); + + resources.addAll(findAllChangeLogFiles(liquibaseBuildConfig.changeLog, changeLogParserFactory, + classLoaderResourceAccessor, changeLogParameters)); + + LOGGER.debugf("Liquibase changeLogs: %s", resources); + + return new ArrayList<>(resources); + } + + /** + * Finds all resource files for the given change log file + */ + private Set findAllChangeLogFiles(String file, ChangeLogParserFactory changeLogParserFactory, + ClassLoaderResourceAccessor classLoaderResourceAccessor, + ChangeLogParameters changeLogParameters) { + try { + ChangeLogParser parser = changeLogParserFactory.getParser(file, classLoaderResourceAccessor); + DatabaseChangeLog changelog = parser.parse(file, changeLogParameters, classLoaderResourceAccessor); + + if (changelog != null) { + Set result = new LinkedHashSet<>(); + // get all changeSet files + for (ChangeSet changeSet : changelog.getChangeSets()) { + result.add(changeSet.getFilePath()); + + changeSet.getChanges().stream() + .map(change -> extractChangeFile(change, changeSet.getFilePath())) + .forEach(changeFile -> changeFile.ifPresent(result::add)); + + // get all parents of the changeSet + DatabaseChangeLog parent = changeSet.getChangeLog(); + while (parent != null) { + result.add(parent.getFilePath()); + parent = parent.getParentChangeLog(); + } + } + result.add(changelog.getFilePath()); + return result; + } + } catch (LiquibaseException ex) { + throw new IllegalStateException(ex); + } + return Collections.emptySet(); + } + + private Optional extractChangeFile(Change change, String changeSetFilePath) { + String path = null; + Boolean relative = null; + if (change instanceof LoadDataChange) { + LoadDataChange loadDataChange = (LoadDataChange) change; + path = loadDataChange.getFile(); + relative = loadDataChange.isRelativeToChangelogFile(); + } else if (change instanceof SQLFileChange) { + SQLFileChange sqlFileChange = (SQLFileChange) change; + path = sqlFileChange.getPath(); + relative = sqlFileChange.isRelativeToChangelogFile(); + } else if (change instanceof CreateProcedureChange) { + CreateProcedureChange createProcedureChange = (CreateProcedureChange) change; + path = createProcedureChange.getPath(); + relative = createProcedureChange.isRelativeToChangelogFile(); + } else if (change instanceof CreateViewChange) { + CreateViewChange createViewChange = (CreateViewChange) change; + path = createViewChange.getPath(); + relative = createViewChange.getRelativeToChangelogFile(); + } + + // unrelated change or change does not reference a file (e.g. inline view) + if (path == null) { + return Optional.empty(); + } + // absolute file path or changeSet has no file path + if (relative == null || !relative || changeSetFilePath == null) { + return Optional.of(path); + } + + // relative file path needs to be resolved against changeSetFilePath + // notes: ClassLoaderResourceAccessor does not provide a suitable method and CLRA.getFinalPath() is not visible + return Optional.of(Paths.get(changeSetFilePath).resolveSibling(path).toString().replace('\\', '/')); + } +} diff --git a/extensions/liquibase-mongodb/pom.xml b/extensions/liquibase-mongodb/pom.xml new file mode 100644 index 00000000000000..f9432f43440cb1 --- /dev/null +++ b/extensions/liquibase-mongodb/pom.xml @@ -0,0 +1,22 @@ + + + + quarkus-extensions-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-liquibase-mongodb-parent + Quarkus - Liquibase MongoDB + pom + + + runtime + deployment + + + \ No newline at end of file diff --git a/extensions/liquibase-mongodb/runtime/pom.xml b/extensions/liquibase-mongodb/runtime/pom.xml new file mode 100644 index 00000000000000..e027988136da56 --- /dev/null +++ b/extensions/liquibase-mongodb/runtime/pom.xml @@ -0,0 +1,71 @@ + + + + quarkus-liquibase-mongodb-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-liquibase-mongodb + Quarkus - Liquibase MongoDB - Runtime + Handle your MongoDB schema migrations with Liquibase + + + io.quarkus + quarkus-mongodb-client + + + org.liquibase + liquibase-core + + + org.yaml + snakeyaml + + + org.liquibase.ext + liquibase-mongodb + + + org.graalvm.nativeimage + svm + provided + + + + + io.quarkus + quarkus-junit5-internal + test + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + io.quarkus.liquibase.mongodb + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/LiquibaseMongodbFactory.java b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/LiquibaseMongodbFactory.java new file mode 100644 index 00000000000000..5369cfdad6b3bf --- /dev/null +++ b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/LiquibaseMongodbFactory.java @@ -0,0 +1,95 @@ +package io.quarkus.liquibase; + +import java.util.Map; +import java.util.regex.Pattern; + +import io.quarkus.liquibase.runtime.LiquibaseMongodbBuildTimeConfig; +import io.quarkus.liquibase.runtime.LiquibaseMongodbConfig; +import io.quarkus.mongodb.runtime.MongoClientConfig; +import liquibase.Contexts; +import liquibase.LabelExpression; +import liquibase.Liquibase; +import liquibase.database.Database; +import liquibase.database.DatabaseFactory; +import liquibase.resource.ClassLoaderResourceAccessor; +import liquibase.resource.ResourceAccessor; + +public class LiquibaseMongodbFactory { + + private final MongoClientConfig mongoClientConfig; + private final LiquibaseMongodbConfig liquibaseMongodbConfig; + private final LiquibaseMongodbBuildTimeConfig liquibaseMongodbBuildTimeConfig; + + //connection-string format, see https://docs.mongodb.com/manual/reference/connection-string/ + Pattern HAS_DB = Pattern.compile("(mongodb|mongodb\\+srv)://[^/]*/.*"); + + public LiquibaseMongodbFactory(LiquibaseMongodbConfig config, + LiquibaseMongodbBuildTimeConfig liquibaseMongodbBuildTimeConfig, MongoClientConfig mongoClientConfig) { + this.liquibaseMongodbConfig = config; + this.liquibaseMongodbBuildTimeConfig = liquibaseMongodbBuildTimeConfig; + this.mongoClientConfig = mongoClientConfig; + } + + public Liquibase createLiquibase() { + try { + ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(Thread.currentThread().getContextClassLoader()); + String connectionString = this.mongoClientConfig.connectionString.orElse("mongodb://localhost:27017"); + if (!HAS_DB.matcher(connectionString).matches()) { + connectionString += "/" + this.mongoClientConfig.database.orElseThrow( + () -> new IllegalArgumentException( + "Config property 'quarkus.mongodb.database' must be defined when no database " + + "exist in the connection string")); + } + Database database = DatabaseFactory.getInstance().openDatabase(connectionString, + this.mongoClientConfig.credentials.username.orElse(null), + this.mongoClientConfig.credentials.password.orElse(null), + null, resourceAccessor); + + ; + if (database != null) { + liquibaseMongodbConfig.liquibaseCatalogName.ifPresent(database::setLiquibaseCatalogName); + liquibaseMongodbConfig.liquibaseSchemaName.ifPresent(database::setLiquibaseSchemaName); + liquibaseMongodbConfig.liquibaseTablespaceName.ifPresent(database::setLiquibaseTablespaceName); + + if (liquibaseMongodbConfig.defaultCatalogName.isPresent()) { + database.setDefaultCatalogName(liquibaseMongodbConfig.defaultCatalogName.get()); + } + if (liquibaseMongodbConfig.defaultSchemaName.isPresent()) { + database.setDefaultSchemaName(liquibaseMongodbConfig.defaultSchemaName.get()); + } + } + Liquibase liquibase = new Liquibase(liquibaseMongodbBuildTimeConfig.changeLog, resourceAccessor, database); + + for (Map.Entry entry : liquibaseMongodbConfig.changeLogParameters.entrySet()) { + liquibase.getChangeLogParameters().set(entry.getKey(), entry.getValue()); + } + + return liquibase; + + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + public LiquibaseMongodbConfig getConfiguration() { + return liquibaseMongodbConfig; + } + + /** + * Creates the default labels base on the configuration + * + * @return the label expression + */ + public LabelExpression createLabels() { + return new LabelExpression(liquibaseMongodbConfig.labels.orElse(null)); + } + + /** + * Creates the default contexts base on the configuration + * + * @return the contexts + */ + public Contexts createContexts() { + return new Contexts(liquibaseMongodbConfig.contexts.orElse(null)); + } +} diff --git a/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbBuildTimeConfig.java b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbBuildTimeConfig.java new file mode 100644 index 00000000000000..8945d1d07b549c --- /dev/null +++ b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbBuildTimeConfig.java @@ -0,0 +1,18 @@ +package io.quarkus.liquibase.runtime; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * The liquibase configuration + */ +@ConfigRoot(name = "liquibase-mongodb", phase = ConfigPhase.BUILD_TIME) +public class LiquibaseMongodbBuildTimeConfig { + + /** + * The change log file + */ + @ConfigItem(defaultValue = "db/changeLog.xml") + public String changeLog; +} diff --git a/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbConfig.java b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbConfig.java new file mode 100644 index 00000000000000..0917e5204285e1 --- /dev/null +++ b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbConfig.java @@ -0,0 +1,84 @@ +package io.quarkus.liquibase.runtime; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * The liquibase configuration + */ +@ConfigRoot(name = "liquibase-mongodb", phase = ConfigPhase.RUN_TIME) +public class LiquibaseMongodbConfig { + + /** + * The migrate at start flag + */ + @ConfigItem + public boolean migrateAtStart; + + /** + * The validate on update flag + */ + @ConfigItem(defaultValue = "true") + public boolean validateOnMigrate; + + /** + * The clean at start flag + */ + @ConfigItem + public boolean cleanAtStart; + + /** + * The parameters to be passed to the changelog. + * Defined as key value pairs. + */ + @ConfigItem + public Map changeLogParameters = new HashMap<>();; + + /** + * The list of contexts + */ + @ConfigItem + public Optional> contexts = Optional.empty(); + + /** + * The list of labels + */ + @ConfigItem + public Optional> labels = Optional.empty(); + + /** + * The default catalog name + */ + @ConfigItem + public Optional defaultCatalogName = Optional.empty(); + + /** + * The default schema name + */ + @ConfigItem + public Optional defaultSchemaName = Optional.empty(); + + /** + * The liquibase tables catalog name + */ + @ConfigItem + public Optional liquibaseCatalogName = Optional.empty(); + + /** + * The liquibase tables schema name + */ + @ConfigItem + public Optional liquibaseSchemaName = Optional.empty(); + + /** + * The liquibase tables tablespace name + */ + @ConfigItem + public Optional liquibaseTablespaceName = Optional.empty(); +} diff --git a/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbRecorder.java b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbRecorder.java new file mode 100644 index 00000000000000..84759f8ac1e6cd --- /dev/null +++ b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/LiquibaseMongodbRecorder.java @@ -0,0 +1,63 @@ +package io.quarkus.liquibase.runtime; + +import java.util.function.Supplier; + +import javax.enterprise.inject.Any; +import javax.enterprise.inject.UnsatisfiedResolutionException; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.InjectableInstance; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.liquibase.LiquibaseMongodbFactory; +import io.quarkus.mongodb.runtime.MongodbConfig; +import io.quarkus.runtime.annotations.Recorder; +import liquibase.Liquibase; + +@Recorder +public class LiquibaseMongodbRecorder { + + public Supplier liquibaseSupplier(LiquibaseMongodbConfig config, + LiquibaseMongodbBuildTimeConfig buildTimeConfig, MongodbConfig mongodbConfig) { + return new Supplier() { + @Override + public LiquibaseMongodbFactory get() { + return new LiquibaseMongodbFactory(config, buildTimeConfig, mongodbConfig.defaultMongoClientConfig); + } + }; + } + + public void doStartActions() { + try { + InjectableInstance liquibaseFactoryInstance = Arc.container() + .select(LiquibaseMongodbFactory.class, Any.Literal.INSTANCE); + if (liquibaseFactoryInstance.isUnsatisfied()) { + return; + } + + for (InstanceHandle liquibaseFactoryHandle : liquibaseFactoryInstance.handles()) { + try { + LiquibaseMongodbFactory liquibaseFactory = liquibaseFactoryHandle.get(); + if (liquibaseFactory.getConfiguration().cleanAtStart) { + try (Liquibase liquibase = liquibaseFactory.createLiquibase()) { + liquibase.dropAll(); + } + } + if (liquibaseFactory.getConfiguration().migrateAtStart) { + if (liquibaseFactory.getConfiguration().validateOnMigrate) { + try (Liquibase liquibase = liquibaseFactory.createLiquibase()) { + liquibase.validate(); + } + } + try (Liquibase liquibase = liquibaseFactory.createLiquibase()) { + liquibase.update(liquibaseFactory.createContexts(), liquibaseFactory.createLabels()); + } + } + } catch (UnsatisfiedResolutionException e) { + //ignore, the DS is not configured + } + } + } catch (Exception e) { + throw new IllegalStateException("Error starting Liquibase", e); + } + } +} diff --git a/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/graal/SubstituteStringUtil.java b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/graal/SubstituteStringUtil.java new file mode 100644 index 00000000000000..d4f28a00e8464f --- /dev/null +++ b/extensions/liquibase-mongodb/runtime/src/main/java/io/quarkus/liquibase/runtime/graal/SubstituteStringUtil.java @@ -0,0 +1,37 @@ +package io.quarkus.liquibase.runtime.graal; + +import java.security.SecureRandom; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.InjectAccessors; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "liquibase.util.StringUtil") +final class SubstituteStringUtil { + + @Alias + @InjectAccessors(SecureRandomAccessors.class) + private static SecureRandom rnd; + + public static final class SecureRandomAccessors { + + private static volatile SecureRandom volatileRandom; + + public static SecureRandom get() { + SecureRandom localVolatileRandom = volatileRandom; + if (localVolatileRandom == null) { + synchronized (SecureRandomAccessors.class) { + localVolatileRandom = volatileRandom; + if (localVolatileRandom == null) { + volatileRandom = localVolatileRandom = new SecureRandom(); + } + } + } + return localVolatileRandom; + } + + public static void set(SecureRandom rnd) { + throw new IllegalStateException("The setter for liquibase.util.StringUtil#rnd shouldn't be called."); + } + } +} diff --git a/extensions/liquibase-mongodb/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/liquibase-mongodb/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 00000000000000..8cacc679b79752 --- /dev/null +++ b/extensions/liquibase-mongodb/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,11 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Liquibase MongoDB" +metadata: + keywords: + - "liquibase" + - "mongodb" + - "data" + categories: + - "data" + status: "experimental" \ No newline at end of file diff --git a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java index 1564f2fbbf38f6..1f49d9c9bca880 100644 --- a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java +++ b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/DevServicesMongoProcessor.java @@ -27,7 +27,11 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesConfigResultBuildItem; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.console.ConsoleInstalledBuildItem; +import io.quarkus.deployment.console.StartupLogCompressor; import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.deployment.logging.LoggingSetupBuildItem; import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.mongodb.runtime.MongodbConfig; import io.quarkus.runtime.configuration.ConfigUtils; @@ -46,7 +50,10 @@ public class DevServicesMongoProcessor { public void startMongo(List mongoConnections, MongoClientBuildTimeConfig mongoClientBuildTimeConfig, Optional devServicesSharedNetworkBuildItem, - BuildProducer devServices) { + BuildProducer devServices, + Optional consoleInstalledBuildItem, + LaunchModeBuildItem launchMode, + LoggingSetupBuildItem loggingSetupBuildItem) { List connectionNames = new ArrayList<>(mongoConnections.size()); for (MongoConnectionNameBuildItem mongoConnection : mongoConnections) { @@ -86,8 +93,18 @@ public void startMongo(List mongoConnections, // TODO: we need to go through each connection String connectionName = connectionNames.get(0); - StartResult startResult = startMongo(connectionName, currentCapturedProperties.get(connectionName), - devServicesSharedNetworkBuildItem.isPresent()); + StartResult startResult; + StartupLogCompressor compressor = new StartupLogCompressor( + (launchMode.isTest() ? "(test) " : "") + "Mongo Dev Services Starting:", consoleInstalledBuildItem, + loggingSetupBuildItem); + try { + startResult = startMongo(connectionName, currentCapturedProperties.get(connectionName), + devServicesSharedNetworkBuildItem.isPresent()); + compressor.close(); + } catch (Throwable t) { + compressor.closeAndDumpCaptured(); + throw new RuntimeException(t); + } if (startResult != null) { currentCloseables.add(startResult.getCloseable()); String connectionStringPropertyName = getConfigPrefix(connectionName) + "connection-string"; diff --git a/extensions/mutiny/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/mutiny/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 658ebabc08beaf..001d82655819ad 100644 --- a/extensions/mutiny/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/mutiny/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -10,4 +10,4 @@ metadata: - "Reactor" categories: - "reactive" - status: "preview" \ No newline at end of file + status: "stable" \ No newline at end of file diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/CDIDelegatingTransactionManager.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/CDIDelegatingTransactionManager.java index db0f965516b85e..d8caf9dc874873 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/CDIDelegatingTransactionManager.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/CDIDelegatingTransactionManager.java @@ -20,12 +20,15 @@ import org.jboss.logging.Logger; +import io.quarkus.arc.Unremovable; + /** * A delegating transaction manager which receives an instance of Narayana transaction manager * and delegates all calls to it. * On top of it the implementation adds the CDI events processing for {@link TransactionScoped}. */ @Singleton +@Unremovable // used by Arc for transactional observers public class CDIDelegatingTransactionManager implements TransactionManager, Serializable { private static final Logger log = Logger.getLogger(CDIDelegatingTransactionManager.class); diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaProducers.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaProducers.java index a6c13e63d8df23..85e4b99d2e17e2 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaProducers.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaProducers.java @@ -39,7 +39,7 @@ public XAResourceRecoveryRegistry xaResourceRecoveryRegistry() { @Produces @ApplicationScoped - @Unremovable // needed by Arc for transactional observers + @Unremovable public TransactionSynchronizationRegistry transactionSynchronizationRegistry() { return new TransactionSynchronizationRegistryImple(); } diff --git a/extensions/oidc-client-filter/runtime/pom.xml b/extensions/oidc-client-filter/runtime/pom.xml index c590a6b40ca155..a942bd43fc823a 100644 --- a/extensions/oidc-client-filter/runtime/pom.xml +++ b/extensions/oidc-client-filter/runtime/pom.xml @@ -12,7 +12,7 @@ quarkus-oidc-client-filter Quarkus - OpenID Connect Client Filter - Runtime - Use JAX-RS client filter to get and refresh the access tokens and set them as HTTP Authorization Bearer values + Use JAX-RS Client filter to get and refresh access tokens with OpenId Connect Client and send them as HTTP Authorization Bearer tokens io.quarkus diff --git a/extensions/oidc-client-filter/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/oidc-client-filter/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 6b87f37707e2cd..a88e19aebb088d 100644 --- a/extensions/oidc-client-filter/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/oidc-client-filter/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -11,6 +11,6 @@ metadata: guide: "https://quarkus.io/guides/security-openid-connect-client" categories: - "security" - status: "preview" + status: "stable" config: - "quarkus.oidc-client." diff --git a/extensions/oidc-client-reactive-filter/runtime/pom.xml b/extensions/oidc-client-reactive-filter/runtime/pom.xml index 31034ca04b9a7e..9fbb3bd9b2593c 100644 --- a/extensions/oidc-client-reactive-filter/runtime/pom.xml +++ b/extensions/oidc-client-reactive-filter/runtime/pom.xml @@ -12,7 +12,7 @@ quarkus-oidc-client-reactive-filter Quarkus - OpenID Connect Client Reactive Filter - Runtime - Use Reactive RestClient filter to get and refresh the access tokens and set them as HTTP Authorization Bearer values + Use Reactive RestClient filter to get and refresh access tokens with OpenId Connect Client and send them as HTTP Authorization Bearer tokens io.quarkus diff --git a/extensions/oidc-client/runtime/pom.xml b/extensions/oidc-client/runtime/pom.xml index e3825023299458..7becfd54a8a8d4 100644 --- a/extensions/oidc-client/runtime/pom.xml +++ b/extensions/oidc-client/runtime/pom.xml @@ -12,7 +12,7 @@ quarkus-oidc-client Quarkus - OpenID Connect Client - Runtime - Use OpenID Connect Client to get and refresh access tokens + Get and refresh access tokens from OpenID Connect providers io.quarkus diff --git a/extensions/oidc-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/oidc-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml index b3b4af7ed627e4..434ccf025932b2 100644 --- a/extensions/oidc-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/oidc-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -10,7 +10,7 @@ metadata: guide: "https://quarkus.io/guides/security-openid-connect-client" categories: - "security" - status: "preview" + status: "stable" config: - "quarkus.oidc-client." - "quarkus.oidc." diff --git a/extensions/oidc-token-propagation/runtime/pom.xml b/extensions/oidc-token-propagation/runtime/pom.xml index 1b9d5e48774f96..3f47c7e1982eac 100644 --- a/extensions/oidc-token-propagation/runtime/pom.xml +++ b/extensions/oidc-token-propagation/runtime/pom.xml @@ -12,7 +12,7 @@ quarkus-oidc-token-propagation Quarkus - OpenID Connect Token Propagation - Runtime - Use JAX-RS client filter to propagate the current access token as HTTP Authorization Bearer value + Use JAX-RS Client filter to propagate the incoming Bearer access token or token acquired from Authorization Code Flow as HTTP Authorization Bearer token io.quarkus diff --git a/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/AccessToken.java b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/AccessToken.java index a3075712a3bfe5..2debaf34ecd775 100644 --- a/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/AccessToken.java +++ b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/AccessToken.java @@ -6,6 +6,13 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * When this annotation is added to a MicroProfile REST Client interface, the {@link AccessTokenRequestFilter} will be added to + * the request pipeline. + * The end result is that the request propagates the Bearer token present in the current active request or the token acquired + * from the Authorization Code Flow, + * as the HTTP {@code Authorization} header's {@code Bearer} scheme value. + */ @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented diff --git a/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/AccessTokenRequestFilter.java b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/AccessTokenRequestFilter.java index 0a5df9d7a98e10..0ec0b0877ae9b0 100644 --- a/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/AccessTokenRequestFilter.java +++ b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/AccessTokenRequestFilter.java @@ -20,6 +20,9 @@ public class AccessTokenRequestFilter extends AbstractTokenRequestFilter { private static final String EXCHANGE_SUBJECT_TOKEN = "subject_token"; + // note: We can't use constructor injection for these fields because they are registered by RESTEasy + // which doesn't know about CDI at the point of registration + @Inject Instance accessToken; diff --git a/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/JsonWebToken.java b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/JsonWebToken.java index e7b8601602bd62..81649761f76cf6 100644 --- a/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/JsonWebToken.java +++ b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/JsonWebToken.java @@ -6,6 +6,13 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * When this annotation is added to a MicroProfile REST Client interface, the {@link JsonWebTokenRequestFilter} will be added to + * the request pipeline. + * The end result is that the request propagates the JWT token present in the current active request or the token acquired from + * the Authorization Code Flow, + * as the HTTP {@code Authorization} header's {@code Bearer} scheme value. + */ @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented diff --git a/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/JsonWebTokenRequestFilter.java b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/JsonWebTokenRequestFilter.java index 4151e696ac946e..1be3a3795359c3 100644 --- a/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/JsonWebTokenRequestFilter.java +++ b/extensions/oidc-token-propagation/runtime/src/main/java/io/quarkus/oidc/token/propagation/JsonWebTokenRequestFilter.java @@ -12,6 +12,10 @@ import io.smallrye.jwt.build.Jwt; public class JsonWebTokenRequestFilter extends AbstractTokenRequestFilter { + + // note: We can't use constructor injection for these fields because they are registered by RESTEasy + // which doesn't know about CDI at the point of registration + @Inject Instance jwtAccessToken; diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java index b0b958807c0da4..fad473c1b05b78 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java @@ -38,6 +38,7 @@ import io.quarkus.oidc.runtime.TenantConfigBean; import io.quarkus.runtime.TlsConfig; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; +import io.quarkus.vertx.http.deployment.SecurityInformationBuildItem; import io.smallrye.jwt.auth.cdi.ClaimValueProducer; import io.smallrye.jwt.auth.cdi.CommonJwtProducer; import io.smallrye.jwt.auth.cdi.JsonValueProducer; @@ -51,6 +52,14 @@ FeatureBuildItem featureBuildItem() { return new FeatureBuildItem(Feature.OIDC); } + @BuildStep(onlyIf = IsEnabled.class) + public void provideSecurityInformation(BuildProducer securityInformationProducer) { + // TODO: By default quarkus.oidc.application-type = service + // Also look at other options (web-app, hybrid) + securityInformationProducer + .produce(SecurityInformationBuildItem.OPENIDCONNECT("quarkus.oidc.auth-server-url")); + } + @BuildStep(onlyIf = IsEnabled.class) AdditionalBeanBuildItem jwtClaimIntegration(Capabilities capabilities) { if (!capabilities.isPresent(Capability.JWT)) { diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakAuthorizationCodePostHandler.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcAuthorizationCodePostHandler.java similarity index 67% rename from extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakAuthorizationCodePostHandler.java rename to extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcAuthorizationCodePostHandler.java index df550d73225cd2..2b9d9b7becf125 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakAuthorizationCodePostHandler.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcAuthorizationCodePostHandler.java @@ -1,10 +1,13 @@ -package io.quarkus.oidc.deployment.devservices.keycloak; +package io.quarkus.oidc.deployment.devservices; + +import java.time.Duration; import org.jboss.logging.Logger; import io.quarkus.devconsole.runtime.spi.DevConsolePostHandler; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; import io.vertx.core.http.HttpHeaders; import io.vertx.ext.web.RoutingContext; import io.vertx.mutiny.core.buffer.Buffer; @@ -12,24 +15,32 @@ import io.vertx.mutiny.ext.web.client.HttpResponse; import io.vertx.mutiny.ext.web.client.WebClient; -public class KeycloakAuthorizationCodePostHandler extends DevConsolePostHandler { - private static final Logger LOG = Logger.getLogger(KeycloakAuthorizationCodePostHandler.class); +public class OidcAuthorizationCodePostHandler extends DevConsolePostHandler { + private static final Logger LOG = Logger.getLogger(OidcAuthorizationCodePostHandler.class); + + Vertx vertxInstance; + Duration timeout; + + public OidcAuthorizationCodePostHandler(Vertx vertxInstance, Duration timeout) { + this.vertxInstance = vertxInstance; + this.timeout = timeout; + } @Override protected void handlePostAsync(RoutingContext event, MultiMap form) throws Exception { - WebClient client = KeycloakDevServicesUtils.createWebClient(); - String keycloakUrl = form.get("keycloakUrl") + "/realms/" + form.get("realm") + "/protocol/openid-connect/token"; + WebClient client = OidcDevServicesUtils.createWebClient(vertxInstance); + String tokenUrl = form.get("tokenUrl"); try { - LOG.infof("Using authorization_code grant to get a token from '%s' in realm '%s' with client id '%s'", - keycloakUrl, form.get("realm"), form.get("client")); + LOG.infof("Using authorization_code grant to get a token from '%s' with client id '%s'", + tokenUrl, form.get("client")); - HttpRequest request = client.postAbs(keycloakUrl); + HttpRequest request = client.postAbs(tokenUrl); request.putHeader(HttpHeaders.CONTENT_TYPE.toString(), HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString()); io.vertx.mutiny.core.MultiMap props = new io.vertx.mutiny.core.MultiMap(MultiMap.caseInsensitiveMultiMap()); props.add("client_id", form.get("client")); - if (form.get("clientSecret") != null) { + if (form.get("clientSecret") != null && !form.get("clientSecret").isBlank()) { props.add("client_secret", form.get("clientSecret")); } props.add("grant_type", "authorization_code"); @@ -38,12 +49,12 @@ protected void handlePostAsync(RoutingContext event, MultiMap form) throws Excep String tokens = request.sendBuffer(OidcCommonUtils.encodeForm(props)).onItem() .transform(resp -> getBodyAsString(resp)) - .await().atMost(KeycloakDevServicesProcessor.capturedDevServicesConfiguration.webClienTimeout); + .await().atMost(timeout); event.put("tokens", tokens); } catch (Throwable t) { - LOG.errorf("Token can not be acquired from Keycloak: %s", t.toString()); + LOG.errorf("Token can not be acquired from OpenId Connect provider: %s", t.toString()); } finally { client.close(); } diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevConsoleProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevConsoleProcessor.java new file mode 100644 index 00000000000000..c1975727214feb --- /dev/null +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevConsoleProcessor.java @@ -0,0 +1,146 @@ +package io.quarkus.oidc.deployment.devservices; + +import java.time.Duration; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logging.Logger; + +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Consume; +import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; +import io.quarkus.devconsole.spi.DevConsoleRouteBuildItem; +import io.quarkus.devconsole.spi.DevConsoleTemplateInfoBuildItem; +import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.runtime.configuration.ConfigUtils; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.json.JsonObject; +import io.vertx.mutiny.core.buffer.Buffer; +import io.vertx.mutiny.ext.web.client.HttpResponse; +import io.vertx.mutiny.ext.web.client.WebClient; + +public class OidcDevConsoleProcessor { + static volatile Vertx vertxInstance; + private static final Logger LOG = Logger.getLogger(OidcDevConsoleProcessor.class); + + private static final String CONFIG_PREFIX = "quarkus.oidc."; + private static final String TENANT_ENABLED_CONFIG_KEY = CONFIG_PREFIX + "tenant-enabled"; + private static final String AUTH_SERVER_URL_CONFIG_KEY = CONFIG_PREFIX + "auth-server-url"; + private static final String APP_TYPE_CONFIG_KEY = CONFIG_PREFIX + "application-type"; + private static final String SERVICE_APP_TYPE = "service"; + private static final String CLIENT_ID_CONFIG_KEY = CONFIG_PREFIX + "client-id"; + private static final String CLIENT_SECRET_CONFIG_KEY = CONFIG_PREFIX + "credentials.secret"; + + @BuildStep(onlyIf = IsDevelopment.class) + @Consume(RuntimeConfigSetupCompleteBuildItem.class) + void prepareOidcDevConsole(BuildProducer console, + BuildProducer devConsoleRoute) { + if (isOidcTenantEnabled() && isAuthServerUrlSet() && isClientIdSet() && isServiceAuthType()) { + + if (vertxInstance == null) { + vertxInstance = Vertx.vertx(); + + Runnable closeTask = new Runnable() { + @Override + public void run() { + if (vertxInstance != null) { + try { + vertxInstance.close(); + } catch (Throwable t) { + LOG.error("Failed to close Vertx instance", t); + } + } + vertxInstance = null; + } + }; + QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread().getContextClassLoader(); + ((QuarkusClassLoader) cl.parent()).addCloseTask(closeTask); + Thread closeHookThread = new Thread(closeTask, "OIDC DevConsole Vertx close thread"); + Runtime.getRuntime().addShutdownHook(closeHookThread); + ((QuarkusClassLoader) cl.parent()).addCloseTask(new Runnable() { + @Override + public void run() { + Runtime.getRuntime().removeShutdownHook(closeHookThread); + } + }); + } + + String authServerUrl = getConfigProperty(AUTH_SERVER_URL_CONFIG_KEY); + JsonObject metadata = discoverMetadata(authServerUrl); + if (metadata == null) { + return; + } + if (authServerUrl.contains("/realms/")) { + console.produce(new DevConsoleTemplateInfoBuildItem("keycloakAdminUrl", + authServerUrl.substring(0, authServerUrl.indexOf("/realms/")))); + } + console.produce(new DevConsoleTemplateInfoBuildItem("oidcApplicationType", SERVICE_APP_TYPE)); + console.produce(new DevConsoleTemplateInfoBuildItem("clientId", getConfigProperty(CLIENT_ID_CONFIG_KEY))); + console.produce(new DevConsoleTemplateInfoBuildItem("clientSecret", getClientSecret())); + + console.produce(new DevConsoleTemplateInfoBuildItem("tokenUrl", metadata.getString("token_endpoint"))); + console.produce( + new DevConsoleTemplateInfoBuildItem("authorizationUrl", metadata.getString("authorization_endpoint"))); + if (metadata.containsKey("end_session_endpoint")) { + console.produce(new DevConsoleTemplateInfoBuildItem("logoutUrl", metadata.getString("end_session_endpoint"))); + } + console.produce(new DevConsoleTemplateInfoBuildItem("oidcGrantType", "code")); + + devConsoleRoute.produce(new DevConsoleRouteBuildItem("testServiceWithToken", "POST", + new OidcTestServiceHandler(vertxInstance, Duration.ofSeconds(3)))); + devConsoleRoute.produce(new DevConsoleRouteBuildItem("exchangeCodeForTokens", "POST", + new OidcAuthorizationCodePostHandler(vertxInstance, Duration.ofSeconds(3)))); + } + } + + private JsonObject discoverMetadata(String authServerUrl) { + WebClient client = OidcDevServicesUtils.createWebClient(vertxInstance); + try { + String metadataUrl = authServerUrl + OidcConstants.WELL_KNOWN_CONFIGURATION; + LOG.infof("OIDC Dev Console: discovering the provider metadata at %s", metadataUrl); + + HttpResponse resp = client.getAbs(metadataUrl) + .putHeader(HttpHeaders.ACCEPT.toString(), "application/json").send().await().indefinitely(); + if (resp.statusCode() == 200) { + return resp.bodyAsJsonObject(); + } else { + LOG.errorf("OIDC metadata discovery failed: %s", resp.bodyAsString()); + return null; + } + } catch (Throwable t) { + LOG.errorf("OIDC metadata discovery failed: %s", t.toString()); + return null; + } finally { + client.close(); + } + } + + private String getConfigProperty(String name) { + return ConfigProvider.getConfig().getValue(name, String.class); + } + + private static boolean isOidcTenantEnabled() { + return ConfigProvider.getConfig().getOptionalValue(TENANT_ENABLED_CONFIG_KEY, Boolean.class).orElse(true); + } + + private static boolean isClientIdSet() { + return ConfigUtils.isPropertyPresent(CLIENT_ID_CONFIG_KEY); + } + + private static String getClientSecret() { + return ConfigProvider.getConfig().getOptionalValue(CLIENT_SECRET_CONFIG_KEY, String.class).orElse(""); + } + + private static boolean isAuthServerUrlSet() { + return ConfigUtils.isPropertyPresent(AUTH_SERVER_URL_CONFIG_KEY); + } + + private boolean isServiceAuthType() { + return SERVICE_APP_TYPE.equals( + ConfigProvider.getConfig().getOptionalValue(APP_TYPE_CONFIG_KEY, String.class).orElse(SERVICE_APP_TYPE)); + } + +} diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesBuildItem.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesBuildItem.java index 78a7c59e677f3b..6fafbb817a38cb 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesBuildItem.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesBuildItem.java @@ -2,6 +2,10 @@ import io.quarkus.builder.item.SimpleBuildItem; +/** + * Marker build item which indicates that Dev Services for OIDC are provided by another extension. + * Dev Services for Keycloak will be disabled if this item is detected. + */ public class OidcDevServicesBuildItem extends SimpleBuildItem { } diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesUtils.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesUtils.java similarity index 83% rename from extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesUtils.java rename to extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesUtils.java index 5fb008965ea2b1..5666973d58bf3f 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesUtils.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesUtils.java @@ -1,9 +1,10 @@ -package io.quarkus.oidc.deployment.devservices.keycloak; +package io.quarkus.oidc.deployment.devservices; import java.time.Duration; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; import io.vertx.core.http.HttpHeaders; import io.vertx.core.json.JsonObject; import io.vertx.mutiny.core.buffer.Buffer; @@ -11,23 +12,23 @@ import io.vertx.mutiny.ext.web.client.HttpResponse; import io.vertx.mutiny.ext.web.client.WebClient; -public final class KeycloakDevServicesUtils { - private KeycloakDevServicesUtils() { +public final class OidcDevServicesUtils { + private OidcDevServicesUtils() { } - public static WebClient createWebClient() { - return WebClient.create(new io.vertx.mutiny.core.Vertx(KeycloakDevServicesProcessor.vertxInstance)); + public static WebClient createWebClient(Vertx vertx) { + return WebClient.create(new io.vertx.mutiny.core.Vertx(vertx)); } public static String getPasswordAccessToken(WebClient client, - String keycloakUrl, + String tokenUrl, String clientId, String clientSecret, String userName, String userPassword, Duration timeout) throws Exception { - HttpRequest request = client.postAbs(keycloakUrl); + HttpRequest request = client.postAbs(tokenUrl); request.putHeader(HttpHeaders.CONTENT_TYPE.toString(), HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString()); io.vertx.mutiny.core.MultiMap props = new io.vertx.mutiny.core.MultiMap(MultiMap.caseInsensitiveMultiMap()); @@ -45,11 +46,11 @@ public static String getPasswordAccessToken(WebClient client, } public static String getClientCredAccessToken(WebClient client, - String keycloakUrl, + String tokenUrl, String clientId, String clientSecret, Duration timeout) throws Exception { - HttpRequest request = client.postAbs(keycloakUrl); + HttpRequest request = client.postAbs(tokenUrl); request.putHeader(HttpHeaders.CONTENT_TYPE.toString(), HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString()); io.vertx.mutiny.core.MultiMap props = new io.vertx.mutiny.core.MultiMap(MultiMap.caseInsensitiveMultiMap()); diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakImplicitGrantPostHandler.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcTestServiceHandler.java similarity index 67% rename from extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakImplicitGrantPostHandler.java rename to extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcTestServiceHandler.java index 6e57f35b85b4dd..6f578d6b9c90ef 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakImplicitGrantPostHandler.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcTestServiceHandler.java @@ -1,28 +1,33 @@ -package io.quarkus.oidc.deployment.devservices.keycloak; +package io.quarkus.oidc.deployment.devservices; + +import java.time.Duration; import org.jboss.logging.Logger; import io.quarkus.devconsole.runtime.spi.DevConsolePostHandler; import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; import io.vertx.core.http.HttpHeaders; import io.vertx.ext.web.RoutingContext; import io.vertx.mutiny.ext.web.client.WebClient; -public class KeycloakImplicitGrantPostHandler extends DevConsolePostHandler { - private static final Logger LOG = Logger.getLogger(KeycloakImplicitGrantPostHandler.class); +public class OidcTestServiceHandler extends DevConsolePostHandler { + private static final Logger LOG = Logger.getLogger(OidcTestServiceHandler.class); + + Vertx vertxInstance; + Duration timeout; + + public OidcTestServiceHandler(Vertx vertxInstance, Duration timeout) { + this.vertxInstance = vertxInstance; + this.timeout = timeout; + } @Override protected void handlePostAsync(RoutingContext event, MultiMap form) throws Exception { - WebClient client = KeycloakDevServicesUtils.createWebClient(); + WebClient client = OidcDevServicesUtils.createWebClient(vertxInstance); try { - String token = form.get("token"); - - LOG.infof("Test token: %s", token); - LOG.infof("Sending token to '%s'", form.get("serviceUrl")); - testServiceInternal(event, client, form.get("serviceUrl"), token); - } catch (Throwable t) { - LOG.errorf("Token can not be acquired from Keycloak: %s", t.toString()); + testServiceInternal(event, client, form.get("serviceUrl"), form.get("token")); } finally { client.close(); } @@ -30,9 +35,11 @@ protected void handlePostAsync(RoutingContext event, MultiMap form) throws Excep private void testServiceInternal(RoutingContext event, WebClient client, String serviceUrl, String token) { try { + LOG.infof("Test token: %s", token); + LOG.infof("Sending token to '%s'", serviceUrl); int statusCode = client.getAbs(serviceUrl) .putHeader(HttpHeaders.AUTHORIZATION.toString(), "Bearer " + token).send().await() - .atMost(KeycloakDevServicesProcessor.capturedDevServicesConfiguration.webClienTimeout) + .atMost(timeout) .statusCode(); LOG.infof("Result: %d", statusCode); event.put("result", String.valueOf(statusCode)); diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java index 754bc3e7db2d2b..b3a31eef22a25e 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java @@ -65,12 +65,27 @@ public class DevServicesConfig { */ @ConfigItem public Optional javaOpts; + /** - * The Keycloak realm. - * This property will be used to create the realm if the realm file pointed to by the 'realm-path' property does not exist. + * The Keycloak realm name. + * This property will be used to create the realm if the realm file pointed to by the 'realm-path' property does not exist, + * default value is 'quarkus' in this case. + * If the realm file pointed to by the 'realm-path' property exists then it is still recommended to set this property + * for Dev Services for Keycloak to avoid parsing the realm file in order to determine the realm name. + * */ - @ConfigItem(defaultValue = "quarkus") - public String realmName; + @ConfigItem + public Optional realmName; + + /** + * Indicates if the Keycloak realm has to be created when the realm file pointed to by the 'realm-path' property does not + * exist. + * + * Disable it if you'd like to create a realm using Keycloak Administration Console + * or Keycloak Admin API from {@linkplain io.quarkus.test.common.QuarkusTestResourceLifecycleManager}. + */ + @ConfigItem(defaultValue = "true") + public boolean createRealm; /** * The Keycloak users map containing the user name and password pairs. diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsolePostHandler.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsolePostHandler.java index 8faa62619bf32b..3ec790d7fecb95 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsolePostHandler.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsolePostHandler.java @@ -5,6 +5,7 @@ import org.jboss.logging.Logger; import io.quarkus.devconsole.runtime.spi.DevConsolePostHandler; +import io.quarkus.oidc.deployment.devservices.OidcDevServicesUtils; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpHeaders; import io.vertx.ext.web.RoutingContext; @@ -20,33 +21,31 @@ public KeycloakDevConsolePostHandler(Map users) { @Override protected void handlePostAsync(RoutingContext event, MultiMap form) throws Exception { - WebClient client = KeycloakDevServicesUtils.createWebClient(); - String keycloakUrl = form.get("keycloakUrl") + "/realms/" + form.get("realm") + "/protocol/openid-connect/token"; + WebClient client = OidcDevServicesUtils.createWebClient(KeycloakDevServicesProcessor.vertxInstance); + String tokenUrl = form.get("tokenUrl"); try { String token = null; if ("password".equals(form.get("grant"))) { - LOG.infof("Using a password grant to get a token from '%s' for user '%s' in realm '%s' with client id '%s'", - keycloakUrl, form.get("user"), form.get("realm"), form.get("client")); + LOG.infof("Using a password grant to get a token from '%s' for user '%s' with client id '%s'", + tokenUrl, form.get("user"), form.get("client")); String userName = form.get("user"); - token = KeycloakDevServicesUtils.getPasswordAccessToken(client, keycloakUrl, + token = OidcDevServicesUtils.getPasswordAccessToken(client, tokenUrl, form.get("client"), form.get("clientSecret"), userName, users.get(userName), KeycloakDevServicesProcessor.capturedDevServicesConfiguration.webClienTimeout); } else { - LOG.infof("Using a client_credentials grant to get a token token from '%s' in realm '%s' with client id '%s'", - keycloakUrl, form.get("realm"), form.get("client")); + LOG.infof("Using a client_credentials grant to get a token token from '%s' with client id '%s'", + tokenUrl, form.get("client")); - token = KeycloakDevServicesUtils.getClientCredAccessToken(client, keycloakUrl, + token = OidcDevServicesUtils.getClientCredAccessToken(client, tokenUrl, form.get("client"), form.get("clientSecret"), KeycloakDevServicesProcessor.capturedDevServicesConfiguration.webClienTimeout); } - LOG.infof("Test token: %s", token); - LOG.infof("Sending token to '%s'", form.get("serviceUrl")); testServiceInternal(event, client, form.get("serviceUrl"), token); } catch (Throwable t) { LOG.errorf("Token can not be acquired from Keycloak: %s", t.toString()); @@ -57,6 +56,8 @@ protected void handlePostAsync(RoutingContext event, MultiMap form) throws Excep private void testServiceInternal(RoutingContext event, WebClient client, String serviceUrl, String token) { try { + LOG.infof("Test token: %s", token); + LOG.infof("Sending token to '%s'", serviceUrl); int statusCode = client.getAbs(serviceUrl) .putHeader(HttpHeaders.AUTHORIZATION.toString(), "Bearer " + token).send().await().indefinitely() .statusCode(); diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsoleProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsoleProcessor.java index 67e911d91a10f8..6404c3ba67be71 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsoleProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsoleProcessor.java @@ -10,6 +10,8 @@ import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; import io.quarkus.devconsole.spi.DevConsoleRouteBuildItem; import io.quarkus.devconsole.spi.DevConsoleTemplateInfoBuildItem; +import io.quarkus.oidc.deployment.devservices.OidcAuthorizationCodePostHandler; +import io.quarkus.oidc.deployment.devservices.OidcTestServiceHandler; public class KeycloakDevConsoleProcessor { @@ -19,21 +21,25 @@ public class KeycloakDevConsoleProcessor { @Consume(RuntimeConfigSetupCompleteBuildItem.class) public void setConfigProperties(BuildProducer console, Optional configProps) { - if (configProps.isPresent()) { - console.produce( - new DevConsoleTemplateInfoBuildItem("devServicesEnabled", config.devservices.enabled)); - console.produce( - new DevConsoleTemplateInfoBuildItem("keycloakUrl", configProps.get().getProperties().get("keycloak.url"))); + if (configProps.isPresent() && configProps.get().getProperties().containsKey("keycloak.url")) { + String keycloakUrl = (String) configProps.get().getProperties().get("keycloak.url"); + String realmUrl = keycloakUrl + "/realms/" + configProps.get().getProperties().get("keycloak.realm"); + + console.produce(new DevConsoleTemplateInfoBuildItem("keycloakUrl", keycloakUrl)); + console.produce(new DevConsoleTemplateInfoBuildItem("keycloakAdminUrl", keycloakUrl)); console.produce(new DevConsoleTemplateInfoBuildItem("oidcApplicationType", configProps.get().getProperties().get("quarkus.oidc.application-type"))); - console.produce(new DevConsoleTemplateInfoBuildItem("keycloakClient", + console.produce(new DevConsoleTemplateInfoBuildItem("clientId", configProps.get().getProperties().get("quarkus.oidc.client-id"))); - console.produce(new DevConsoleTemplateInfoBuildItem("keycloakClientSecret", + console.produce(new DevConsoleTemplateInfoBuildItem("clientSecret", configProps.get().getProperties().get("quarkus.oidc.credentials.secret"))); console.produce( new DevConsoleTemplateInfoBuildItem("keycloakUsers", configProps.get().getProperties().get("oidc.users"))); - console.produce(new DevConsoleTemplateInfoBuildItem("keycloakRealm", config.devservices.realmName)); + console.produce(new DevConsoleTemplateInfoBuildItem("tokenUrl", realmUrl + "/protocol/openid-connect/token")); + console.produce( + new DevConsoleTemplateInfoBuildItem("authorizationUrl", realmUrl + "/protocol/openid-connect/auth")); + console.produce(new DevConsoleTemplateInfoBuildItem("logoutUrl", realmUrl + "/protocol/openid-connect/logout")); console.produce(new DevConsoleTemplateInfoBuildItem("oidcGrantType", config.devservices.grant.type.getGrantType())); } } @@ -41,15 +47,19 @@ public void setConfigProperties(BuildProducer c @BuildStep(onlyIf = IsDevelopment.class) void invokeEndpoint(BuildProducer devConsoleRoute, Optional configProps) { - if (configProps.isPresent()) { + if (configProps.isPresent() && configProps.get().getProperties().containsKey("keycloak.url")) { @SuppressWarnings("unchecked") Map users = (Map) configProps.get().getProperties().get("oidc.users"); devConsoleRoute.produce( new DevConsoleRouteBuildItem("testService", "POST", new KeycloakDevConsolePostHandler(users))); devConsoleRoute.produce( - new DevConsoleRouteBuildItem("testServiceWithToken", "POST", new KeycloakImplicitGrantPostHandler())); + new DevConsoleRouteBuildItem("testServiceWithToken", "POST", + new OidcTestServiceHandler(KeycloakDevServicesProcessor.vertxInstance, + KeycloakDevServicesProcessor.capturedDevServicesConfiguration.webClienTimeout))); devConsoleRoute.produce( - new DevConsoleRouteBuildItem("exchangeCodeForTokens", "POST", new KeycloakAuthorizationCodePostHandler())); + new DevConsoleRouteBuildItem("exchangeCodeForTokens", "POST", + new OidcAuthorizationCodePostHandler(KeycloakDevServicesProcessor.vertxInstance, + KeycloakDevServicesProcessor.capturedDevServicesConfiguration.webClienTimeout))); } } } diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java index 1a0658f65fe06e..111a2857704e0a 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java @@ -2,6 +2,10 @@ import java.io.Closeable; import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -41,15 +45,21 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesConfigResultBuildItem; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.console.ConsoleInstalledBuildItem; +import io.quarkus.deployment.console.StartupLogCompressor; import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.deployment.logging.LoggingSetupBuildItem; import io.quarkus.devservices.common.ContainerAddress; import io.quarkus.devservices.common.ContainerLocator; import io.quarkus.oidc.deployment.OidcBuildStep.IsEnabled; import io.quarkus.oidc.deployment.devservices.OidcDevServicesBuildItem; +import io.quarkus.oidc.deployment.devservices.OidcDevServicesUtils; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.configuration.ConfigUtils; import io.vertx.core.Vertx; import io.vertx.core.http.HttpHeaders; +import io.vertx.core.json.JsonObject; import io.vertx.mutiny.core.buffer.Buffer; import io.vertx.mutiny.ext.web.client.HttpResponse; import io.vertx.mutiny.ext.web.client.WebClient; @@ -67,6 +77,7 @@ public class KeycloakDevServicesProcessor { private static final String CLIENT_ID_CONFIG_KEY = CONFIG_PREFIX + "client-id"; private static final String CLIENT_SECRET_CONFIG_KEY = CONFIG_PREFIX + "credentials.secret"; private static final String KEYCLOAK_URL_KEY = "keycloak.url"; + private static final String KEYCLOAK_REALM_KEY = "keycloak.realm"; private static final int KEYCLOAK_PORT = 8080; private static final String JAVA_OPTS = "JAVA_OPTS"; @@ -93,13 +104,17 @@ public class KeycloakDevServicesProcessor { private static volatile String capturedKeycloakUrl; private static volatile FileTime capturedRealmFileLastModifiedDate; private final IsDockerWorking isDockerWorking = new IsDockerWorking(true); + private static volatile KeycloakDevServicesConfigBuildItem existingDevServiceConfig; @BuildStep(onlyIfNot = IsNormal.class, onlyIf = { IsEnabled.class, GlobalDevServicesConfig.Enabled.class }) public KeycloakDevServicesConfigBuildItem startKeycloakContainer( Optional devServicesSharedNetworkBuildItem, BuildProducer devServices, Optional oidcProviderBuildItem, - KeycloakBuildTimeConfig config) { + KeycloakBuildTimeConfig config, + LaunchModeBuildItem launchMode, + Optional consoleInstalledBuildItem, + LoggingSetupBuildItem loggingSetupBuildItem) { if (oidcProviderBuildItem.isPresent()) { // Dev Services for the alternative OIDC provider are enabled @@ -121,7 +136,7 @@ public KeycloakDevServicesConfigBuildItem startKeycloakContainer( } } if (!restartRequired) { - return null; + return existingDevServiceConfig; } for (Closeable closeable : closeables) { try { @@ -133,62 +148,73 @@ public KeycloakDevServicesConfigBuildItem startKeycloakContainer( closeables = null; capturedDevServicesConfiguration = null; capturedKeycloakUrl = null; + existingDevServiceConfig = null; } capturedDevServicesConfiguration = currentDevServicesConfiguration; + StartResult startResult; + StartupLogCompressor compressor = new StartupLogCompressor( + (launchMode.isTest() ? "(test) " : "") + "KeyCloak Dev Services Starting:", + consoleInstalledBuildItem, loggingSetupBuildItem); + try { + startResult = startContainer(devServicesSharedNetworkBuildItem.isPresent()); + if (startResult == null) { + return null; + } - StartResult startResult = startContainer(devServicesSharedNetworkBuildItem.isPresent()); - if (startResult == null) { - return null; - } - - closeables = startResult.closeable != null ? Collections.singletonList(startResult.closeable) : null; - - if (first) { - first = false; - Runnable closeTask = new Runnable() { - @Override - public void run() { - if (closeables != null) { - for (Closeable closeable : closeables) { + closeables = startResult.closeable != null ? Collections.singletonList(startResult.closeable) : null; + + if (first) { + first = false; + Runnable closeTask = new Runnable() { + @Override + public void run() { + if (closeables != null) { + for (Closeable closeable : closeables) { + try { + closeable.close(); + } catch (Throwable t) { + LOG.error("Failed to stop Keycloak container", t); + } + } + } + if (vertxInstance != null) { try { - closeable.close(); + vertxInstance.close(); } catch (Throwable t) { - LOG.error("Failed to stop Keycloak container", t); + LOG.error("Failed to close Vertx instance", t); } } + first = true; + closeables = null; + capturedDevServicesConfiguration = null; + vertxInstance = null; + capturedRealmFileLastModifiedDate = null; } - if (vertxInstance != null) { - try { - vertxInstance.close(); - } catch (Throwable t) { - LOG.error("Failed to close Vertx instance", t); - } - } - first = true; - closeables = null; - capturedDevServicesConfiguration = null; - vertxInstance = null; - capturedRealmFileLastModifiedDate = null; - } - }; - QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread().getContextClassLoader(); - ((QuarkusClassLoader) cl.parent()).addCloseTask(closeTask); - } + }; + QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread().getContextClassLoader(); + ((QuarkusClassLoader) cl.parent()).addCloseTask(closeTask); + } - capturedKeycloakUrl = startResult.url + "/auth"; - if (vertxInstance == null) { - vertxInstance = Vertx.vertx(); + capturedKeycloakUrl = startResult.url + "/auth"; + if (vertxInstance == null) { + vertxInstance = Vertx.vertx(); + } + capturedRealmFileLastModifiedDate = getRealmFileLastModifiedDate(capturedDevServicesConfiguration.realmPath); + compressor.close(); + } catch (Throwable t) { + compressor.closeAndDumpCaptured(); + throw new RuntimeException(t); } - capturedRealmFileLastModifiedDate = getRealmFileLastModifiedDate(capturedDevServicesConfiguration.realmPath); - LOG.info("Dev Services for Keycloak started."); - return prepareConfiguration(startResult.createDefaultRealm, devServices, startResult.shared); + return prepareConfiguration(capturedDevServicesConfiguration.createRealm && startResult.createDefaultRealm, + startResult.realmNameToUse, devServices, startResult.shared); } - private KeycloakDevServicesConfigBuildItem prepareConfiguration(boolean createRealm, + private KeycloakDevServicesConfigBuildItem prepareConfiguration(boolean createRealm, String realmNameToUse, BuildProducer devServices, boolean shared) { - final String authServerUrl = capturedKeycloakUrl + "/realms/" + capturedDevServicesConfiguration.realmName; + final String realmName = realmNameToUse != null ? realmNameToUse : getDefaultRealmName(); + final String authServerUrl = capturedKeycloakUrl + "/realms/" + realmName; String oidcClientId = getOidcClientId(); String oidcClientSecret = getOidcClientSecret(); @@ -206,13 +232,19 @@ private KeycloakDevServicesConfigBuildItem prepareConfiguration(boolean createRe Map configProperties = new HashMap<>(); configProperties.put(KEYCLOAK_URL_KEY, capturedKeycloakUrl); + configProperties.put(KEYCLOAK_REALM_KEY, realmName); configProperties.put(AUTH_SERVER_URL_CONFIG_KEY, authServerUrl); configProperties.put(APPLICATION_TYPE_CONFIG_KEY, oidcApplicationType); configProperties.put(CLIENT_ID_CONFIG_KEY, oidcClientId); configProperties.put(CLIENT_SECRET_CONFIG_KEY, oidcClientSecret); configProperties.put(OIDC_USERS, users); - return new KeycloakDevServicesConfigBuildItem(configProperties); + existingDevServiceConfig = new KeycloakDevServicesConfigBuildItem(configProperties); + return existingDevServiceConfig; + } + + private String getDefaultRealmName() { + return capturedDevServicesConfiguration.realmName.orElse("quarkus"); } private StartResult startContainer(boolean useSharedContainer) { @@ -236,7 +268,7 @@ private StartResult startContainer(boolean useSharedContainer) { } final Optional maybeContainerAddress = keycloakDevModeContainerLocator.locateContainer( - capturedDevServicesConfiguration.realmName, + capturedDevServicesConfiguration.serviceName, capturedDevServicesConfiguration.shared, LaunchMode.current()); @@ -247,6 +279,7 @@ private StartResult startContainer(boolean useSharedContainer) { QuarkusOidcContainer oidcContainer = new QuarkusOidcContainer(dockerImageName, capturedDevServicesConfiguration.port, useSharedContainer, + capturedDevServicesConfiguration.realmName, capturedDevServicesConfiguration.realmPath, capturedDevServicesConfiguration.serviceName, capturedDevServicesConfiguration.shared, @@ -257,6 +290,7 @@ private StartResult startContainer(boolean useSharedContainer) { String url = "http://" + oidcContainer.getHost() + ":" + oidcContainer.getPort(); return new StartResult(url, !oidcContainer.realmFileExists, + oidcContainer.realmNameToUse, new Closeable() { @Override public void close() { @@ -269,7 +303,8 @@ public void close() { }; return maybeContainerAddress - .map(containerAddress -> new StartResult(getSharedContainerUrl(containerAddress), false, null, true)) + .map(containerAddress -> new StartResult(getSharedContainerUrl(containerAddress), false, + null, null, true)) .orElseGet(defaultKeycloakContainerSupplier); } @@ -281,12 +316,14 @@ private String getSharedContainerUrl(ContainerAddress containerAddress) { private static class StartResult { private final String url; private final boolean createDefaultRealm; + private String realmNameToUse; private final Closeable closeable; private final boolean shared; - public StartResult(String url, boolean createDefaultRealm, Closeable closeable, boolean shared) { + public StartResult(String url, boolean createDefaultRealm, String realmNameToUse, Closeable closeable, boolean shared) { this.url = url; this.createDefaultRealm = createDefaultRealm; + this.realmNameToUse = realmNameToUse; this.closeable = closeable; this.shared = shared; } @@ -295,18 +332,22 @@ public StartResult(String url, boolean createDefaultRealm, Closeable closeable, private static class QuarkusOidcContainer extends GenericContainer { private final OptionalInt fixedExposedPort; private final boolean useSharedNetwork; + private final Optional configuredRealmName; private final Optional realmPath; private final String containerLabelValue; private final Optional javaOpts; private final boolean sharedContainer; private boolean realmFileExists; private String hostName = null; + private String realmNameToUse; public QuarkusOidcContainer(DockerImageName dockerImageName, OptionalInt fixedExposedPort, boolean useSharedNetwork, - Optional realmPath, String containerLabelValue, boolean sharedContainer, Optional javaOpts) { + Optional configuredRealmName, Optional realmPath, String containerLabelValue, + boolean sharedContainer, Optional javaOpts) { super(dockerImageName); this.fixedExposedPort = fixedExposedPort; this.useSharedNetwork = useSharedNetwork; + this.configuredRealmName = configuredRealmName; this.realmPath = realmPath; this.containerLabelValue = containerLabelValue; this.sharedContainer = sharedContainer; @@ -343,14 +384,19 @@ protected void configure() { } if (realmPath.isPresent()) { - if (Thread.currentThread().getContextClassLoader().getResource(realmPath.get()) != null) { + URL realmPathUrl = null; + if ((realmPathUrl = Thread.currentThread().getContextClassLoader().getResource(realmPath.get())) != null) { realmFileExists = true; + realmNameToUse = configuredRealmName.isPresent() ? null + : getRealmNameFromRealmFile(realmPathUrl, realmPath.get()); withClasspathResourceMapping(realmPath.get(), KEYCLOAK_DOCKER_REALM_PATH, BindMode.READ_ONLY); addEnv(KEYCLOAK_IMPORT_PROP, KEYCLOAK_DOCKER_REALM_PATH); } else { Path filePath = Paths.get(realmPath.get()); if (Files.exists(filePath)) { realmFileExists = true; + realmNameToUse = configuredRealmName.isPresent() ? null + : getRealmNameFromRealmFile(filePath.toUri(), realmPath.get()); withFileSystemBind(realmPath.get(), KEYCLOAK_DOCKER_REALM_PATH, BindMode.READ_ONLY); addEnv(KEYCLOAK_IMPORT_PROP, KEYCLOAK_DOCKER_REALM_PATH); } else { @@ -362,6 +408,28 @@ protected void configure() { super.setWaitStrategy(Wait.forHttp("/auth").forPort(KEYCLOAK_PORT)); } + private String getRealmNameFromRealmFile(URI uri, String realmPath) { + try { + return getRealmNameFromRealmFile(uri.toURL(), realmPath); + } catch (MalformedURLException ex) { + // Will not happen as this method is called only when it is confirmed the file exists + throw new RuntimeException(ex); + } + } + + private String getRealmNameFromRealmFile(URL url, String realmPath) { + try { + try (InputStream is = url.openStream()) { + JsonObject realmJson = new JsonObject(io.vertx.core.buffer.Buffer.buffer(is.readAllBytes())); + String realmName = realmJson.getString("realm"); + return realmName; + } + } catch (IOException ex) { + LOG.errorf("Realm %s resource can not be opened: %s", realmPath, ex.getMessage()); + } + return null; + } + @Override public String getHost() { if (useSharedNetwork) { @@ -401,9 +469,9 @@ private void createRealm(String keycloakUrl, Map users, String o realm.getUsers().add(createUser(entry.getKey(), entry.getValue(), getUserRoles(entry.getKey()))); } - WebClient client = KeycloakDevServicesUtils.createWebClient(); + WebClient client = OidcDevServicesUtils.createWebClient(vertxInstance); try { - String token = KeycloakDevServicesUtils.getPasswordAccessToken(client, + String token = OidcDevServicesUtils.getPasswordAccessToken(client, keycloakUrl + "/realms/master/protocol/openid-connect/token", "admin-cli", null, "admin", "admin", capturedDevServicesConfiguration.webClienTimeout); @@ -444,7 +512,7 @@ private String[] getUserRoles(String user) { private RealmRepresentation createRealmRep() { RealmRepresentation realm = new RealmRepresentation(); - realm.setRealm(capturedDevServicesConfiguration.realmName); + realm.setRealm(getDefaultRealmName()); realm.setEnabled(true); realm.setUsers(new ArrayList<>()); realm.setClients(new ArrayList<>()); diff --git a/extensions/oidc/deployment/src/main/resources/dev-templates/embedded.html b/extensions/oidc/deployment/src/main/resources/dev-templates/embedded.html index 69c0914f3782ef..bc2b692b4d3475 100644 --- a/extensions/oidc/deployment/src/main/resources/dev-templates/embedded.html +++ b/extensions/oidc/deployment/src/main/resources/dev-templates/embedded.html @@ -6,5 +6,8 @@ Provider: Keycloak -{#else} +{#else if info:authorizationUrl??} + + + Dev Console {/if} diff --git a/extensions/oidc/deployment/src/main/resources/dev-templates/provider.html b/extensions/oidc/deployment/src/main/resources/dev-templates/provider.html index 14779285ad7470..ff78e4b8353619 100644 --- a/extensions/oidc/deployment/src/main/resources/dev-templates/provider.html +++ b/extensions/oidc/deployment/src/main/resources/dev-templates/provider.html @@ -1,5 +1,11 @@ {#include main fluid=true} -{#title}Keycloak{/title} +{#title} +{#if info:keycloakUrl??} +Keycloak +{#else} +OpenId Connect Dev Console +{/if} +{/title} {#script} var port = {config:property('quarkus.http.port')}; @@ -28,8 +34,8 @@ loggedIn === true; $('.implicitLoggedOut').hide(); $('.implicitLoggedIn').show(); - var hash = window.location.hash; - var code = hash.match(/code=([^&]+)/)[1]; + var search = window.location.search; + var code = search.match(/code=([^&]+)/)[1]; exchangeCodeForTokens(code); }else{ loggedIn === false; @@ -71,18 +77,18 @@ return false; } - function signInToKeycloakAndGetTokens() { + function signInToOidcProviderAndGetTokens() { {#if info:oidcGrantType is 'implicit'} - window.location.href = '{info:keycloakUrl}' + "/realms/" + '{info:keycloakRealm}' + "/protocol/openid-connect/auth" - + "?client_id=" + '{info:keycloakClient}' + window.location.href = '{info:authorizationUrl}' + + "?client_id=" + '{info:clientId}' + "&redirect_uri=" + "http%3A%2F%2Flocalhost%3A" + port + "%2Fq%2Fdev%2Fio.quarkus.quarkus-oidc%2Fprovider" - + "&scope=openid&response_type=token id_token&response_mode=fragment&prompt=login" + + "&scope=openid&response_type=token id_token&response_mode=query&prompt=login" + "&nonce=" + makeid(); {#else} - window.location.href = '{info:keycloakUrl}' + "/realms/" + '{info:keycloakRealm}' + "/protocol/openid-connect/auth" - + "?client_id=" + '{info:keycloakClient}' + window.location.href = '{info:authorizationUrl}' + + "?client_id=" + '{info:clientId}' + "&redirect_uri=" + "http%3A%2F%2Flocalhost%3A" + port + "%2Fq%2Fdev%2Fio.quarkus.quarkus-oidc%2Fprovider" - + "&scope=openid&response_type=code&response_mode=fragment&prompt=login" + + "&scope=openid&response_type=code&response_mode=query&prompt=login" + "&nonce=" + makeid(); {/if} } @@ -140,17 +146,16 @@ } function logout() { - window.location.assign('{info:keycloakUrl}' + "/realms/" + '{info:keycloakRealm}' + "/protocol/openid-connect/logout" + window.location.assign('{info:logoutUrl??}' + "?post_logout_redirect_uri=" + "http%3A%2F%2Flocalhost%3A" + port + "%2Fq%2Fdev%2Fio.quarkus.quarkus-oidc%2Fprovider"); } function exchangeCodeForTokens(code){ $.post("exchangeCodeForTokens", { - keycloakUrl: '{info:keycloakUrl}', - realm: '{info:keycloakRealm}', - client: '{info:keycloakClient}', - clientSecret: '{info:keycloakClientSecret}', + tokenUrl: '{info:tokenUrl}', + client: '{info:clientId}', + clientSecret: '{info:clientSecret}', authorizationCode: code, redirectUri: "http://localhost:" + port + "/q/dev/io.quarkus.quarkus-oidc/provider" }, @@ -178,7 +183,9 @@ userName = jsonPayload.preferred_username; } if (userName) { - $('#loggedInUser').append(" Logged in as " + userName); + $('#loggedInUser').append("Logged in as " + userName + " "); + } else { + $('#loggedInUser').append("Logged in "); } } return "

" + 
@@ -212,11 +219,10 @@
     function testServiceWithPassword(userName, servicePath){
         $.post("testService",
             {
-              keycloakUrl: '{info:keycloakUrl}',
+              tokenUrl: '{info:tokenUrl}',
               serviceUrl: "http://localhost:" + port + servicePath,
-              realm: '{info:keycloakRealm}',
-              client: '{info:keycloakClient}',
-              clientSecret: '{info:keycloakClientSecret}',
+              client: '{info:clientId}',
+              clientSecret: '{info:clientSecret}',
               user: userName,
               grant: '{info:oidcGrantType}'
             },
@@ -230,11 +236,10 @@
     function testServiceWithClientCredentials(servicePath) {
         $.post("testService",
             {
-              keycloakUrl: '{info:keycloakUrl}',
+              tokenUrl: '{info:tokenUrl}',
               serviceUrl: "http://localhost:" + port + servicePath,
-              realm: '{info:keycloakRealm}',
-              client: '{info:keycloakClient}',
-              clientSecret: '{info:keycloakClientSecret}',
+              client: '{info:clientId}',
+              clientSecret: '{info:clientSecret}',
               grant: '{info:oidcGrantType}'
             },
             function(data, status){
@@ -264,9 +269,9 @@
 {#body}
 

-{#if info:keycloakUrl??} +{#if info:keycloakAdminUrl??}

@@ -278,7 +283,7 @@
@@ -289,9 +294,12 @@ Your tokens
@@ -430,7 +438,7 @@
Decoded
{#else if info:oidcGrantType is 'client_credentials'}
- Get access token for {info:keycloakClient} and test your service + Get access token for {info:clientId} and test your service
diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeDefaultTenantTestCase.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeDefaultTenantTestCase.java index fe82852eafb169..5cb10d6bf605f9 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeDefaultTenantTestCase.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeDefaultTenantTestCase.java @@ -40,15 +40,15 @@ public void testAccessAndRefreshTokenInjectionDevMode() throws IOException, Inte try { webClient.getPage("http://localhost:8080/protected"); - fail("Exception is expected because auth-server-url is not available and the authentication can not be completed"); + fail("Exception is expected because by default the bearer token is required"); } catch (FailingHttpStatusCodeException ex) { // Reported by Quarkus - assertEquals(500, ex.getStatusCode()); + assertEquals(401, ex.getStatusCode()); } - // Enable auth-server-url + // Enable 'web-app' application type test.modifyResourceFile("application.properties", - s -> s.replace("#quarkus.oidc.auth-server-url", "quarkus.oidc.auth-server-url")); + s -> s.replace("#quarkus.oidc.application-type=web-app", "quarkus.oidc.application-type=web-app")); HtmlPage page = webClient.getPage("http://localhost:8080/protected"); diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeTestCase.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeTestCase.java index 1d55d0df736eea..ed0704e02aae7e 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeTestCase.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CodeFlowDevModeTestCase.java @@ -1,10 +1,12 @@ package io.quarkus.oidc.test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.io.IOException; +import java.net.URI; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; @@ -14,6 +16,8 @@ import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.WebResponse; import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlPage; @@ -87,6 +91,12 @@ public void testAccessAndRefreshTokenInjectionDevMode() throws IOException, Inte assertEquals("custom", page.getWebClient().getCookieManager().getCookie("q_session").getValue().split("\\|")[3]); + webClient.getOptions().setRedirectEnabled(false); + WebResponse webResponse = webClient + .loadWebResponse(new WebRequest(URI.create("http://localhost:8080/protected/logout").toURL())); + assertEquals(302, webResponse.getStatusCode()); + assertNull(webClient.getCookieManager().getCookie("q_session")); + webClient.getCookieManager().clearCookies(); } } diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomTokenStateManager.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomTokenStateManager.java index 220d89e1ff0edc..4a57c25770a12b 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomTokenStateManager.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/CustomTokenStateManager.java @@ -8,6 +8,7 @@ import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.TokenStateManager; import io.quarkus.oidc.runtime.DefaultTokenStateManager; +import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; @ApplicationScoped @@ -18,25 +19,29 @@ public class CustomTokenStateManager implements TokenStateManager { DefaultTokenStateManager tokenStateManager; @Override - public String createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig, - AuthorizationCodeTokens sessionContent) { - return tokenStateManager.createTokenState(routingContext, oidcConfig, sessionContent) + "|custom"; + public Uni createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig, + AuthorizationCodeTokens sessionContent, TokenStateManager.CreateTokenStateRequestContext requestContext) { + return tokenStateManager.createTokenState(routingContext, oidcConfig, sessionContent, requestContext) + .map(t -> (t + "|custom")); } @Override - public AuthorizationCodeTokens getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, - String tokenState) { + public Uni getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, + String tokenState, TokenStateManager.GetTokensRequestContext requestContext) { if (!tokenState.endsWith("|custom")) { throw new IllegalStateException(); } String defaultState = tokenState.substring(0, tokenState.length() - 7); - return tokenStateManager.getTokens(routingContext, oidcConfig, defaultState); + return tokenStateManager.getTokens(routingContext, oidcConfig, defaultState, requestContext); } @Override - public void deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState) { + public Uni deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState, + TokenStateManager.DeleteTokensRequestContext requestContext) { if (!tokenState.endsWith("|custom")) { throw new IllegalStateException(); } + String defaultState = tokenState.substring(0, tokenState.length() - 7); + return tokenStateManager.deleteTokens(routingContext, oidcConfig, defaultState, requestContext); } } diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResource.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResource.java index fa87a328bd5c83..9c1c73e03987bc 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResource.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResource.java @@ -28,4 +28,10 @@ public String getName() { public String getTenantName(@PathParam("id") String tenantId) { return tenantId + ":" + idToken.getName(); } + + @GET + @Path("logout") + public void logout() { + throw new RuntimeException("Logout must be handled by CodeAuthenticationMechanism"); + } } diff --git a/extensions/oidc/deployment/src/test/resources/application-dev-mode-default-tenant.properties b/extensions/oidc/deployment/src/test/resources/application-dev-mode-default-tenant.properties index 989e5ebc777c83..2853d435e676f6 100644 --- a/extensions/oidc/deployment/src/test/resources/application-dev-mode-default-tenant.properties +++ b/extensions/oidc/deployment/src/test/resources/application-dev-mode-default-tenant.properties @@ -1,9 +1,6 @@ -#quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus quarkus.oidc.client-id=quarkus-web-app quarkus.oidc.credentials.secret=secret -quarkus.oidc.application-type=web-app - -quarkus.keycloak.devservices.enabled=false +#quarkus.oidc.application-type=web-app quarkus.log.category."com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL diff --git a/extensions/oidc/deployment/src/test/resources/application-dev-mode.properties b/extensions/oidc/deployment/src/test/resources/application-dev-mode.properties index 15b1c51e72f529..ee7c873dd6e01f 100644 --- a/extensions/oidc/deployment/src/test/resources/application-dev-mode.properties +++ b/extensions/oidc/deployment/src/test/resources/application-dev-mode.properties @@ -5,6 +5,7 @@ quarkus.oidc.client-id=client-dev quarkus.oidc.credentials.client-secret.provider.name=vault-secret-provider quarkus.oidc.credentials.client-secret.provider.key=secret-from-vault quarkus.oidc.application-type=web-app +quarkus.oidc.logout.path=/protected/logout quarkus.log.category."com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL diff --git a/extensions/oidc/runtime/pom.xml b/extensions/oidc/runtime/pom.xml index 162f0a5c95ea4e..d0d4cb9319de46 100644 --- a/extensions/oidc/runtime/pom.xml +++ b/extensions/oidc/runtime/pom.xml @@ -11,7 +11,7 @@ quarkus-oidc Quarkus - OpenID Connect Adapter - Runtime - Secure your applications with OpenID Connect Adapter + Verify Bearer access tokens and authenticate users with Authorization Code Flow io.quarkus diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantConfigResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantConfigResolver.java index c0f9143cef2f97..59fa463895eed0 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantConfigResolver.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantConfigResolver.java @@ -21,10 +21,12 @@ public interface TenantConfigResolver { * * @param context the routing context * @return the tenant configuration. If {@code null}, indicates that the default configuration/tenant should be chosen + * + * @deprecated Use {@link #resolve(RoutingContext, TenantConfigRequestContext))} instead. */ @Deprecated default OidcTenantConfig resolve(RoutingContext context) { - throw new UnsupportedOperationException("resolve not implemented"); + throw new UnsupportedOperationException("resolve is not implemented"); } /** @@ -39,10 +41,10 @@ default Uni resolve(RoutingContext routingContext, TenantConfi } /** - * A context object that can be used to run blocking tasks + * A context object that can be used to run blocking tasks. *

- * Blocking config providers should used this context object to run blocking tasks, to prevent excessive and - * unnecessary delegation to thread pools + * Blocking {@code TenantConfigResolver} providers should use this context object to run blocking tasks, to prevent + * excessive and unnecessary delegation to thread pools. */ interface TenantConfigRequestContext { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenStateManager.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenStateManager.java index 94cf60539cc73b..ecda06f10e225e 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenStateManager.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenStateManager.java @@ -1,5 +1,8 @@ package io.quarkus.oidc; +import java.util.function.Supplier; + +import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; /** @@ -13,9 +16,125 @@ */ public interface TokenStateManager { - String createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig, AuthorizationCodeTokens tokens); + /** + * Convert the authorization code flow tokens into a token state. + * + * @param routingContext the request context + * @param oidcConfig the tenant configuration + * @param tokens the authorization code flow tokens + * + * @return the token state + * + * @deprecated Use + * {@link #createTokenState(RoutingContext, OidcTenantConfig, AuthorizationCodeTokens, CreateTokenStateRequestContext)} + * + */ + @Deprecated + default String createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig, + AuthorizationCodeTokens tokens) { + throw new UnsupportedOperationException("createTokenState is not implemented"); + } + + /** + * Convert the authorization code flow tokens into a token state. + * + * @param routingContext the request context + * @param oidcConfig the tenant configuration + * @param tokens the authorization code flow tokens + * @param requestContext the request context + * + * @return the token state + */ + default Uni createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig, + AuthorizationCodeTokens tokens, CreateTokenStateRequestContext requestContext) { + return Uni.createFrom().item(createTokenState(routingContext, oidcConfig, tokens)); + } + + /** + * Convert the token state into the authorization code flow tokens. + * + * @param routingContext the request context + * @param oidcConfig the tenant configuration + * @param tokens the token state + * + * @return the authorization code flow tokens + * + * @deprecated Use {@link #getTokens(RoutingContext, OidcTenantConfig, String, GetTokensRequestContext)} instead. + */ + @Deprecated + default AuthorizationCodeTokens getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState) { + throw new UnsupportedOperationException("getTokens is not implemented"); + } + + /** + * Convert the token state into the authorization code flow tokens. + * + * @param routingContext the request context + * @param oidcConfig the tenant configuration + * @param tokens the token state + * @param requestContext the request context + * + * @return the authorization code flow tokens + */ + default Uni getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, + String tokenState, GetTokensRequestContext requestContext) { + return Uni.createFrom().item(getTokens(routingContext, oidcConfig, tokenState)); + } + + /** + * Delete the token state. + * + * @param routingContext the request context + * @param oidcConfig the tenant configuration + * @param tokens the token state + * + * @deprecated Use {@link #deleteTokens(RoutingContext, OidcTenantConfig, String, DeleteTokensRequestContext)} instead + */ + @Deprecated + default void deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState) { + throw new UnsupportedOperationException("deleteTokens is not implemented"); + } + + /** + * Delete the token state. + * + * @param routingContext the request context + * @param oidcConfig the tenant configuration + * @param tokens the token state + */ + default Uni deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState, + DeleteTokensRequestContext requestContext) { + deleteTokens(routingContext, oidcConfig, tokenState); + return Uni.createFrom().voidItem(); + } + + /** + * A context object that can be used to create a token state by running a blocking task. + *

+ * Blocking providers should use this context to prevent excessive and unnecessary delegation to thread pools. + */ + interface CreateTokenStateRequestContext { + + Uni runBlocking(Supplier function); + } + + /** + * A context object that can be used to convert the token state to the tokens by running a blocking task. + *

+ * Blocking providers should use this context to prevent excessive and unnecessary delegation to thread pools. + */ + interface GetTokensRequestContext { + + Uni runBlocking(Supplier function); + } - AuthorizationCodeTokens getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState); + /** + * A context object that can be used to delete the token state by running a blocking task. + *

+ * Blocking providers should use this context to prevent excessive and unnecessary delegation to thread pools. + */ + interface DeleteTokensRequestContext { - void deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState); + Uni runBlocking(Supplier function); + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index cb4bb8ccd77016..8e6e5e656fa491 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -10,7 +10,9 @@ import java.util.Optional; import java.util.UUID; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Supplier; import java.util.regex.Pattern; import org.jboss.logging.Logger; @@ -23,8 +25,11 @@ import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.OidcTenantConfig.Authentication; import io.quarkus.oidc.SecurityEvent; +import io.quarkus.oidc.TokenStateManager; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.runtime.BlockingOperationControl; +import io.quarkus.runtime.ExecutorRecorder; import io.quarkus.security.AuthenticationCompletionException; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.AuthenticationRedirectException; @@ -32,6 +37,7 @@ import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.vertx.http.runtime.security.ChallengeData; import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.subscription.UniEmitter; import io.vertx.core.http.Cookie; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.impl.CookieImpl; @@ -47,12 +53,17 @@ public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMecha static final Pattern COOKIE_PATTERN = Pattern.compile("\\" + COOKIE_DELIM); static final String SESSION_COOKIE_NAME = "q_session"; static final String SESSION_MAX_AGE_PARAM = "session-max-age"; + static final Uni VOID_UNI = Uni.createFrom().voidItem(); private static final Logger LOG = Logger.getLogger(CodeAuthenticationMechanism.class); private static final String STATE_COOKIE_NAME = "q_auth"; private static final String POST_LOGOUT_COOKIE_NAME = "q_post_logout"; + private final CreateTokenStateRequestContext createTokenStateRequestContext = new CreateTokenStateRequestContext(); + private final GetTokensRequestContext getTokenStateRequestContext = new GetTokensRequestContext(); + private final DeleteTokensRequestContext deleteTokensRequestContext = new DeleteTokensRequestContext(); + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { return resolver.resolveConfig(context).chain(new Function>() { @@ -96,51 +107,60 @@ private Uni reAuthenticate(Cookie sessionCookie, IdentityProviderManager identityProviderManager, TenantConfigContext configContext) { - AuthorizationCodeTokens session = resolver.getTokenStateManager().getTokens(context, configContext.oidcConfig, - sessionCookie.getValue()); - - context.put(OidcConstants.ACCESS_TOKEN_VALUE, session.getAccessToken()); - context.put(AuthorizationCodeTokens.class.getName(), session); - return authenticate(identityProviderManager, context, new IdTokenCredential(session.getIdToken(), context)) - .map(new Function() { - @Override - public SecurityIdentity apply(SecurityIdentity identity) { - if (isLogout(context, configContext)) { - fireEvent(SecurityEvent.Type.OIDC_LOGOUT_RP_INITIATED, identity); - throw redirectToLogoutEndpoint(context, configContext, session.getIdToken()); - } - return identity; - } - }).onFailure().recoverWithUni(new Function>() { + return resolver.getTokenStateManager().getTokens(context, configContext.oidcConfig, + sessionCookie.getValue(), getTokenStateRequestContext) + .chain(new Function>() { @Override - public Uni apply(Throwable t) { - if (t instanceof AuthenticationRedirectException) { - throw (AuthenticationRedirectException) t; - } + public Uni apply(AuthorizationCodeTokens session) { + context.put(OidcConstants.ACCESS_TOKEN_VALUE, session.getAccessToken()); + context.put(AuthorizationCodeTokens.class.getName(), session); + return authenticate(identityProviderManager, context, + new IdTokenCredential(session.getIdToken(), context)) + .call(new Function>() { + @Override + public Uni apply(SecurityIdentity identity) { + if (isLogout(context, configContext)) { + fireEvent(SecurityEvent.Type.OIDC_LOGOUT_RP_INITIATED, identity); + return buildLogoutRedirectUriUni(context, configContext, + session.getIdToken()); + } + return VOID_UNI; + } + }).onFailure() + .recoverWithUni(new Function>() { + @Override + public Uni apply(Throwable t) { + if (t instanceof AuthenticationRedirectException) { + throw (AuthenticationRedirectException) t; + } - if (!(t instanceof TokenAutoRefreshException)) { - boolean expired = (t.getCause() instanceof InvalidJwtException) - && ((InvalidJwtException) t.getCause()).hasErrorCode(ErrorCodes.EXPIRED); + if (!(t instanceof TokenAutoRefreshException)) { + boolean expired = (t.getCause() instanceof InvalidJwtException) + && ((InvalidJwtException) t.getCause()) + .hasErrorCode(ErrorCodes.EXPIRED); - if (!expired) { - LOG.debugf("Authentication failure: %s", t.getCause()); - throw new AuthenticationCompletionException(t.getCause()); - } - if (!configContext.oidcConfig.token.refreshExpired) { - LOG.debug("Token has expired, token refresh is not allowed"); - throw new AuthenticationCompletionException(t.getCause()); - } - LOG.debug("Token has expired, trying to refresh it"); - return refreshSecurityIdentity(configContext, session.getRefreshToken(), context, - identityProviderManager, false, null); - } else { - return refreshSecurityIdentity(configContext, session.getRefreshToken(), context, - identityProviderManager, true, - ((TokenAutoRefreshException) t).getSecurityIdentity()); - } + if (!expired) { + LOG.debugf("Authentication failure: %s", t.getCause()); + throw new AuthenticationCompletionException(t.getCause()); + } + if (!configContext.oidcConfig.token.refreshExpired) { + LOG.debug("Token has expired, token refresh is not allowed"); + throw new AuthenticationCompletionException(t.getCause()); + } + LOG.debug("Token has expired, trying to refresh it"); + return refreshSecurityIdentity(configContext, session.getRefreshToken(), + context, + identityProviderManager, false, null); + } else { + return refreshSecurityIdentity(configContext, session.getRefreshToken(), + context, + identityProviderManager, true, + ((TokenAutoRefreshException) t).getSecurityIdentity()); + } + } + }); } }); - } private boolean isJavaScript(RoutingContext context) { @@ -168,54 +188,63 @@ public Uni apply(TenantConfigContext tenantContext) { } public Uni getChallengeInternal(RoutingContext context, TenantConfigContext configContext) { - removeCookie(context, configContext, getSessionCookieName(configContext.oidcConfig)); - - if (!shouldAutoRedirect(configContext, context)) { - // If the client (usually an SPA) wants to handle the redirect manually, then - // return status code 499 and WWW-Authenticate header with the 'OIDC' value. - return Uni.createFrom().item(new ChallengeData(499, "WWW-Authenticate", "OIDC")); - } - - StringBuilder codeFlowParams = new StringBuilder(); - - // response_type - codeFlowParams.append(OidcConstants.CODE_FLOW_RESPONSE_TYPE).append(EQ).append(OidcConstants.CODE_FLOW_CODE); - - // client_id - codeFlowParams.append(AMP).append(OidcConstants.CLIENT_ID).append(EQ) - .append(OidcCommonUtils.urlEncode(configContext.oidcConfig.clientId.get())); - - // scope - List scopes = new ArrayList<>(); - scopes.add("openid"); - configContext.oidcConfig.getAuthentication().scopes.ifPresent(scopes::addAll); - codeFlowParams.append(AMP).append(OidcConstants.TOKEN_SCOPE).append(EQ) - .append(OidcCommonUtils.urlEncode(String.join(" ", scopes))); - - // redirect_uri - String redirectPath = getRedirectPath(configContext, context); - String redirectUriParam = buildUri(context, isForceHttps(configContext), redirectPath); - LOG.debugf("Authentication request redirect_uri parameter: %s", redirectUriParam); + return removeSessionCookie(context, configContext, getSessionCookieName(configContext.oidcConfig)) + .chain(new Function>() { - codeFlowParams.append(AMP).append(OidcConstants.CODE_FLOW_REDIRECT_URI).append(EQ) - .append(OidcCommonUtils.urlEncode(redirectUriParam)); + @Override + public Uni apply(Void t) { + if (!shouldAutoRedirect(configContext, context)) { + // If the client (usually an SPA) wants to handle the redirect manually, then + // return status code 499 and WWW-Authenticate header with the 'OIDC' value. + return Uni.createFrom().item(new ChallengeData(499, "WWW-Authenticate", "OIDC")); + } - // state - codeFlowParams.append(AMP).append(OidcConstants.CODE_FLOW_STATE).append(EQ) - .append(generateCodeFlowState(context, configContext, redirectPath)); + StringBuilder codeFlowParams = new StringBuilder(); + + // response_type + codeFlowParams.append(OidcConstants.CODE_FLOW_RESPONSE_TYPE).append(EQ) + .append(OidcConstants.CODE_FLOW_CODE); + + // client_id + codeFlowParams.append(AMP).append(OidcConstants.CLIENT_ID).append(EQ) + .append(OidcCommonUtils.urlEncode(configContext.oidcConfig.clientId.get())); + + // scope + List scopes = new ArrayList<>(); + scopes.add("openid"); + configContext.oidcConfig.getAuthentication().scopes.ifPresent(scopes::addAll); + codeFlowParams.append(AMP).append(OidcConstants.TOKEN_SCOPE).append(EQ) + .append(OidcCommonUtils.urlEncode(String.join(" ", scopes))); + + // redirect_uri + String redirectPath = getRedirectPath(configContext, context); + String redirectUriParam = buildUri(context, isForceHttps(configContext), redirectPath); + LOG.debugf("Authentication request redirect_uri parameter: %s", redirectUriParam); + + codeFlowParams.append(AMP).append(OidcConstants.CODE_FLOW_REDIRECT_URI).append(EQ) + .append(OidcCommonUtils.urlEncode(redirectUriParam)); + + // state + codeFlowParams.append(AMP).append(OidcConstants.CODE_FLOW_STATE).append(EQ) + .append(generateCodeFlowState(context, configContext, redirectPath)); + + // extra redirect parameters, see https://openid.net/specs/openid-connect-core-1_0.html#AuthRequests + if (configContext.oidcConfig.authentication.getExtraParams() != null) { + for (Map.Entry entry : configContext.oidcConfig.authentication.getExtraParams() + .entrySet()) { + codeFlowParams.append(AMP).append(entry.getKey()).append(EQ) + .append(OidcCommonUtils.urlEncode(entry.getValue())); + } + } - // extra redirect parameters, see https://openid.net/specs/openid-connect-core-1_0.html#AuthRequests - if (configContext.oidcConfig.authentication.getExtraParams() != null) { - for (Map.Entry entry : configContext.oidcConfig.authentication.getExtraParams().entrySet()) { - codeFlowParams.append(AMP).append(entry.getKey()).append(EQ) - .append(OidcCommonUtils.urlEncode(entry.getValue())); - } - } + String authorizationURL = configContext.provider.getMetadata().getAuthorizationUri() + "?" + + codeFlowParams.toString(); - String authorizationURL = configContext.provider.getMetadata().getAuthorizationUri() + "?" + codeFlowParams.toString(); + return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.FOUND.code(), HttpHeaders.LOCATION, + authorizationURL)); + } - return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.FOUND.code(), HttpHeaders.LOCATION, - authorizationURL)); + }); } private Uni performCodeFlow(IdentityProviderManager identityProviderManager, @@ -278,12 +307,16 @@ public Uni apply(final AuthorizationCodeTokens tokens, final T return authenticate(identityProviderManager, context, new IdTokenCredential(tokens.getIdToken(), context)) + .call(new Function>() { + @Override + public Uni apply(SecurityIdentity identity) { + return processSuccessfulAuthentication(context, configContext, + tokens, identity); + } + }) .map(new Function() { @Override public SecurityIdentity apply(SecurityIdentity identity) { - processSuccessfulAuthentication(context, configContext, - tokens, identity); - boolean removeRedirectParams = configContext.oidcConfig.authentication .isRemoveRedirectParameters(); if (removeRedirectParams || finalUserPath != null @@ -325,31 +358,48 @@ public Throwable apply(Throwable tInner) { } - private void processSuccessfulAuthentication(RoutingContext context, + private Uni processSuccessfulAuthentication(RoutingContext context, TenantConfigContext configContext, AuthorizationCodeTokens tokens, SecurityIdentity securityIdentity) { - removeCookie(context, configContext, getSessionCookieName(configContext.oidcConfig)); + return removeSessionCookie(context, configContext, getSessionCookieName(configContext.oidcConfig)) + .chain(new Function>() { - JsonObject idToken = OidcUtils.decodeJwtContent(tokens.getIdToken()); + @Override + public Uni apply(Void t) { + JsonObject idToken = OidcUtils.decodeJwtContent(tokens.getIdToken()); - if (!idToken.containsKey("exp") || !idToken.containsKey("iat")) { - LOG.debug("ID Token is required to contain 'exp' and 'iat' claims"); - throw new AuthenticationCompletionException(); - } - long maxAge = idToken.getLong("exp") - idToken.getLong("iat"); - if (configContext.oidcConfig.token.lifespanGrace.isPresent()) { - maxAge += configContext.oidcConfig.token.lifespanGrace.getAsInt(); - } - if (configContext.oidcConfig.token.refreshExpired) { - maxAge += configContext.oidcConfig.authentication.sessionAgeExtension.getSeconds(); - } - context.put(SESSION_MAX_AGE_PARAM, maxAge); - String cookieValue = resolver.getTokenStateManager() - .createTokenState(context, configContext.oidcConfig, tokens); - createCookie(context, configContext.oidcConfig, getSessionCookieName(configContext.oidcConfig), cookieValue, maxAge); + if (!idToken.containsKey("exp") || !idToken.containsKey("iat")) { + LOG.debug("ID Token is required to contain 'exp' and 'iat' claims"); + throw new AuthenticationCompletionException(); + } + long maxAge = idToken.getLong("exp") - idToken.getLong("iat"); + if (configContext.oidcConfig.token.lifespanGrace.isPresent()) { + maxAge += configContext.oidcConfig.token.lifespanGrace.getAsInt(); + } + if (configContext.oidcConfig.token.refreshExpired) { + maxAge += configContext.oidcConfig.authentication.sessionAgeExtension.getSeconds(); + } + final long sessionMaxAge = maxAge; + context.put(SESSION_MAX_AGE_PARAM, maxAge); + return resolver.getTokenStateManager() + .createTokenState(context, configContext.oidcConfig, tokens, createTokenStateRequestContext) + .map(new Function() { + + @Override + public Void apply(String cookieValue) { + createCookie(context, configContext.oidcConfig, + getSessionCookieName(configContext.oidcConfig), + cookieValue, sessionMaxAge); + fireEvent(SecurityEvent.Type.OIDC_LOGIN, securityIdentity); + return null; + } + + }); + } + + }); - fireEvent(SecurityEvent.Type.OIDC_LOGIN, securityIdentity); } private void fireEvent(SecurityEvent.Type eventType, SecurityIdentity securityIdentity) { @@ -441,14 +491,24 @@ private String buildUri(RoutingContext context, boolean forceHttps, String autho .toString(); } - private void removeCookie(RoutingContext context, TenantConfigContext configContext, String cookieName) { + private Uni removeSessionCookie(RoutingContext context, TenantConfigContext configContext, String cookieName) { + String cookieValue = removeCookie(context, configContext, cookieName); + if (cookieValue != null) { + return resolver.getTokenStateManager().deleteTokens(context, configContext.oidcConfig, cookieValue, + deleteTokensRequestContext); + } else { + return VOID_UNI; + } + } + + private String removeCookie(RoutingContext context, TenantConfigContext configContext, String cookieName) { ServerCookie cookie = (ServerCookie) context.cookieMap().get(cookieName); + String cookieValue = null; if (cookie != null) { - if (SESSION_COOKIE_NAME.equals(cookieName)) { - resolver.getTokenStateManager().deleteTokens(context, configContext.oidcConfig, cookie.getValue()); - } + cookieValue = cookie.getValue(); removeCookie(context, cookie, configContext.oidcConfig); } + return cookieValue; } static void removeCookie(RoutingContext context, ServerCookie cookie, OidcTenantConfig oidcConfig) { @@ -500,12 +560,17 @@ public Uni apply(final AuthorizationCodeTokens tokens, final T return authenticate(identityProviderManager, context, new IdTokenCredential(tokens.getIdToken(), context)) - .map(new Function() { + .call(new Function>() { @Override - public SecurityIdentity apply(SecurityIdentity identity) { + public Uni apply(SecurityIdentity identity) { // after a successful refresh, rebuild the identity and update the cookie - processSuccessfulAuthentication(context, configContext, + return processSuccessfulAuthentication(context, configContext, tokens, identity); + } + }) + .map(new Function() { + @Override + public SecurityIdentity apply(SecurityIdentity identity) { fireEvent(autoRefresh ? SecurityEvent.Type.OIDC_SESSION_REFRESHED : SecurityEvent.Type.OIDC_SESSION_EXPIRED_AND_REFRESHED, identity); @@ -555,10 +620,15 @@ private boolean isForceHttps(TenantConfigContext configContext) { return configContext.oidcConfig.authentication.forceRedirectHttpsScheme; } - private AuthenticationRedirectException redirectToLogoutEndpoint(RoutingContext context, TenantConfigContext configContext, + private Uni buildLogoutRedirectUriUni(RoutingContext context, TenantConfigContext configContext, String idToken) { - removeCookie(context, configContext, getSessionCookieName(configContext.oidcConfig)); - return new AuthenticationRedirectException(buildLogoutRedirectUri(configContext, idToken, context)); + return removeSessionCookie(context, configContext, getSessionCookieName(configContext.oidcConfig)) + .map(new Function() { + @Override + public Void apply(Void t) { + throw new AuthenticationRedirectException(buildLogoutRedirectUri(configContext, idToken, context)); + } + }); } private static String getStateCookieName(TenantConfigContext configContext) { @@ -580,4 +650,48 @@ static String getCookieSuffix(String tenantId) { return !"Default".equals(tenantId) ? "_" + tenantId : ""; } + private static class CreateTokenStateRequestContext extends BlockingTaskRunner + implements TokenStateManager.CreateTokenStateRequestContext { + } + + private static class GetTokensRequestContext extends BlockingTaskRunner + implements TokenStateManager.GetTokensRequestContext { + } + + private static class DeleteTokensRequestContext extends BlockingTaskRunner + implements TokenStateManager.DeleteTokensRequestContext { + } + + private static class BlockingTaskRunner { + public Uni runBlocking(Supplier function) { + return Uni.createFrom().deferred(new Supplier>() { + @Override + public Uni get() { + if (BlockingOperationControl.isBlockingAllowed()) { + try { + return Uni.createFrom().item(function.get()); + } catch (Throwable t) { + return Uni.createFrom().failure(t); + } + } else { + return Uni.createFrom().emitter(new Consumer>() { + @Override + public void accept(UniEmitter uniEmitter) { + ExecutorRecorder.getCurrent().execute(new Runnable() { + @Override + public void run() { + try { + uniEmitter.complete(function.get()); + } catch (Throwable t) { + uniEmitter.fail(t); + } + } + }); + } + }); + } + } + }); + } + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java index 2b0a951258c24a..b5f05b044882dd 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java @@ -5,6 +5,7 @@ import io.quarkus.oidc.AuthorizationCodeTokens; import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.TokenStateManager; +import io.smallrye.mutiny.Uni; import io.vertx.core.http.Cookie; import io.vertx.core.http.impl.ServerCookie; import io.vertx.ext.web.RoutingContext; @@ -16,8 +17,8 @@ public class DefaultTokenStateManager implements TokenStateManager { private static final String SESSION_RT_COOKIE_NAME = CodeAuthenticationMechanism.SESSION_COOKIE_NAME + "_rt"; @Override - public String createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig, - AuthorizationCodeTokens tokens) { + public Uni createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig, + AuthorizationCodeTokens tokens, TokenStateManager.CreateTokenStateRequestContext requestContext) { StringBuilder sb = new StringBuilder(); sb.append(tokens.getIdToken()); if (oidcConfig.tokenStateManager.strategy == OidcTenantConfig.TokenStateManager.Strategy.KEEP_ALL_TOKENS) { @@ -56,11 +57,12 @@ public String createTokenState(RoutingContext routingContext, OidcTenantConfig o } } } - return sb.toString(); + return Uni.createFrom().item(sb.toString()); } @Override - public AuthorizationCodeTokens getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState) { + public Uni getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState, + TokenStateManager.GetTokensRequestContext requestContext) { String[] tokens = CodeAuthenticationMechanism.COOKIE_PATTERN.split(tokenState); String idToken = tokens[0]; @@ -91,17 +93,19 @@ public AuthorizationCodeTokens getTokens(RoutingContext routingContext, OidcTena } } - return new AuthorizationCodeTokens(idToken, accessToken, refreshToken); + return Uni.createFrom().item(new AuthorizationCodeTokens(idToken, accessToken, refreshToken)); } @Override - public void deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState) { + public Uni deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState, + TokenStateManager.DeleteTokensRequestContext requestContext) { if (oidcConfig.tokenStateManager.splitTokens) { CodeAuthenticationMechanism.removeCookie(routingContext, getAccessTokenCookie(routingContext, oidcConfig), oidcConfig); CodeAuthenticationMechanism.removeCookie(routingContext, getRefreshTokenCookie(routingContext, oidcConfig), oidcConfig); } + return CodeAuthenticationMechanism.VOID_UNI; } private static ServerCookie getAccessTokenCookie(RoutingContext routingContext, OidcTenantConfig oidcConfig) { diff --git a/extensions/opentelemetry/opentelemetry/deployment/pom.xml b/extensions/opentelemetry/opentelemetry/deployment/pom.xml index c066a7c1f65030..f124d4a8646183 100644 --- a/extensions/opentelemetry/opentelemetry/deployment/pom.xml +++ b/extensions/opentelemetry/opentelemetry/deployment/pom.xml @@ -69,12 +69,6 @@ quarkus-resteasy-deployment test - - io.quarkus - quarkus-vertx-http-deployment - test - test-jar - io.quarkus quarkus-smallrye-health-deployment diff --git a/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryContinuousTestingTest.java b/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryContinuousTestingTest.java index 8f213de335f5c3..8345afa9615b9e 100644 --- a/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryContinuousTestingTest.java +++ b/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryContinuousTestingTest.java @@ -7,9 +7,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.test.ContinuousTestingTestUtils; +import io.quarkus.test.ContinuousTestingTestUtils.TestStatus; import io.quarkus.test.QuarkusDevModeTest; -import io.quarkus.vertx.http.deployment.devmode.tests.TestStatus; -import io.quarkus.vertx.http.testrunner.ContinuousTestingTestUtils; public class OpenTelemetryContinuousTestingTest { @RegisterExtension diff --git a/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDevModeTest.java b/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDevModeTest.java index 801673acdb8721..b7dc348682d116 100644 --- a/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDevModeTest.java +++ b/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDevModeTest.java @@ -8,8 +8,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.test.ContinuousTestingTestUtils; import io.quarkus.test.QuarkusDevModeTest; -import io.quarkus.vertx.http.testrunner.ContinuousTestingTestUtils; import io.restassured.RestAssured; public class OpenTelemetryDevModeTest { diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/restclient/ClientTracingFilter.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/restclient/ClientTracingFilter.java index 9d34efb149b285..5f43898887b75e 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/restclient/ClientTracingFilter.java +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/restclient/ClientTracingFilter.java @@ -26,7 +26,7 @@ @Priority(Priorities.HEADER_DECORATOR) public class ClientTracingFilter implements ClientRequestFilter, ClientResponseFilter { - private static final TextMapPropagator TEXT_MAP_PROPAGATOR = GlobalOpenTelemetry.getPropagators().getTextMapPropagator(); + private final TextMapPropagator TEXT_MAP_PROPAGATOR = GlobalOpenTelemetry.getPropagators().getTextMapPropagator(); private static final String SCOPE_KEY = ClientTracingFilter.class.getName() + ".scope"; private static final String SPAN_KEY = ClientTracingFilter.class.getName() + ".span"; diff --git a/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/PanacheJAXBTest.java b/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/PanacheJAXBTest.java index 8672238e57defe..ea251d1fcc9136 100644 --- a/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/PanacheJAXBTest.java +++ b/extensions/panache/hibernate-orm-panache/deployment/src/test/java/io/quarkus/hibernate/orm/panache/deployment/test/PanacheJAXBTest.java @@ -17,6 +17,7 @@ import javax.xml.bind.annotation.XmlTransient; import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -30,7 +31,9 @@ public class PanacheJAXBTest { @RegisterExtension static QuarkusUnitTest runner = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) - .addClasses(JAXBEntity.class, JAXBTestResource.class)); + .addClasses(JAXBEntity.class, JAXBTestResource.class) + .addAsResource(new StringAsset("quarkus.hibernate-orm.database.generation=none"), + "application.properties")); @Test public void testJaxbAnnotationTransfer() throws Exception { diff --git a/extensions/panache/hibernate-orm-panache/deployment/src/test/resources/application-erroneous-multiple-persistence-units.properties b/extensions/panache/hibernate-orm-panache/deployment/src/test/resources/application-erroneous-multiple-persistence-units.properties index cbcb3eec5ffa3e..d6b78c1f0c86d0 100644 --- a/extensions/panache/hibernate-orm-panache/deployment/src/test/resources/application-erroneous-multiple-persistence-units.properties +++ b/extensions/panache/hibernate-orm-panache/deployment/src/test/resources/application-erroneous-multiple-persistence-units.properties @@ -1,5 +1,5 @@ quarkus.datasource.db-kind=h2 -quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1 +quarkus.datasource.jdbc.url=jdbc:h2:mem:first;DB_CLOSE_DELAY=-1 quarkus.datasource.second.db-kind=h2 quarkus.datasource.second.jdbc.url=jdbc:h2:mem:second;DB_CLOSE_DELAY=-1 diff --git a/extensions/panache/hibernate-orm-panache/deployment/src/test/resources/application-multiple-persistence-units-for-repository.properties b/extensions/panache/hibernate-orm-panache/deployment/src/test/resources/application-multiple-persistence-units-for-repository.properties index d257a345b938f0..7111224eafffe8 100644 --- a/extensions/panache/hibernate-orm-panache/deployment/src/test/resources/application-multiple-persistence-units-for-repository.properties +++ b/extensions/panache/hibernate-orm-panache/deployment/src/test/resources/application-multiple-persistence-units-for-repository.properties @@ -1,5 +1,5 @@ quarkus.datasource.db-kind=h2 -quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1 +quarkus.datasource.jdbc.url=jdbc:h2:mem:first;DB_CLOSE_DELAY=-1 quarkus.datasource.repository.db-kind=h2 quarkus.datasource.repository.jdbc.url=jdbc:h2:mem:repository;DB_CLOSE_DELAY=-1 diff --git a/extensions/panache/hibernate-orm-panache/deployment/src/test/resources/application-multiple-persistence-units.properties b/extensions/panache/hibernate-orm-panache/deployment/src/test/resources/application-multiple-persistence-units.properties index 777cc071973323..2a5bbd813f0532 100644 --- a/extensions/panache/hibernate-orm-panache/deployment/src/test/resources/application-multiple-persistence-units.properties +++ b/extensions/panache/hibernate-orm-panache/deployment/src/test/resources/application-multiple-persistence-units.properties @@ -1,5 +1,5 @@ quarkus.datasource.db-kind=h2 -quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1 +quarkus.datasource.jdbc.url=jdbc:h2:mem:first;DB_CLOSE_DELAY=-1 quarkus.datasource.second.db-kind=h2 quarkus.datasource.second.jdbc.url=jdbc:h2:mem:second;DB_CLOSE_DELAY=-1 diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/pom.xml b/extensions/panache/hibernate-orm-rest-data-panache/deployment/pom.xml index 0a23562bece626..da7f91a34c697e 100644 --- a/extensions/panache/hibernate-orm-rest-data-panache/deployment/pom.xml +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/pom.xml @@ -37,7 +37,7 @@ io.quarkus - quarkus-resteasy-jsonb-deployment + quarkus-resteasy-reactive-jsonb-deployment test diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/HibernateOrmPanacheRestProcessor.java b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/HibernateOrmPanacheRestProcessor.java index 5af71d2b4b5112..72c049a936f071 100644 --- a/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/HibernateOrmPanacheRestProcessor.java +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/orm/rest/data/panache/deployment/HibernateOrmPanacheRestProcessor.java @@ -5,6 +5,8 @@ import java.lang.reflect.Modifier; import java.util.List; +import javax.ws.rs.Priorities; + import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; @@ -23,9 +25,10 @@ import io.quarkus.hibernate.orm.rest.data.panache.PanacheRepositoryResource; import io.quarkus.hibernate.orm.rest.data.panache.runtime.RestDataPanacheExceptionMapper; import io.quarkus.hibernate.orm.rest.data.panache.runtime.jta.TransactionalUpdateExecutor; +import io.quarkus.rest.data.panache.RestDataPanacheException; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.RestDataResourceBuildItem; -import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; +import io.quarkus.resteasy.reactive.spi.ExceptionMapperBuildItem; class HibernateOrmPanacheRestProcessor { @@ -41,8 +44,9 @@ FeatureBuildItem feature() { } @BuildStep - ResteasyJaxrsProviderBuildItem registerRestDataPanacheExceptionMapper() { - return new ResteasyJaxrsProviderBuildItem(RestDataPanacheExceptionMapper.class.getName()); + ExceptionMapperBuildItem registerRestDataPanacheExceptionMapper() { + return new ExceptionMapperBuildItem(RestDataPanacheExceptionMapper.class.getName(), + RestDataPanacheException.class.getName(), Priorities.USER + 100, false); } @BuildStep diff --git a/extensions/panache/hibernate-orm-rest-data-panache/runtime/pom.xml b/extensions/panache/hibernate-orm-rest-data-panache/runtime/pom.xml index 64b292a742ffe1..b0aa7a2a9e871d 100644 --- a/extensions/panache/hibernate-orm-rest-data-panache/runtime/pom.xml +++ b/extensions/panache/hibernate-orm-rest-data-panache/runtime/pom.xml @@ -22,6 +22,10 @@ io.quarkus quarkus-hibernate-orm-panache + + jakarta.validation + jakarta.validation-api + diff --git a/extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/MongoPanacheRestProcessor.java b/extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/MongoPanacheRestProcessor.java index 684a70d9ca9a2f..f2764201458b79 100644 --- a/extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/MongoPanacheRestProcessor.java +++ b/extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/MongoPanacheRestProcessor.java @@ -5,6 +5,8 @@ import java.lang.reflect.Modifier; import java.util.List; +import javax.ws.rs.Priorities; + import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; @@ -27,9 +29,10 @@ import io.quarkus.mongodb.rest.data.panache.PanacheMongoRepositoryResource; import io.quarkus.mongodb.rest.data.panache.runtime.NoopUpdateExecutor; import io.quarkus.mongodb.rest.data.panache.runtime.RestDataPanacheExceptionMapper; +import io.quarkus.rest.data.panache.RestDataPanacheException; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.RestDataResourceBuildItem; -import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; +import io.quarkus.resteasy.reactive.spi.ExceptionMapperBuildItem; class MongoPanacheRestProcessor { @@ -45,8 +48,9 @@ FeatureBuildItem feature() { } @BuildStep - ResteasyJaxrsProviderBuildItem registerRestDataPanacheExceptionMapper() { - return new ResteasyJaxrsProviderBuildItem(RestDataPanacheExceptionMapper.class.getName()); + ExceptionMapperBuildItem registerRestDataPanacheExceptionMapper() { + return new ExceptionMapperBuildItem(RestDataPanacheExceptionMapper.class.getName(), + RestDataPanacheException.class.getName(), Priorities.USER + 100, false); } @BuildStep diff --git a/extensions/panache/rest-data-panache/deployment/pom.xml b/extensions/panache/rest-data-panache/deployment/pom.xml index d3b0d7ec113a8a..56e8cd354bce5e 100644 --- a/extensions/panache/rest-data-panache/deployment/pom.xml +++ b/extensions/panache/rest-data-panache/deployment/pom.xml @@ -23,7 +23,7 @@ io.quarkus - quarkus-resteasy-deployment + quarkus-resteasy-reactive-links-deployment io.quarkus @@ -66,4 +66,4 @@ - \ No newline at end of file + diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/RestDataProcessor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/RestDataProcessor.java index 5b9ae46ff75d09..4a26a76f87d24f 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/RestDataProcessor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/RestDataProcessor.java @@ -3,16 +3,12 @@ import java.util.Arrays; import java.util.List; -import org.jboss.resteasy.links.impl.EL; - -import io.quarkus.arc.deployment.GeneratedBeanBuildItem; -import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; -import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.gizmo.ClassOutput; import io.quarkus.jackson.spi.JacksonModuleBuildItem; import io.quarkus.jsonb.spi.JsonbSerializerBuildItem; @@ -28,14 +24,21 @@ import io.quarkus.rest.data.panache.runtime.hal.HalLink; import io.quarkus.rest.data.panache.runtime.hal.HalLinkJacksonSerializer; import io.quarkus.rest.data.panache.runtime.hal.HalLinkJsonbSerializer; +import io.quarkus.resteasy.reactive.server.deployment.GeneratedJaxRsResourceGizmoAdaptor; +import io.quarkus.resteasy.reactive.spi.GeneratedJaxRsResourceBuildItem; public class RestDataProcessor { + @BuildStep + ReflectiveClassBuildItem registerReflection() { + return new ReflectiveClassBuildItem(true, true, HalLink.class); + } + @BuildStep void implementResources(CombinedIndexBuildItem index, List resourceBuildItems, List resourcePropertiesBuildItems, Capabilities capabilities, - BuildProducer implementationsProducer) { - ClassOutput classOutput = new GeneratedBeanGizmoAdaptor(implementationsProducer); + BuildProducer implementationsProducer) { + ClassOutput classOutput = new GeneratedJaxRsResourceGizmoAdaptor(implementationsProducer); JaxRsResourceImplementor jaxRsResourceImplementor = new JaxRsResourceImplementor(hasValidatorCapability(capabilities)); ResourcePropertiesProvider resourcePropertiesProvider = new ResourcePropertiesProvider(index.getIndex()); @@ -71,11 +74,6 @@ JsonbSerializerBuildItem registerJsonbSerializers() { HalLinkJsonbSerializer.class.getName())); } - @BuildStep - RuntimeInitializedClassBuildItem el() { - return new RuntimeInitializedClassBuildItem(EL.class.getCanonicalName()); - } - private ResourceProperties getResourceProperties(ResourcePropertiesProvider resourcePropertiesProvider, ResourceMetadata resourceMetadata, List resourcePropertiesBuildItems) { for (ResourcePropertiesBuildItem resourcePropertiesBuildItem : resourcePropertiesBuildItems) { @@ -92,7 +90,7 @@ private boolean hasValidatorCapability(Capabilities capabilities) { } private boolean hasHalCapability(Capabilities capabilities) { - return capabilities.isPresent(Capability.RESTEASY_JSON_JSONB) - || capabilities.isPresent(Capability.RESTEASY_JSON_JACKSON); + return capabilities.isPresent(Capability.RESTEASY_REACTIVE_JSON_JSONB) + || capabilities.isPresent(Capability.RESTEASY_REACTIVE_JSON_JACKSON); } } diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/StandardMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/StandardMethodImplementor.java index bc2c937f65c195..eef6fd4c1602dc 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/StandardMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/StandardMethodImplementor.java @@ -12,7 +12,7 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; -import org.jboss.resteasy.links.LinkResource; +import org.jboss.logging.Logger; import io.quarkus.gizmo.AnnotatedElement; import io.quarkus.gizmo.AnnotationCreator; @@ -25,12 +25,15 @@ import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; import io.quarkus.rest.data.panache.runtime.sort.SortQueryParamValidator; +import io.quarkus.resteasy.reactive.links.RestLink; /** * A standard JAX-RS method implementor. */ public abstract class StandardMethodImplementor implements MethodImplementor { + private static final Logger LOGGER = Logger.getLogger(StandardMethodImplementor.class); + /** * Implement exposed JAX-RS method. */ @@ -78,9 +81,15 @@ protected void addDeleteAnnotation(AnnotatedElement element) { } protected void addLinksAnnotation(AnnotatedElement element, String entityClassName, String rel) { - AnnotationCreator linkResource = element.addAnnotation(LinkResource.class); - linkResource.addValue("entityClassName", entityClassName); - linkResource.addValue("rel", rel); + AnnotationCreator linkResource = element.addAnnotation(RestLink.class); + Class entityClass; + try { + entityClass = Thread.currentThread().getContextClassLoader().loadClass(entityClassName); + linkResource.addValue("entityType", entityClass); + linkResource.addValue("rel", rel); + } catch (ClassNotFoundException e) { + LOGGER.error("Unable to create links for entity: '" + entityClassName + "'", e); + } } protected void addPathAnnotation(AnnotatedElement element, String value) { diff --git a/extensions/panache/rest-data-panache/runtime/pom.xml b/extensions/panache/rest-data-panache/runtime/pom.xml index 9183fc75e140ce..b6666464f792c8 100644 --- a/extensions/panache/rest-data-panache/runtime/pom.xml +++ b/extensions/panache/rest-data-panache/runtime/pom.xml @@ -19,17 +19,7 @@ io.quarkus - quarkus-resteasy - - - org.jboss.resteasy - resteasy-links - - - jakarta.activation - jakarta.activation-api - - + quarkus-resteasy-reactive-links io.quarkus diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapperJacksonSerializer.java b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapperJacksonSerializer.java index b0c31d39d09c01..8efc74b8b5d833 100644 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapperJacksonSerializer.java +++ b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapperJacksonSerializer.java @@ -32,22 +32,19 @@ public void serialize(HalEntityWrapper wrapper, JsonGenerator generator, Seriali for (BeanPropertyDefinition property : getPropertyDefinitions(serializers, wrapper.getEntity().getClass())) { AnnotatedMember accessor = property.getAccessor(); if (accessor != null) { - writeValue(property.getName(), accessor.getValue(wrapper.getEntity()), generator); + Object value = accessor.getValue(wrapper.getEntity()); + generator.writeFieldName(property.getName()); + if (value == null) { + generator.writeNull(); + } else { + serializers.findValueSerializer(value.getClass()).serialize(value, generator, serializers); + } } } writeLinks(wrapper.getEntity(), generator); generator.writeEndObject(); } - private void writeValue(String name, Object value, JsonGenerator generator) throws IOException { - generator.writeFieldName(name); - if (value == null) { - generator.writeNull(); - } else { - generator.writeObject(value); - } - } - private void writeLinks(Object entity, JsonGenerator generator) throws IOException { Map links = linksExtractor.getLinks(entity); generator.writeFieldName("_links"); diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/ResourceLinksProvider.java b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/ResourceLinksProvider.java index 7712a422385feb..fcc202f939842f 100644 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/ResourceLinksProvider.java +++ b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/ResourceLinksProvider.java @@ -1,41 +1,51 @@ package io.quarkus.rest.data.panache.runtime.resource; +import java.util.Collection; import java.util.HashMap; import java.util.Map; -import org.jboss.resteasy.links.LinksProvider; -import org.jboss.resteasy.links.RESTServiceDiscovery; +import javax.ws.rs.core.Link; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.resteasy.reactive.links.RestLinksProvider; public final class ResourceLinksProvider { private static final String SELF_REF = "self"; public Map getClassLinks(Class className) { - RESTServiceDiscovery links = LinksProvider - .getClassLinksProvider() - .getLinks(className, Thread.currentThread().getContextClassLoader()); - return linksToMap(links); + return linksToMap(restLinksProvider().getTypeLinks(className)); } public Map getInstanceLinks(Object instance) { - RESTServiceDiscovery links = LinksProvider - .getObjectLinksProvider() - .getLinks(instance, Thread.currentThread().getContextClassLoader()); - return linksToMap(links); + return linksToMap(restLinksProvider().getInstanceLinks(instance)); } public String getSelfLink(Object instance) { - RESTServiceDiscovery.AtomLink link = LinksProvider.getObjectLinksProvider() - .getLinks(instance, Thread.currentThread().getContextClassLoader()) - .getLinkForRel(SELF_REF); - return link == null ? null : link.getHref(); + Collection links = restLinksProvider().getInstanceLinks(instance); + for (Link link : links) { + if (SELF_REF.equals(link.getRel())) { + return link.getUri().toString(); + } + } + return null; + } + + private RestLinksProvider restLinksProvider() { + InstanceHandle instance = Arc.container().instance(RestLinksProvider.class); + if (instance.isAvailable()) { + return instance.get(); + } + throw new IllegalStateException("Invalid use of '" + this.getClass().getName() + + "'. No request scope bean found for type '" + ResourceLinksProvider.class.getName() + "'"); } - private Map linksToMap(RESTServiceDiscovery serviceDiscovery) { - Map links = new HashMap<>(serviceDiscovery.size()); - for (RESTServiceDiscovery.AtomLink atomLink : serviceDiscovery) { - links.put(atomLink.getRel(), atomLink.getHref()); + private Map linksToMap(Collection links) { + Map result = new HashMap<>(); + for (Link link : links) { + result.put(link.getRel(), link.getUri().toString()); } - return links; + return result; } } diff --git a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/AbstractSerializersTest.java b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/AbstractSerializersTest.java index 59ab3f35e74cde..88ae8efea84d26 100644 --- a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/AbstractSerializersTest.java +++ b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/AbstractSerializersTest.java @@ -17,7 +17,9 @@ abstract class AbstractSerializersTest { @Test void shouldSerializeOneBook() { - Book book = new Book(1, "Black Swan"); + int id = 1; + String title = "Black Swan"; + Book book = usePublishedBook() ? new PublishedBook(id, title) : new Book(id, title); JsonReader jsonReader = Json.createReader(new StringReader(toJson(new HalEntityWrapper(book)))); assertBook(book, jsonReader.readObject()); @@ -25,7 +27,9 @@ void shouldSerializeOneBook() { @Test void shouldSerializeOneBookWithNullName() { - Book book = new Book(1, null); + int id = 1; + String title = null; + Book book = usePublishedBook() ? new PublishedBook(id, title) : new Book(id, title); JsonReader jsonReader = Json.createReader(new StringReader(toJson(new HalEntityWrapper(book)))); assertBook(book, jsonReader.readObject()); @@ -33,7 +37,9 @@ void shouldSerializeOneBookWithNullName() { @Test void shouldSerializeCollectionOfBooks() { - Book book = new Book(1, "Black Swan"); + int id = 1; + String title = "Black Swan"; + Book book = usePublishedBook() ? new PublishedBook(id, title) : new Book(id, title); HalCollectionWrapper wrapper = new HalCollectionWrapper(Collections.singleton(book), Book.class, "books"); JsonReader jsonReader = Json.createReader(new StringReader(toJson(wrapper))); JsonObject collectionJson = jsonReader.readObject(); @@ -60,4 +66,6 @@ private void assertBook(Book book, JsonObject bookJson) { assertThat(bookLinksJson.getJsonObject("list").getString("href")).isEqualTo("/books"); assertThat(bookLinksJson.getJsonObject("add").getString("href")).isEqualTo("/books"); } + + protected abstract boolean usePublishedBook(); } diff --git a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JacksonSerializersTest.java b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JacksonSerializersTest.java index cdab327d344746..2b444463d25a5f 100644 --- a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JacksonSerializersTest.java +++ b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JacksonSerializersTest.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; class JacksonSerializersTest extends AbstractSerializersTest { @@ -18,6 +19,7 @@ void setup() { module.addSerializer(HalCollectionWrapper.class, new HalCollectionWrapperJacksonSerializer(new BookHalLinksProvider())); objectMapper.registerModule(module); + objectMapper.registerModule(new JavaTimeModule()); } @Override @@ -28,4 +30,9 @@ String toJson(Object object) { throw new RuntimeException(e); } } + + @Override + protected boolean usePublishedBook() { + return true; + } } diff --git a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JsonbSerializersTest.java b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JsonbSerializersTest.java index ae87e78a65e0cc..5884f3b5894c73 100644 --- a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JsonbSerializersTest.java +++ b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JsonbSerializersTest.java @@ -22,4 +22,9 @@ void setup() { String toJson(Object object) { return jsonb.toJson(object); } -} \ No newline at end of file + + @Override + protected boolean usePublishedBook() { + return false; + } +} diff --git a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/PublishedBook.java b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/PublishedBook.java new file mode 100644 index 00000000000000..38ff69b5463d1c --- /dev/null +++ b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/PublishedBook.java @@ -0,0 +1,13 @@ +package io.quarkus.rest.data.panache.runtime.hal; + +import java.time.LocalDate; +import java.time.Month; + +public class PublishedBook extends Book { + + public LocalDate publicationDate = LocalDate.of(2021, Month.AUGUST, 31); + + public PublishedBook(long id, String name) { + super(id, name); + } +} diff --git a/extensions/picocli/runtime/src/main/java/io/quarkus/picocli/runtime/PicocliRunner.java b/extensions/picocli/runtime/src/main/java/io/quarkus/picocli/runtime/PicocliRunner.java index 79b95041a3a7b3..33393a9dd5bceb 100644 --- a/extensions/picocli/runtime/src/main/java/io/quarkus/picocli/runtime/PicocliRunner.java +++ b/extensions/picocli/runtime/src/main/java/io/quarkus/picocli/runtime/PicocliRunner.java @@ -36,6 +36,11 @@ public PicocliRunner(CommandLine commandLine, Event par @Override public int run(String... args) throws Exception { - return commandLine.execute(args); + try { + return commandLine.execute(args); + } finally { + commandLine.getOut().flush(); + commandLine.getErr().flush(); + } } } diff --git a/extensions/pom.xml b/extensions/pom.xml index 03e4923b78cb8f..d20956728e9eac 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -175,6 +175,7 @@ flyway liquibase + liquibase-mongodb vault diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/DevServicesRedisProcessor.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/DevServicesRedisProcessor.java index f0edb1dc239344..e1d0cda173e61c 100644 --- a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/DevServicesRedisProcessor.java +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/client/deployment/DevServicesRedisProcessor.java @@ -28,7 +28,10 @@ import io.quarkus.deployment.builditem.DevServicesConfigResultBuildItem; import io.quarkus.deployment.builditem.DevServicesSharedNetworkBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.console.ConsoleInstalledBuildItem; +import io.quarkus.deployment.console.StartupLogCompressor; import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.deployment.logging.LoggingSetupBuildItem; import io.quarkus.devservices.common.ContainerLocator; import io.quarkus.redis.client.deployment.RedisBuildTimeConfig.DevServiceConfiguration; import io.quarkus.redis.client.runtime.RedisClientUtil; @@ -60,7 +63,9 @@ public class DevServicesRedisProcessor { @BuildStep(onlyIfNot = IsNormal.class, onlyIf = { GlobalDevServicesConfig.Enabled.class }) public void startRedisContainers(LaunchModeBuildItem launchMode, Optional devServicesSharedNetworkBuildItem, - BuildProducer devConfigProducer, RedisBuildTimeConfig config) { + BuildProducer devConfigProducer, RedisBuildTimeConfig config, + Optional consoleInstalledBuildItem, + LoggingSetupBuildItem loggingSetupBuildItem) { Map currentDevServicesConfiguration = new HashMap<>(config.additionalDevServices); currentDevServicesConfiguration.put(RedisClientUtil.DEFAULT_CLIENT, config.defaultDevService); @@ -85,17 +90,28 @@ public void startRedisContainers(LaunchModeBuildItem launchMode, capturedDevServicesConfiguration = currentDevServicesConfiguration; List currentCloseables = new ArrayList<>(); - for (Entry entry : currentDevServicesConfiguration.entrySet()) { - String connectionName = entry.getKey(); - StartResult startResult = startContainer(connectionName, entry.getValue().devservices, launchMode.getLaunchMode(), - devServicesSharedNetworkBuildItem.isPresent()); - if (startResult == null) { - continue; + + StartupLogCompressor compressor = new StartupLogCompressor( + (launchMode.isTest() ? "(test) " : "") + "Redis Dev Services Starting:", consoleInstalledBuildItem, + loggingSetupBuildItem); + try { + for (Entry entry : currentDevServicesConfiguration.entrySet()) { + String connectionName = entry.getKey(); + StartResult startResult = startContainer(connectionName, entry.getValue().devservices, + launchMode.getLaunchMode(), + devServicesSharedNetworkBuildItem.isPresent()); + if (startResult == null) { + continue; + } + currentCloseables.add(startResult.closeable); + String configKey = getConfigPrefix(connectionName) + RedisConfig.HOSTS_CONFIG_NAME; + devConfigProducer.produce(new DevServicesConfigResultBuildItem(configKey, startResult.url)); + log.infof("The %s redis server is ready to accept connections on %s", connectionName, startResult.url); } - currentCloseables.add(startResult.closeable); - String configKey = getConfigPrefix(connectionName) + RedisConfig.HOSTS_CONFIG_NAME; - devConfigProducer.produce(new DevServicesConfigResultBuildItem(configKey, startResult.url)); - log.infof("The %s redis server is ready to accept connections on %s", connectionName, startResult.url); + compressor.close(); + } catch (Throwable t) { + compressor.closeAndDumpCaptured(); + throw new RuntimeException(t); } closeables = currentCloseables; diff --git a/extensions/resteasy-classic/resteasy-common/runtime/src/main/java/io/quarkus/resteasy/common/runtime/QuarkusInjectorFactory.java b/extensions/resteasy-classic/resteasy-common/runtime/src/main/java/io/quarkus/resteasy/common/runtime/QuarkusInjectorFactory.java index cc4bf24082679e..695c001f11b966 100644 --- a/extensions/resteasy-classic/resteasy-common/runtime/src/main/java/io/quarkus/resteasy/common/runtime/QuarkusInjectorFactory.java +++ b/extensions/resteasy-classic/resteasy-common/runtime/src/main/java/io/quarkus/resteasy/common/runtime/QuarkusInjectorFactory.java @@ -28,6 +28,10 @@ public class QuarkusInjectorFactory extends InjectorFactoryImpl { @SuppressWarnings("rawtypes") @Override public ConstructorInjector createConstructor(Constructor constructor, ResteasyProviderFactory providerFactory) { + if (constructor == null) { + throw new IllegalStateException( + "Unable to locate proper constructor for dynamically registered provider. Make sure the class has a no-args constructor and that it uses '@Context' for field injection if necessary."); + } log.debugf("Create constructor: %s", constructor); return new QuarkusConstructorInjector(constructor, super.createConstructor(constructor, providerFactory)); } diff --git a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java index 16b31619285581..16cf244beafc9f 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java @@ -1,5 +1,6 @@ package io.quarkus.resteasy.deployment; +import java.util.List; import java.util.Optional; import java.util.regex.Pattern; @@ -18,6 +19,7 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; +import io.quarkus.resteasy.common.spi.ResteasyDotNames; import io.quarkus.resteasy.runtime.QuarkusRestPathTemplate; import io.quarkus.resteasy.runtime.QuarkusRestPathTemplateInterceptor; import io.quarkus.resteasy.server.common.spi.ResteasyJaxrsConfigBuildItem; @@ -81,14 +83,22 @@ public void transform(TransformationContext ctx) { AnnotationInstance annotation = methodInfo.annotation(REST_PATH); if (annotation == null) { - return; + // Check for @Path on class and not method + if (!isRestEndpointMethod(methodInfo.annotations())) { + return; + } } // Don't create annotations for rest clients if (classInfo.classAnnotation(REGISTER_REST_CLIENT) != null) { return; } - StringBuilder stringBuilder = new StringBuilder(slashify(annotation.value().asString())); + StringBuilder stringBuilder; + if (annotation != null) { + stringBuilder = new StringBuilder(slashify(annotation.value().asString())); + } else { + stringBuilder = new StringBuilder(); + } // Look for @Path annotation on the class annotation = classInfo.classAnnotation(REST_PATH); @@ -128,6 +138,19 @@ String slashify(String path) { return '/' + path; } + boolean isRestEndpointMethod(List annotations) { + boolean isRestEndpointMethod = false; + + for (AnnotationInstance annotation : annotations) { + if (ResteasyDotNames.JAXRS_METHOD_ANNOTATIONS.contains(annotation.name())) { + isRestEndpointMethod = true; + break; + } + } + + return isRestEndpointMethod; + } + private boolean notRequired(Capabilities capabilities, Optional metricsCapability) { return capabilities.isMissing(Capability.RESTEASY) || diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index 815441cbad7502..e843d538face91 100644 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -146,6 +146,9 @@ public class JaxrsClientReactiveProcessor { String.class, Object.class); private static final MethodDescriptor MULTIVALUED_MAP_ADD = MethodDescriptor.ofMethod(MultivaluedMap.class, "add", void.class, Object.class, Object.class); + private static final MethodDescriptor PATH_GET_FILENAME = MethodDescriptor.ofMethod(Path.class, "getFileName", + Path.class); + private static final MethodDescriptor OBJECT_TO_STRING = MethodDescriptor.ofMethod(Object.class, "toString", String.class); static final DotName CONTINUATION = DotName.createSimple("kotlin.coroutines.Continuation"); private static final DotName UNI_KT = DotName.createSimple("io.smallrye.mutiny.coroutines.UniKt"); @@ -502,7 +505,11 @@ A more full example of generated client (with sub-resource) can is at the bottom ClassInfo subResourceInterface = index.getClassByName(returnType.name()); if (!Modifier.isInterface(subResourceInterface.flags())) { throw new IllegalArgumentException( - "Sub resource type is not an interface: " + returnType.name().toString()); + "Client interface method: " + jandexMethod.declaringClass().name() + "#" + jandexMethod + + " has no HTTP method annotation (@GET, @POST, etc) and it's return type: " + + returnType.name().toString() + " is not an interface. " + + "If it's a sub resource method, it has to return an interface. " + + "If it's not, it has to have one of the HTTP method annotations."); } // generate implementation for a method from the jaxrs interface: MethodCreator methodCreator = c.getMethodCreator(method.getName(), method.getSimpleReturnType(), @@ -989,7 +996,7 @@ private ResultHandle createMultipartForm(MethodCreator methodCreator, ResultHand formClass.name() + "." + field.name()); } ResultHandle filePath = methodCreator.invokeVirtualMethod( - MethodDescriptor.ofMethod(File.class, "getPath", String.class), fieldValue); + MethodDescriptor.ofMethod(File.class, "toPath", Path.class), fieldValue); addFile(methodCreator, multipartForm, formParamName, partType, filePath); } else if (is(PATH, fieldClass, index)) { // and so is path @@ -998,9 +1005,7 @@ private ResultHandle createMultipartForm(MethodCreator methodCreator, ResultHand "No @PartType annotation found on multipart form field of type Path: " + formClass.name() + "." + field.name()); } - ResultHandle filePath = methodCreator.invokeInterfaceMethod( - MethodDescriptor.ofMethod(Path.class, "toString", String.class), fieldValue); - addFile(methodCreator, multipartForm, formParamName, partType, filePath); + addFile(methodCreator, multipartForm, formParamName, partType, fieldValue); } else if (is(BUFFER, fieldClass, index)) { // and buffer addBuffer(methodCreator, multipartForm, formParamName, partType, fieldValue, field); @@ -1047,6 +1052,9 @@ private ResultHandle createMultipartForm(MethodCreator methodCreator, ResultHand */ private void addFile(MethodCreator methodCreator, AssignableResultHandle multipartForm, String formParamName, String partType, ResultHandle filePath) { + ResultHandle fileNamePath = methodCreator.invokeInterfaceMethod(PATH_GET_FILENAME, filePath); + ResultHandle fileName = methodCreator.invokeVirtualMethod(OBJECT_TO_STRING, fileNamePath); + ResultHandle pathString = methodCreator.invokeVirtualMethod(OBJECT_TO_STRING, filePath); if (partType.equalsIgnoreCase(MediaType.APPLICATION_OCTET_STREAM)) { methodCreator.assign(multipartForm, // MultipartForm#binaryFileUpload(String name, String filename, String pathname, String mediaType); @@ -1055,8 +1063,8 @@ private void addFile(MethodCreator methodCreator, AssignableResultHandle multipa MethodDescriptor.ofMethod(MultipartForm.class, "binaryFileUpload", MultipartForm.class, String.class, String.class, String.class, String.class), - multipartForm, methodCreator.load(formParamName), methodCreator.load(formParamName), - filePath, methodCreator.load(partType))); + multipartForm, methodCreator.load(formParamName), fileName, + pathString, methodCreator.load(partType))); } else { methodCreator.assign(multipartForm, // MultipartForm#textFileUpload(String name, String filename, String pathname, String mediaType);; @@ -1065,8 +1073,8 @@ private void addFile(MethodCreator methodCreator, AssignableResultHandle multipa MethodDescriptor.ofMethod(MultipartForm.class, "textFileUpload", MultipartForm.class, String.class, String.class, String.class, String.class), - multipartForm, methodCreator.load(formParamName), methodCreator.load(formParamName), - filePath, methodCreator.load(partType))); + multipartForm, methodCreator.load(formParamName), fileName, + pathString, methodCreator.load(partType))); } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/JaxRsResourceIndexBuildItem.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/JaxRsResourceIndexBuildItem.java new file mode 100644 index 00000000000000..7d69199960335c --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/JaxRsResourceIndexBuildItem.java @@ -0,0 +1,23 @@ +package io.quarkus.resteasy.reactive.common.deployment; + +import org.jboss.jandex.IndexView; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Represents the index that is going to be used to look up JAX-RS Resources. + * This index contains both code that is present on disk at build time, + * and code that is generated by other extensions. + */ +public final class JaxRsResourceIndexBuildItem extends SimpleBuildItem { + + private final IndexView indexView; + + public JaxRsResourceIndexBuildItem(IndexView indexView) { + this.indexView = indexView; + } + + public IndexView getIndexView() { + return indexView; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java index 4a8ca5145da6c7..41f0c5d41c517d 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/ResteasyReactiveCommonProcessor.java @@ -1,5 +1,7 @@ package io.quarkus.resteasy.reactive.common.deployment; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -17,8 +19,10 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.CompositeIndex; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; +import org.jboss.jandex.Indexer; import org.jboss.jandex.Type; import org.jboss.resteasy.reactive.common.jaxrs.RuntimeDelegateImpl; import org.jboss.resteasy.reactive.common.model.InterceptorContainer; @@ -36,6 +40,7 @@ import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.PreAdditionalBeanBuildTimeConditionBuildItem; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -48,6 +53,7 @@ import io.quarkus.resteasy.reactive.spi.AbstractInterceptorBuildItem; import io.quarkus.resteasy.reactive.spi.ContainerRequestFilterBuildItem; import io.quarkus.resteasy.reactive.spi.ContainerResponseFilterBuildItem; +import io.quarkus.resteasy.reactive.spi.GeneratedJaxRsResourceBuildItem; import io.quarkus.resteasy.reactive.spi.MessageBodyReaderBuildItem; import io.quarkus.resteasy.reactive.spi.MessageBodyReaderOverrideBuildItem; import io.quarkus.resteasy.reactive.spi.MessageBodyWriterBuildItem; @@ -212,14 +218,30 @@ private boolean namePresent(Set nameBindingNames, Set globalName return false; } + @BuildStep + JaxRsResourceIndexBuildItem resourceIndex(CombinedIndexBuildItem combinedIndex, + List generatedJaxRsResources, + BuildProducer generatedBeansProducer) throws IOException { + if (generatedJaxRsResources.isEmpty()) { + return new JaxRsResourceIndexBuildItem(combinedIndex.getComputingIndex()); + } + + Indexer indexer = new Indexer(); + for (GeneratedJaxRsResourceBuildItem generatedJaxRsResource : generatedJaxRsResources) { + indexer.index(new ByteArrayInputStream(generatedJaxRsResource.getData())); + generatedBeansProducer + .produce(new GeneratedBeanBuildItem(generatedJaxRsResource.getName(), generatedJaxRsResource.getData())); + } + return new JaxRsResourceIndexBuildItem(CompositeIndex.create(combinedIndex.getComputingIndex(), indexer.complete())); + } + @BuildStep void scanResources( - // TODO: We need to use this index instead of BeanArchiveIndexBuildItem to avoid build cycles. It it OK? - CombinedIndexBuildItem combinedIndexBuildItem, + JaxRsResourceIndexBuildItem jaxRsResourceIndexBuildItem, BuildProducer annotationsTransformerBuildItemBuildProducer, BuildProducer resourceScanningResultBuildItemBuildProducer) { - ResourceScanningResult res = ResteasyReactiveScanner.scanResources(combinedIndexBuildItem.getComputingIndex()); + ResourceScanningResult res = ResteasyReactiveScanner.scanResources(jaxRsResourceIndexBuildItem.getIndexView()); if (res == null) { return; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ArcBeanFactory.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ArcBeanFactory.java index 0808558c0ca57b..c44b8bb45458c4 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ArcBeanFactory.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/runtime/src/main/java/io/quarkus/resteasy/reactive/common/runtime/ArcBeanFactory.java @@ -22,17 +22,28 @@ public String toString() { @Override public BeanInstance createInstance() { - BeanContainer.Instance instance = factory.create(); - return new BeanInstance() { - @Override - public T getInstance() { - return instance.get(); + BeanContainer.Instance instance; + try { + instance = factory.create(); + return new BeanInstance() { + @Override + public T getInstance() { + return instance.get(); + } + + @Override + public void close() { + instance.close(); + } + }; + } catch (Exception e) { + if (factory.getClass().getName().contains("DefaultInstanceFactory")) { + throw new IllegalArgumentException( + "Unable to create class '" + targetClassName + + "'. To fix the problem, make sure this class is a CDI bean.", + e); } - - @Override - public void close() { - instance.close(); - } - }; + throw e; + } } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/GeneratedJaxRsResourceBuildItem.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/GeneratedJaxRsResourceBuildItem.java new file mode 100644 index 00000000000000..3c8bb89853e846 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/GeneratedJaxRsResourceBuildItem.java @@ -0,0 +1,26 @@ +package io.quarkus.resteasy.reactive.spi; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Represents a JAX-RS resource that is generated. + * Meant to be used by extension that generate JAX-RS resources as part of their build time processing + */ +public final class GeneratedJaxRsResourceBuildItem extends MultiBuildItem { + + private final String name; + private final byte[] data; + + public GeneratedJaxRsResourceBuildItem(String name, byte[] data) { + this.name = name; + this.data = data; + } + + public String getName() { + return name; + } + + public byte[] getData() { + return data; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/pom.xml index 4810c69ea7cd47..bf22e85e0524c2 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/pom.xml +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/pom.xml @@ -29,6 +29,12 @@ io.quarkus quarkus-bootstrap-maven-plugin + + + io.quarkus.rest.jackson + io.quarkus.resteasy.reactive.json.jackson + + maven-compiler-plugin diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/runtime/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/runtime/pom.xml index fe85ce1a97913e..0032cb13da00d9 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/runtime/pom.xml +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/runtime/pom.xml @@ -29,6 +29,12 @@ io.quarkus quarkus-bootstrap-maven-plugin + + + io.quarkus.rest.jsonb + io.quarkus.resteasy.reactive.json.jsonb + + maven-compiler-plugin diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/DotNames.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/DotNames.java new file mode 100644 index 00000000000000..4a493e9584a468 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/DotNames.java @@ -0,0 +1,15 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import org.jboss.jandex.DotName; + +import io.quarkus.resteasy.reactive.links.InjectRestLinks; +import io.quarkus.resteasy.reactive.links.RestLink; + +final class DotNames { + + static final DotName INJECT_REST_LINKS_ANNOTATION = DotName.createSimple(InjectRestLinks.class.getName()); + static final DotName REST_LINK_ANNOTATION = DotName.createSimple(RestLink.class.getName()); + + private DotNames() { + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java index 12c0c3eb196e2e..6e9142867e5689 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java @@ -24,8 +24,6 @@ final class LinksContainerFactory { - private static final DotName REST_LINK_ANNOTATION = DotName.createSimple(RestLink.class.getName()); - private final IndexView index; LinksContainerFactory(IndexView index) { @@ -41,7 +39,7 @@ LinksContainer getLinksContainer(List resourceClasses) { for (ResourceClass resourceClass : resourceClasses) { for (ResourceMethod resourceMethod : resourceClass.getMethods()) { MethodInfo resourceMethodInfo = getResourceMethodInfo(resourceClass, resourceMethod); - AnnotationInstance restLinkAnnotation = resourceMethodInfo.annotation(REST_LINK_ANNOTATION); + AnnotationInstance restLinkAnnotation = resourceMethodInfo.annotation(DotNames.REST_LINK_ANNOTATION); if (restLinkAnnotation != null) { LinkInfo linkInfo = getLinkInfo(resourceClass, resourceMethod, resourceMethodInfo, restLinkAnnotation); @@ -120,7 +118,7 @@ private boolean isSameMethod(MethodInfo resourceMethodInfo, ResourceMethod resou List parameterTypes = resourceMethodInfo.parameters(); MethodParameter[] parameters = resourceMethod.getParameters(); for (int i = 0; i < parameters.length; i++) { - if (!parameterTypes.get(i).name().equals(DotName.createSimple(parameters[i].type))) { + if (!parameterTypes.get(i).name().equals(DotName.createSimple(parameters[i].declaredType))) { return false; } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksMethodScanner.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksMethodScanner.java new file mode 100644 index 00000000000000..59d54071640ad2 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksMethodScanner.java @@ -0,0 +1,59 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.MethodInfo; +import org.jboss.resteasy.reactive.server.model.FixedHandlerChainCustomizer; +import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; +import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner; + +import io.quarkus.resteasy.reactive.links.RestLinkType; +import io.quarkus.resteasy.reactive.links.RestLinksHandler; + +public class LinksMethodScanner implements MethodScanner { + + @Override + public List scan(MethodInfo method, ClassInfo actualEndpointClass, + Map methodContext) { + AnnotationInstance injectRestLinksInstance = getInjectRestLinksAnnotation(method, actualEndpointClass); + if (injectRestLinksInstance == null) { + return Collections.emptyList(); + } + + RestLinkType restLinkType = RestLinkType.TYPE; + AnnotationValue injectRestLinksValue = injectRestLinksInstance.value(); + if (injectRestLinksValue != null) { + restLinkType = RestLinkType.valueOf(injectRestLinksValue.asEnum()); + } + + AnnotationInstance restLinkInstance = method.annotation(DotNames.REST_LINK_ANNOTATION); + String entityType = null; + if (restLinkInstance != null) { + AnnotationValue restInstanceValue = restLinkInstance.value("entityType"); + if (restInstanceValue != null) { + entityType = restInstanceValue.asClass().name().toString(); + } + } + + RestLinksHandler handler = new RestLinksHandler(); + handler.setRestLinkData(new RestLinksHandler.RestLinkData(restLinkType, entityType)); + return Collections.singletonList(new FixedHandlerChainCustomizer(handler, + HandlerChainCustomizer.Phase.AFTER_RESPONSE_CREATED)); + } + + private AnnotationInstance getInjectRestLinksAnnotation(MethodInfo method, ClassInfo actualEndpointClass) { + AnnotationInstance annotationInstance = method.annotation(DotNames.INJECT_REST_LINKS_ANNOTATION); + if (annotationInstance == null) { + annotationInstance = method.declaringClass().classAnnotation(DotNames.INJECT_REST_LINKS_ANNOTATION); + if ((annotationInstance == null) && !actualEndpointClass.equals(method.declaringClass())) { + annotationInstance = actualEndpointClass.classAnnotation(DotNames.INJECT_REST_LINKS_ANNOTATION); + } + } + return annotationInstance; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java index 3f8983032c57e2..380e9086b5598e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java @@ -18,11 +18,10 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; -import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.gizmo.ClassOutput; -import io.quarkus.resteasy.reactive.links.RestLinksResponseFilter; +import io.quarkus.resteasy.reactive.common.deployment.JaxRsResourceIndexBuildItem; import io.quarkus.resteasy.reactive.links.runtime.GetterAccessorsContainer; import io.quarkus.resteasy.reactive.links.runtime.GetterAccessorsContainerRecorder; import io.quarkus.resteasy.reactive.links.runtime.LinkInfo; @@ -30,7 +29,7 @@ import io.quarkus.resteasy.reactive.links.runtime.LinksProviderRecorder; import io.quarkus.resteasy.reactive.links.runtime.RestLinksProviderProducer; import io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveDeploymentInfoBuildItem; -import io.quarkus.resteasy.reactive.spi.CustomContainerResponseFilterBuildItem; +import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; import io.quarkus.runtime.RuntimeValue; final class LinksProcessor { @@ -42,15 +41,20 @@ void feature(BuildProducer feature) { feature.produce(new FeatureBuildItem(Feature.RESTEASY_REACTIVE_LINKS)); } + @BuildStep + MethodScannerBuildItem linksSupport() { + return new MethodScannerBuildItem(new LinksMethodScanner()); + } + @BuildStep @Record(STATIC_INIT) - void initializeLinksProvider(CombinedIndexBuildItem indexBuildItem, + void initializeLinksProvider(JaxRsResourceIndexBuildItem indexBuildItem, ResteasyReactiveDeploymentInfoBuildItem deploymentInfoBuildItem, BuildProducer bytecodeTransformersProducer, BuildProducer generatedClassesProducer, GetterAccessorsContainerRecorder getterAccessorsContainerRecorder, LinksProviderRecorder linksProviderRecorder) { - IndexView index = indexBuildItem.getIndex(); + IndexView index = indexBuildItem.getIndexView(); ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClassesProducer, true); // Initialize links container @@ -68,11 +72,6 @@ AdditionalBeanBuildItem registerRestLinksProviderProducer() { return AdditionalBeanBuildItem.unremovableOf(RestLinksProviderProducer.class); } - @BuildStep - CustomContainerResponseFilterBuildItem registerRestLinksResponseFilter() { - return new CustomContainerResponseFilterBuildItem(RestLinksResponseFilter.class.getName()); - } - private LinksContainer getLinksContainer(IndexView index, ResteasyReactiveDeploymentInfoBuildItem deploymentInfoBuildItem) { LinksContainerFactory linksContainerFactory = new LinksContainerFactory(index); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksHandler.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksHandler.java new file mode 100644 index 00000000000000..10c2e24571c8e3 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksHandler.java @@ -0,0 +1,82 @@ +package io.quarkus.resteasy.reactive.links; + +import java.util.Collection; + +import javax.ws.rs.core.Link; +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; + +import io.quarkus.arc.Arc; + +public class RestLinksHandler implements ServerRestHandler { + + private RestLinkData restLinkData; + + public RestLinkData getRestLinkData() { + return restLinkData; + } + + public void setRestLinkData(RestLinkData restLinkData) { + this.restLinkData = restLinkData; + } + + @Override + public void handle(ResteasyReactiveRequestContext context) { + Response response = context.getResponse().get(); + for (Link link : getLinks(response)) { + response.getHeaders().add("Link", link); + } + } + + private Collection getLinks(Response response) { + if ((restLinkData.getRestLinkType() == RestLinkType.INSTANCE) && response.hasEntity()) { + return getTestLinksProvider().getInstanceLinks(response.getEntity()); + } + return getTestLinksProvider() + .getTypeLinks(restLinkData.getEntityType() != null ? entityTypeClass() : response.getEntity().getClass()); + } + + private Class entityTypeClass() { + try { + return Thread.currentThread().getContextClassLoader().loadClass(restLinkData.getEntityType()); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Unable load class '" + restLinkData.getEntityType() + "'", e); + } + } + + private RestLinksProvider getTestLinksProvider() { + return Arc.container().instance(RestLinksProvider.class).get(); + } + + public static class RestLinkData { + + public RestLinkData(RestLinkType restLinkType, String entityType) { + this.restLinkType = restLinkType; + this.entityType = entityType; + } + + public RestLinkData() { + } + + private RestLinkType restLinkType; + private String entityType; + + public RestLinkType getRestLinkType() { + return restLinkType; + } + + public void setRestLinkType(RestLinkType restLinkType) { + this.restLinkType = restLinkType; + } + + public String getEntityType() { + return entityType; + } + + public void setEntityType(String entityType) { + this.entityType = entityType; + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksResponseFilter.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksResponseFilter.java deleted file mode 100644 index 09b275d0d7a15a..00000000000000 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksResponseFilter.java +++ /dev/null @@ -1,76 +0,0 @@ -package io.quarkus.resteasy.reactive.links; - -import java.lang.annotation.Annotation; -import java.util.Collection; -import java.util.Collections; - -import javax.ws.rs.container.ContainerResponseContext; -import javax.ws.rs.container.ResourceInfo; -import javax.ws.rs.core.Link; - -import org.jboss.resteasy.reactive.server.ServerResponseFilter; -import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; - -public class RestLinksResponseFilter { - - private final RestLinksProvider linksProvider; - - public RestLinksResponseFilter(RestLinksProvider linksProvider) { - this.linksProvider = linksProvider; - } - - @ServerResponseFilter - public void filter(ResourceInfo resourceInfo, ContainerResponseContext responseContext) { - if (!(resourceInfo instanceof ResteasyReactiveResourceInfo)) { - return; - } - for (Link link : getLinks((ResteasyReactiveResourceInfo) resourceInfo, responseContext)) { - responseContext.getHeaders().add("Link", link); - } - } - - private Collection getLinks(ResteasyReactiveResourceInfo resourceInfo, - ContainerResponseContext responseContext) { - InjectRestLinks injectRestLinksAnnotation = getInjectRestLinksAnnotation(resourceInfo); - if (injectRestLinksAnnotation == null) { - return Collections.emptyList(); - } - - if (injectRestLinksAnnotation.value() == RestLinkType.INSTANCE && responseContext.hasEntity()) { - return linksProvider.getInstanceLinks(responseContext.getEntity()); - } - - return linksProvider.getTypeLinks(getEntityType(resourceInfo, responseContext)); - } - - private InjectRestLinks getInjectRestLinksAnnotation(ResteasyReactiveResourceInfo resourceInfo) { - if (resourceInfo.getMethodAnnotationNames().contains(InjectRestLinks.class.getName())) { - for (Annotation annotation : resourceInfo.getAnnotations()) { - if (annotation instanceof InjectRestLinks) { - return (InjectRestLinks) annotation; - } - } - } - if (resourceInfo.getClassAnnotationNames().contains(InjectRestLinks.class.getName())) { - for (Annotation annotation : resourceInfo.getClassAnnotations()) { - if (annotation instanceof InjectRestLinks) { - return (InjectRestLinks) annotation; - } - } - } - return null; - } - - private Class getEntityType(ResteasyReactiveResourceInfo resourceInfo, - ContainerResponseContext responseContext) { - for (Annotation annotation : resourceInfo.getAnnotations()) { - if (annotation instanceof RestLink) { - Class entityType = ((RestLink) annotation).entityType(); - if (entityType != Object.class) { - return entityType; - } - } - } - return responseContext.getEntityClass(); - } -} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java index 7a0354d4a6d74e..8960e382906d8d 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedList; import java.util.List; import javax.ws.rs.core.Link; @@ -34,8 +33,9 @@ static void setGetterAccessorsContainer(GetterAccessorsContainer getterAccessors public Collection getTypeLinks(Class elementType) { verifyInit(); - List links = new LinkedList<>(); - for (LinkInfo linkInfo : linksContainer.getForClass(elementType)) { + List linkInfoList = linksContainer.getForClass(elementType); + List links = new ArrayList<>(linkInfoList.size()); + for (LinkInfo linkInfo : linkInfoList) { if (linkInfo.getPathParameters().size() == 0) { links.add(Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(linkInfo.getPath())) .rel(linkInfo.getRel()) @@ -49,8 +49,9 @@ public Collection getTypeLinks(Class elementType) { public Collection getInstanceLinks(T instance) { verifyInit(); - List links = new LinkedList<>(); - for (LinkInfo linkInfo : linksContainer.getForClass(instance.getClass())) { + List linkInfoList = linksContainer.getForClass(instance.getClass()); + List links = new ArrayList<>(linkInfoList.size()); + for (LinkInfo linkInfo : linkInfoList) { links.add(Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(linkInfo.getPath())) .rel(linkInfo.getRel()) .build(getPathParameterValues(linkInfo, instance))); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomFilterGenerator.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomFilterGenerator.java index b45231b36da205..73f6888dfd6938 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomFilterGenerator.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomFilterGenerator.java @@ -31,6 +31,7 @@ import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.container.ResourceInfo; import javax.ws.rs.core.Response; import org.jboss.jandex.DotName; @@ -410,7 +411,7 @@ private static ResultHandle getRRReqCtxHandle(MethodCreator filter, ResultHandle private static AssignableResultHandle getResourceInfoHandle(MethodCreator filterMethod, ResultHandle rrReqCtxHandle) { ResultHandle runtimeResourceHandle = GeneratorUtils.runtimeResourceHandle(filterMethod, rrReqCtxHandle); - AssignableResultHandle resourceInfo = filterMethod.createVariable(ResteasyReactiveResourceInfo.class); + AssignableResultHandle resourceInfo = filterMethod.createVariable(ResourceInfo.class); BranchResult ifNullBranch = filterMethod.ifNull(runtimeResourceHandle); ifNullBranch.trueBranch().assign(resourceInfo, ifNullBranch.trueBranch().readStaticField( FieldDescriptor.of(SimpleResourceInfo.NullValues.class, "INSTANCE", SimpleResourceInfo.NullValues.class))); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/GeneratedJaxRsResourceGizmoAdaptor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/GeneratedJaxRsResourceGizmoAdaptor.java new file mode 100644 index 00000000000000..331150eeb0b841 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/GeneratedJaxRsResourceGizmoAdaptor.java @@ -0,0 +1,20 @@ +package io.quarkus.resteasy.reactive.server.deployment; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.resteasy.reactive.spi.GeneratedJaxRsResourceBuildItem; + +public class GeneratedJaxRsResourceGizmoAdaptor implements ClassOutput { + + private final BuildProducer classOutput; + + public GeneratedJaxRsResourceGizmoAdaptor(BuildProducer classOutput) { + this.classOutput = classOutput; + } + + @Override + public void write(String className, byte[] bytes) { + classOutput.produce(new GeneratedJaxRsResourceBuildItem(className, bytes)); + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/MultipartPopulatorGenerator.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/MultipartPopulatorGenerator.java index f05f9dd80b16bf..b9353e698a05e9 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/MultipartPopulatorGenerator.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/MultipartPopulatorGenerator.java @@ -189,7 +189,8 @@ private MultipartPopulatorGenerator() { */ static String generate(ClassInfo multipartClassInfo, ClassOutput classOutput, IndexView index) { if (!multipartClassInfo.hasNoArgsConstructor()) { - throw new IllegalArgumentException("Classes annotated with '@MultipartForm' must contain a no-args constructor"); + throw new IllegalArgumentException("Classes annotated with '@MultipartForm' must contain a no-args constructor. " + + "The constructor is missing on " + multipartClassInfo.name()); } String multipartClassName = multipartClassInfo.name().toString(); @@ -394,11 +395,11 @@ static String generate(ClassInfo multipartClassInfo, ClassOutput classOutput, In String.class, Class.class, java.lang.reflect.Type.class, MediaType.class, - ResteasyReactiveRequestContext.class), + ResteasyReactiveRequestContext.class, String.class), formStrValueHandle, populate.readStaticField(typeField), populate.readStaticField(genericTypeField), populate.readStaticField(mediaTypeField), - rrCtxHandle)); + rrCtxHandle, formAttrNameHandle)); } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index d9b37a93faab06..62fac03831ff8b 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -10,11 +10,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; -import java.util.stream.Collectors; import java.util.stream.Stream; import javax.ws.rs.Priorities; @@ -41,13 +41,13 @@ import org.jboss.resteasy.reactive.common.model.ResourceDynamicFeature; import org.jboss.resteasy.reactive.common.model.ResourceFeature; import org.jboss.resteasy.reactive.common.model.ResourceInterceptors; -import org.jboss.resteasy.reactive.common.model.ResourceMethod; import org.jboss.resteasy.reactive.common.model.ResourceReader; import org.jboss.resteasy.reactive.common.model.ResourceWriter; import org.jboss.resteasy.reactive.common.processor.AdditionalReaderWriter; import org.jboss.resteasy.reactive.common.processor.AdditionalReaders; import org.jboss.resteasy.reactive.common.processor.AdditionalWriters; import org.jboss.resteasy.reactive.common.processor.DefaultProducesHandler; +import org.jboss.resteasy.reactive.common.processor.EndpointIndexer; import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; import org.jboss.resteasy.reactive.common.processor.scanning.ApplicationScanningResult; import org.jboss.resteasy.reactive.common.processor.scanning.ResourceScanningResult; @@ -335,7 +335,7 @@ public void setupEndpoints(Capabilities capabilities, BeanArchiveIndexBuildItem QuarkusServerEndpointIndexer.Builder serverEndpointIndexerBuilder = new QuarkusServerEndpointIndexer.Builder() .addMethodScanners( - methodScanners.stream().map(MethodScannerBuildItem::getMethodScanner).collect(Collectors.toList())) + methodScanners.stream().map(MethodScannerBuildItem::getMethodScanner).collect(toList())) .setIndex(index) .setFactoryCreator(new QuarkusFactoryCreator(recorder, beanContainerBuildItem.getValue())) .setEndpointInvokerFactory(new QuarkusInvokerFactory(generatedClassBuildItemBuildProducer, recorder)) @@ -353,13 +353,20 @@ public void setupEndpoints(Capabilities capabilities, BeanArchiveIndexBuildItem .setClassLevelExceptionMappers( classLevelExceptionMappers.isPresent() ? classLevelExceptionMappers.get().getMappers() : Collections.emptyMap()) - .setResourceMethodCallback(new Consumer>() { + .setResourceMethodCallback(new Consumer<>() { @Override - public void accept(Map.Entry entry) { - MethodInfo method = entry.getKey(); + public void accept(EndpointIndexer.ResourceMethodCallbackData entry) { + MethodInfo method = entry.getMethodInfo(); String source = ResteasyReactiveProcessor.class.getSimpleName() + " > " + method.declaringClass() + "[" + method + "]"; + ClassInfo classInfoWithSecurity = consumeStandardSecurityAnnotations(method, + entry.getActualEndpointInfo(), index, c -> c); + if (classInfoWithSecurity != null) { + reflectiveClass.produce(new ReflectiveClassBuildItem(false, true, false, + entry.getActualEndpointInfo().name().toString())); + } + reflectiveHierarchy.produce(new ReflectiveHierarchyBuildItem.Builder() .type(method.returnType()) .index(index) @@ -648,21 +655,29 @@ MethodScannerBuildItem integrateEagerSecurity(Capabilities capabilities, Combine @Override public List scan(MethodInfo method, ClassInfo actualEndpointClass, Map methodContext) { - if (SecurityTransformerUtils.hasStandardSecurityAnnotation(method)) { - return Collections.singletonList(new EagerSecurityHandler.Customizer()); - } - ClassInfo c = actualEndpointClass; - while (c.superName() != null) { - if (SecurityTransformerUtils.hasStandardSecurityAnnotation(c)) { - return Collections.singletonList(new EagerSecurityHandler.Customizer()); - } - c = index.getClassByName(c.superName()); - } - return Collections.emptyList(); + return Objects.requireNonNullElse( + consumeStandardSecurityAnnotations(method, actualEndpointClass, index, + (c) -> Collections.singletonList(new EagerSecurityHandler.Customizer())), + Collections.emptyList()); } }); } + private T consumeStandardSecurityAnnotations(MethodInfo methodInfo, ClassInfo classInfo, IndexView index, + Function function) { + if (SecurityTransformerUtils.hasStandardSecurityAnnotation(methodInfo)) { + return function.apply(methodInfo.declaringClass()); + } + ClassInfo c = classInfo; + while (c.superName() != null) { + if (SecurityTransformerUtils.hasStandardSecurityAnnotation(c)) { + return function.apply(c); + } + c = index.getClassByName(c.superName()); + } + return null; + } + private Optional getAppPath(Optional newPropertyValue) { Optional legacyProperty = ConfigProvider.getConfig().getOptionalValue("quarkus.rest.path", String.class); if (legacyProperty.isPresent()) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/InnerClassTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/InnerClassTest.java new file mode 100644 index 00000000000000..1513476f85ecff --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/InnerClassTest.java @@ -0,0 +1,44 @@ +package io.quarkus.resteasy.reactive.server.test; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.function.Supplier; + +import javax.enterprise.inject.spi.DeploymentException; +import javax.ws.rs.Path; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.common.annotation.Blocking; + +public class InnerClassTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(Resource.class); + } + }).setExpectedException(DeploymentException.class); + + @Test + public void test() { + fail("Should never have been called"); + } + + @Path("test") + public class Resource { + + @Path("hello") + @Blocking + public String hello() { + return "hello"; + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/cache/NoCacheOnMethodsTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/cache/NoCacheOnMethodsTest.java index 00662d7c38287d..1925a3855d8522 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/cache/NoCacheOnMethodsTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/cache/NoCacheOnMethodsTest.java @@ -30,37 +30,53 @@ public JavaArchive get() { }); @Test - public void testWith() { - RestAssured.get("/test/with") + public void testWithFields() { + RestAssured.get("/test/withFields") .then() .statusCode(200) - .body(equalTo("with")) + .body(equalTo("withFields")) .header("Cache-Control", "no-cache=\"f1\", no-cache=\"f2\""); } @Test - public void testWithout() { - RestAssured.get("/test/without") + public void testWithoutFields() { + RestAssured.get("/test/withoutFields") .then() .statusCode(200) - .body(equalTo("without")) + .body(equalTo("withoutFields")) + .header("Cache-Control", "no-cache"); + } + + @Test + public void testWithoutAnnotation() { + RestAssured.get("/test/withoutAnnotation") + .then() + .statusCode(200) + .body(equalTo("withoutAnnotation")) .header("Cache-Control", nullValue()); } @Path("test") public static class ResourceWithNoCache { - @Path("with") + @Path("withFields") @GET @NoCache(fields = { "f1", "f2" }) - public String with() { - return "with"; + public String withFields() { + return "withFields"; + } + + @Path("withoutFields") + @GET + @NoCache + public String withoutFields() { + return "withoutFields"; } - @Path("without") + @Path("withoutAnnotation") @GET - public String without() { - return "without"; + public String withoutAnnotation() { + return "withoutAnnotation"; } } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/NoTargetTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/NoTargetTest.java new file mode 100644 index 00000000000000..4d2bb9a21e58dd --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customproviders/NoTargetTest.java @@ -0,0 +1,90 @@ +package io.quarkus.resteasy.reactive.server.test.customproviders; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.util.function.Supplier; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.PreMatching; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import org.jboss.resteasy.reactive.server.ServerResponseFilter; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.http.Headers; + +public class NoTargetTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(HelloResource.class, ThrowingPreMatchFilter.class, DummyExceptionMapper.class); + } + }); + + @Path("hello") + public static class HelloResource { + + @GET + public String hello() { + return "hello"; + } + } + + @Test + public void test() { + Headers headers = RestAssured.get("/hello") + .then().statusCode(200).extract().headers(); + assertEquals("mapper", headers.get("source").getValue()); + assertEquals("NullValues", headers.get("resourceInfoClass").getValue()); + } + + public static class CustomResponseFilter { + + @ServerResponseFilter + public void filter(ContainerResponseContext responseContext, ResourceInfo resourceInfo) { + responseContext.getHeaders().add("resourceInfoClass", resourceInfo.getClass().getSimpleName()); + } + } + + @PreMatching + @Provider + public static class ThrowingPreMatchFilter implements ContainerRequestFilter { + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + throw new DummyException(); + } + } + + @Provider + public static class DummyExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(DummyException exception) { + return Response.ok().header("source", "mapper").build(); + } + } + + public static class DummyException extends RuntimeException { + public DummyException() { + super("dummy"); + setStackTrace(new StackTraceElement[0]); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/generatedresource/GeneratedJaxRsResourceTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/generatedresource/GeneratedJaxRsResourceTest.java new file mode 100644 index 00000000000000..a5c97b442f7120 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/generatedresource/GeneratedJaxRsResourceTest.java @@ -0,0 +1,82 @@ +package io.quarkus.resteasy.reactive.server.test.generatedresource; + +import static io.restassured.RestAssured.when; + +import java.util.function.Consumer; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.resteasy.reactive.server.deployment.GeneratedJaxRsResourceGizmoAdaptor; +import io.quarkus.resteasy.reactive.spi.GeneratedJaxRsResourceBuildItem; +import io.quarkus.test.QuarkusUnitTest; + +public class GeneratedJaxRsResourceTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(HelloResource.class)) + .addBuildChainCustomizer(buildCustomizer()); + + @Test + public void testRestPath() { + when().get("/hello").then().statusCode(200).body(Matchers.is("hello")); + when().get("/test").then().statusCode(200).body(Matchers.is("test")); + } + + protected static Consumer buildCustomizer() { + return new Consumer<>() { + /** + * This represents the extension that generates a JAX-RS resource like: + * + *

+             * {@code
+             *      @Path("/test')
+             *      public class TestResource {
+             *
+             *          @GET
+             *          public String test() {
+             *              return "test";
+             *          }
+             *      }
+             * }
+             * 
+ */ + @Override + public void accept(BuildChainBuilder builder) { + builder.addBuildStep(context -> { + BuildProducer producer = context::produce; + ClassOutput classOutput = new GeneratedJaxRsResourceGizmoAdaptor(producer); + try (ClassCreator classCreator = ClassCreator.builder() + .classOutput(classOutput).className("com.example.TestResource") + .build()) { + classCreator.addAnnotation(Path.class).addValue("value", "test"); + MethodCreator methodCreator = classCreator.getMethodCreator("test", String.class); + methodCreator.addAnnotation(GET.class); + methodCreator.returnValue(methodCreator.load("test")); + } + }).produces(GeneratedJaxRsResourceBuildItem.class).build(); + } + }; + } + + @Path("hello") + public static class HelloResource { + + @GET + public String hello() { + return "hello"; + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java index 5489e49172b3f6..225943678fa69e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/LargerThanDefaultFormAttributeMultipartFormInputTest.java @@ -39,7 +39,7 @@ public JavaArchive get() { return ShrinkWrap.create(JavaArchive.class) .addClasses(Resource.class, Data.class) .addAsResource(new StringAsset( - "quarkus.http.limits.max-form-attribute-size=4K"), + "quarkus.http.limits.max-form-attribute-size=120K"), "application.properties"); } }); @@ -49,6 +49,12 @@ public JavaArchive get() { @Test public void test() throws IOException { String fileContents = new String(Files.readAllBytes(FILE.toPath()), StandardCharsets.UTF_8); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10; ++i) { + sb.append(fileContents); + } + fileContents = sb.toString(); + Assertions.assertTrue(fileContents.length() > HttpServerOptions.DEFAULT_MAX_FORM_ATTRIBUTE_SIZE); given() .multiPart("text", fileContents) diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/TooLargeFormAttributeMultipartFormInputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/TooLargeFormAttributeMultipartFormInputTest.java index 6699beab8c705d..ef3d7553191684 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/TooLargeFormAttributeMultipartFormInputTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/multipart/TooLargeFormAttributeMultipartFormInputTest.java @@ -66,9 +66,13 @@ public void clearDirectory() { @Test public void test() throws IOException { - String formAttrSourceFileContents = new String(Files.readAllBytes(FORM_ATTR_SOURCE_FILE.toPath()), - StandardCharsets.UTF_8); - Assertions.assertTrue(formAttrSourceFileContents.length() > HttpServerOptions.DEFAULT_MAX_FORM_ATTRIBUTE_SIZE); + String fileContents = new String(Files.readAllBytes(FORM_ATTR_SOURCE_FILE.toPath()), StandardCharsets.UTF_8); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10; ++i) { + sb.append(fileContents); + } + fileContents = sb.toString(); + Assertions.assertTrue(fileContents.length() > HttpServerOptions.DEFAULT_MAX_FORM_ATTRIBUTE_SIZE); given() .multiPart("active", "true") .multiPart("num", "25") @@ -76,7 +80,7 @@ public void test() throws IOException { .multiPart("htmlFile", HTML_FILE, "text/html") .multiPart("xmlFile", XML_FILE, "text/xml") .multiPart("txtFile", TXT_FILE, "text/plain") - .multiPart("name", formAttrSourceFileContents) + .multiPart("name", fileContents) .accept("text/plain") .when() .post("/test") diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/UriInfoTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/UriInfoTest.java index fa6194b91bb728..6357946c627c04 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/UriInfoTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/UriInfoTest.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.resteasy.reactive.server.test.resource.basic.resource.GetAbsolutePathResource; import io.quarkus.resteasy.reactive.server.test.resource.basic.resource.UriInfoEncodedQueryResource; import io.quarkus.resteasy.reactive.server.test.resource.basic.resource.UriInfoEncodedTemplateResource; import io.quarkus.resteasy.reactive.server.test.resource.basic.resource.UriInfoEscapedMatrParamResource; @@ -54,7 +55,7 @@ public JavaArchive get() { war.addClasses(UriInfoSimpleResource.class, UriInfoEncodedQueryResource.class, UriInfoQueryParamsResource.class, UriInfoSimpleSingletonResource.class, UriInfoEncodedTemplateResource.class, UriInfoEscapedMatrParamResource.class, - UriInfoEncodedTemplateResource.class); + UriInfoEncodedTemplateResource.class, GetAbsolutePathResource.class); return war; } }); @@ -167,4 +168,20 @@ private static void basicTest(String path, String testName) throws Exception { public void testQueryParamsMutability() throws Exception { basicTest("/queryParams?a=a,b", "UriInfoQueryParamsResource"); } + + @Test + @DisplayName("Test Get Absolute Path") + public void testGetAbsolutePath() throws Exception { + doTestGetAbsolutePath("/absolutePath", "unset"); + doTestGetAbsolutePath("/absolutePath?dummy=1234", "1234"); + doTestGetAbsolutePath("/absolutePath?foo=bar&dummy=1234", "1234"); + } + + private void doTestGetAbsolutePath(String path, String expectedDummyHeader) { + String absolutePathHeader = RestAssured.get(path) + .then() + .statusCode(200) + .header("dummy", expectedDummyHeader).extract().header("absolutePath"); + org.assertj.core.api.Assertions.assertThat(absolutePathHeader).endsWith("/absolutePath"); + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/GetAbsolutePathResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/GetAbsolutePathResource.java new file mode 100644 index 00000000000000..b35f1bd895ada0 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/resource/basic/resource/GetAbsolutePathResource.java @@ -0,0 +1,20 @@ +package io.quarkus.resteasy.reactive.server.test.resource.basic.resource; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +@Path("absolutePath") +public class GetAbsolutePathResource { + + @GET + public Response response(@QueryParam("dummy") String dummy, @Context UriInfo uriInfo) { + return Response.ok() + .header("absolutePath", uriInfo.getAbsolutePath().toString()) + .header("dummy", dummy == null ? "unset" : dummy) + .build(); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/RawListQueryParamTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/RawListQueryParamTest.java new file mode 100644 index 00000000000000..3fb6ae3effba1b --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/simple/RawListQueryParamTest.java @@ -0,0 +1,56 @@ +package io.quarkus.resteasy.reactive.server.test.simple; + +import java.util.List; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.hamcrest.Matchers; +import org.jboss.resteasy.reactive.RestQuery; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class RawListQueryParamTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(HelloResource.class)); + + @Test + public void noQueryParams() { + RestAssured.get("/hello") + .then().statusCode(200).body(Matchers.equalTo("hello world")); + } + + @Test + public void singleQueryParam() { + RestAssured.get("/hello?name=foo") + .then().statusCode(200).body(Matchers.equalTo("hello foo")); + } + + @Test + public void multipleQueryParams() { + RestAssured.get("/hello?name=foo&name=bar") + .then().statusCode(200).body(Matchers.equalTo("hello foo,bar")); + } + + @Path("hello") + public static class HelloResource { + + @GET + @SuppressWarnings({ "rawtypes", "unchecked" }) + public String hello(@RestQuery("name") List names) { + if (names.isEmpty()) { + return "hello world"; + } + return "hello " + String.join(",", names); + } + + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/multipart/MultipartSupport.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/multipart/MultipartSupport.java index 979fb51e18c845..1ee6f0fe2278be 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/multipart/MultipartSupport.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/multipart/MultipartSupport.java @@ -20,6 +20,7 @@ import javax.ws.rs.ext.MessageBodyReader; import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.multipart.FileUpload; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.core.ServerSerialisers; import org.jboss.resteasy.reactive.server.core.multipart.DefaultFileUpload; @@ -31,6 +32,7 @@ /** * This class isn't used directly, it is however used by generated code meant to deal with multipart forms. */ +@SuppressWarnings("unused") public final class MultipartSupport { private static final Logger log = Logger.getLogger(RequestDeserializeHandler.class); @@ -42,8 +44,22 @@ private MultipartSupport() { @SuppressWarnings({ "unchecked", "rawtypes" }) public static Object convertFormAttribute(String value, Class type, Type genericType, MediaType mediaType, - ResteasyReactiveRequestContext context) { + ResteasyReactiveRequestContext context, String attributeName) { if (value == null) { + FormData formData = context.getFormData(); + if (formData != null) { + Collection fileUploadsForName = formData.get(attributeName); + if (fileUploadsForName != null) { + for (FormData.FormValue fileUpload : fileUploadsForName) { + if (fileUpload.isFileItem()) { + log.debug("Attribute '" + attributeName + + "' of the multipart request is a file and therefore its value is not set. To obtain the contents of the file, use type '" + + FileUpload.class + "' as the field type."); + break; + } + } + } + } return null; } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/MicroProfileRestClientEnricher.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/MicroProfileRestClientEnricher.java index f5e90dc889c70a..88ef8823de5b07 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/MicroProfileRestClientEnricher.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/MicroProfileRestClientEnricher.java @@ -53,6 +53,7 @@ import io.quarkus.gizmo.TryBlock; import io.quarkus.jaxrs.client.reactive.deployment.JaxrsClientReactiveEnricher; import io.quarkus.rest.client.reactive.HeaderFiller; +import io.quarkus.rest.client.reactive.runtime.ConfigUtils; import io.quarkus.rest.client.reactive.runtime.MicroProfileRestClientRequestFilter; import io.quarkus.rest.client.reactive.runtime.NoOpHeaderFiller; import io.quarkus.runtime.util.HashUtil; @@ -321,9 +322,19 @@ private void addHeaderParam(MethodInfo declaringMethod, MethodCreator fillHeader .trueBranch(); if (values.length > 1 || !(values[0].startsWith("{") && values[0].endsWith("}"))) { + boolean required = annotation.valueWithDefault(index, "required").asBoolean(); ResultHandle headerList = fillHeaders.newInstance(MethodDescriptor.ofConstructor(ArrayList.class)); for (String value : values) { - fillHeaders.invokeInterfaceMethod(LIST_ADD_METHOD, headerList, fillHeaders.load(value)); + if (value.startsWith("${") && value.endsWith("}")) { + ResultHandle headerValueFromConfig = fillHeaders.invokeStaticMethod( + MethodDescriptor.ofMethod(ConfigUtils.class, "getConfigValue", String.class, String.class, + boolean.class), + fillHeaders.load(value), fillHeaders.load(required)); + fillHeaders.ifNotNull(headerValueFromConfig) + .trueBranch().invokeInterfaceMethod(LIST_ADD_METHOD, headerList, headerValueFromConfig); + } else { + fillHeaders.invokeInterfaceMethod(LIST_ADD_METHOD, headerList, fillHeaders.load(value)); + } } fillHeaders.invokeInterfaceMethod(MAP_PUT_METHOD, headerMap, fillHeaders.load(headerName), headerList); diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/MultipartFilenameTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/MultipartFilenameTest.java new file mode 100644 index 00000000000000..89ca96c51be375 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/MultipartFilenameTest.java @@ -0,0 +1,80 @@ +package io.quarkus.rest.client.reactive; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.net.URI; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.jboss.resteasy.reactive.MultipartForm; +import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.multipart.FileUpload; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; + +public class MultipartFilenameTest { + + @TestHTTPResource + URI baseUri; + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(Resource.class, FormData.class, Client.class, ClientForm.class)) + .withConfigurationResource("dependent-test-application.properties"); + + @Test + void shouldPassOriginalFileName() throws IOException { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + + File file = File.createTempFile("MultipartTest", ".txt"); + file.deleteOnExit(); + + ClientForm form = new ClientForm(); + form.file = file; + assertThat(client.postMultipart(form)).isEqualTo(file.getName()); + } + + @Path("/multipart") + @ApplicationScoped + public static class Resource { + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String upload(@MultipartForm FormData form) { + return form.myFile.fileName(); + } + } + + public static class FormData { + @FormParam("myFile") + public FileUpload myFile; + + } + + @Path("/multipart") + public interface Client { + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + String postMultipart(@MultipartForm ClientForm clientForm); + + } + + public static class ClientForm { + @FormParam("myFile") + @PartType(MediaType.APPLICATION_OCTET_STREAM) + public File file; + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/ClientHeaderParamFromPropertyTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/ClientHeaderParamFromPropertyTest.java new file mode 100644 index 00000000000000..6a3035988b1078 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/headers/ClientHeaderParamFromPropertyTest.java @@ -0,0 +1,84 @@ +package io.quarkus.rest.client.reactive.headers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.URI; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; + +public class ClientHeaderParamFromPropertyTest { + private static final String HEADER_VALUE = "oifajrofijaeoir5gjaoasfaxcvcz"; + + @TestHTTPResource + URI baseUri; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(Client.class) + .addAsResource( + new StringAsset("my.property-value=" + HEADER_VALUE), + "application.properties")); + + @Test + void shouldSetHeaderFromProperties() { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri) + .build(Client.class); + + assertThat(client.getWithHeader()).isEqualTo(HEADER_VALUE); + } + + @Test + void shouldFailOnMissingRequiredHeaderProperty() { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri) + .build(Client.class); + + assertThatThrownBy(client::missingRequiredProperty) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void shouldSucceedOnMissingNonRequiredHeaderProperty() { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri) + .build(Client.class); + + assertThat(client.missingNonRequiredProperty()).isEqualTo(HEADER_VALUE); + } + + @Path("/") + @ApplicationScoped + public static class Resource { + @GET + public String returnHeaderValue(@HeaderParam("my-header") String header) { + return header; + } + } + + @ClientHeaderParam(name = "my-header", value = "${my.property-value}") + public interface Client { + @GET + String getWithHeader(); + + @GET + @ClientHeaderParam(name = "some-other-header", value = "${non-existent-property}") + String missingRequiredProperty(); + + @GET + @ClientHeaderParam(name = "some-other-header", value = "${non-existent-property}", required = false) + String missingNonRequiredProperty(); + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/ConfigUtils.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/ConfigUtils.java new file mode 100644 index 00000000000000..f1596ddfb713ff --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/ConfigUtils.java @@ -0,0 +1,41 @@ +package io.quarkus.rest.client.reactive.runtime; + +import java.util.NoSuchElementException; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logging.Logger; + +public class ConfigUtils { + + private static final Logger log = Logger.getLogger(ConfigUtils.class); + + public static String getConfigValue(String configProperty, boolean required) { + String propertyName = stripPrefixAndSuffix(configProperty); + try { + return ConfigProvider.getConfig().getValue(propertyName, String.class); + } catch (NoSuchElementException e) { + String message = "Failed to find value for config property " + configProperty + + " in application configuration. Please provide the value for the property, e.g. by adding " + + propertyName + "= to your application.properties"; + if (required) { + throw new IllegalArgumentException(message, e); + } else { + log.warn(message); + return null; + } + } catch (IllegalArgumentException e) { + String message = "Failed to convert value for property " + configProperty + " to String"; + if (required) { + throw new IllegalArgumentException(message, e); + } else { + log.warn(message); + return null; + } + } + } + + private static String stripPrefixAndSuffix(String configProperty) { + // by now we know that configProperty is of form ${...} + return configProperty.substring(2, configProperty.length() - 1); + } +} diff --git a/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java index 490927bc4c6534..f2e1825b9007d6 100644 --- a/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java +++ b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/SchedulerProcessor.java @@ -96,6 +96,7 @@ public class SchedulerProcessor { static final DotName SCHEDULED_NAME = DotName.createSimple(Scheduled.class.getName()); static final DotName SCHEDULES_NAME = DotName.createSimple(Scheduled.Schedules.class.getName()); static final DotName SKIP_NEVER_NAME = DotName.createSimple(Scheduled.Never.class.getName()); + static final DotName SKIP_PREDICATE = DotName.createSimple(Scheduled.SkipPredicate.class.getName()); static final Type SCHEDULED_EXECUTION_TYPE = Type.create(DotName.createSimple(ScheduledExecution.class.getName()), Kind.CLASS); @@ -466,4 +467,9 @@ private Throwable validateScheduled(CronParser parser, AnnotationInstance schedu return null; } + @BuildStep + UnremovableBeanBuildItem unremoveableSkipPredicates() { + return new UnremovableBeanBuildItem(new UnremovableBeanBuildItem.BeanTypeExclusion(SKIP_PREDICATE)); + } + } diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ConditionalExecutionTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ConditionalExecutionTest.java index 6f7e0b9eab1311..f04cd3eff6a4b8 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ConditionalExecutionTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ConditionalExecutionTest.java @@ -1,11 +1,13 @@ package io.quarkus.scheduler.test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import javax.enterprise.event.Observes; import javax.inject.Singleton; @@ -44,17 +46,25 @@ public void testExecution() { Thread.currentThread().interrupt(); throw new IllegalStateException(e); } + + assertTrue(OtherIsDisabled.TESTED.get()); + assertEquals(0, Jobs.OTHER_COUNT.get()); } static class Jobs { static final CountDownLatch COUNTER = new CountDownLatch(1); + static final AtomicInteger OTHER_COUNT = new AtomicInteger(0); @Scheduled(identity = "foo", every = "1s", skipExecutionIf = IsDisabled.class) void doSomething() throws InterruptedException { COUNTER.countDown(); } + @Scheduled(identity = "other-foo", every = "1s", skipExecutionIf = OtherIsDisabled.class) + void doSomethingElse() throws InterruptedException { + OTHER_COUNT.incrementAndGet(); + } } @Singleton @@ -77,4 +87,17 @@ void onSkip(@Observes SkippedExecution event) { } } + + @Singleton + public static class OtherIsDisabled implements Scheduled.SkipPredicate { + + static final AtomicBoolean TESTED = new AtomicBoolean(false); + + @Override + public boolean test(ScheduledExecution execution) { + TESTED.set(true); + return true; + } + + } } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/DenyAllInterceptor.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/DenyAllInterceptor.java index 9b408b7d6c87d0..66f92e3ec14492 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/DenyAllInterceptor.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/DenyAllInterceptor.java @@ -7,6 +7,8 @@ import javax.interceptor.Interceptor; import javax.interceptor.InvocationContext; +import io.quarkus.security.spi.runtime.AuthorizationController; + /** * * @author Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com @@ -19,8 +21,15 @@ public class DenyAllInterceptor { @Inject SecurityHandler handler; + @Inject + AuthorizationController controller; + @AroundInvoke public Object intercept(InvocationContext ic) throws Exception { - return handler.handle(ic); + if (controller.isAuthorizationEnabled()) { + return handler.handle(ic); + } else { + return ic.proceed(); + } } } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/AuthenticatedCheck.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/AuthenticatedCheck.java index 4b236929889147..ac80cbf0249d9e 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/AuthenticatedCheck.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/interceptor/check/AuthenticatedCheck.java @@ -2,21 +2,47 @@ import java.lang.reflect.Method; +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.InstanceHandle; import io.quarkus.security.UnauthorizedException; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.spi.runtime.AuthorizationController; import io.quarkus.security.spi.runtime.SecurityCheck; public class AuthenticatedCheck implements SecurityCheck { public static final AuthenticatedCheck INSTANCE = new AuthenticatedCheck(); + private volatile AuthorizationController authorizationController; + private AuthenticatedCheck() { } @Override public void apply(SecurityIdentity identity, Method method, Object[] parameters) { + if (isAuthorizationDisabled()) { + return; + } if (identity.isAnonymous()) { throw new UnauthorizedException(); } } + + private boolean isAuthorizationDisabled() { + if (authorizationController != null) { + return !authorizationController.isAuthorizationEnabled(); + } + + ArcContainer container = Arc.container(); + if ((container == null) || !container.isRunning()) { + return false; + } + InstanceHandle instance = container.instance(AuthorizationController.class); + if (instance.isAvailable()) { + authorizationController = instance.get(); + return !instance.get().isAuthorizationEnabled(); + } + return false; + } } diff --git a/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecuredClassesBuildItem.java b/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecuredClassesBuildItem.java index 5a1e138548253f..342012156657da 100644 --- a/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecuredClassesBuildItem.java +++ b/extensions/security/spi/src/main/java/io/quarkus/security/spi/AdditionalSecuredClassesBuildItem.java @@ -13,10 +13,8 @@ * Contains classes that need to have @DenyAll on all methods that don't have security annotations */ public final class AdditionalSecuredClassesBuildItem extends MultiBuildItem { + public final Collection additionalSecuredClasses; - /** - * The roles alloe - */ public final Optional> rolesAllowed; public AdditionalSecuredClassesBuildItem(Collection additionalSecuredClasses) { diff --git a/extensions/smallrye-context-propagation/deployment/pom.xml b/extensions/smallrye-context-propagation/deployment/pom.xml index 1cc9af0cfcb62f..2532ae391f4b1d 100644 --- a/extensions/smallrye-context-propagation/deployment/pom.xml +++ b/extensions/smallrye-context-propagation/deployment/pom.xml @@ -24,6 +24,11 @@ io.quarkus quarkus-smallrye-context-propagation
+ + io.quarkus + quarkus-junit5-internal + test +
diff --git a/extensions/smallrye-context-propagation/deployment/src/test/java/io/quarkus/smallrye/context/deployment/test/AllClearedConfigTest.java b/extensions/smallrye-context-propagation/deployment/src/test/java/io/quarkus/smallrye/context/deployment/test/AllClearedConfigTest.java new file mode 100644 index 00000000000000..b93d0b1b160e8b --- /dev/null +++ b/extensions/smallrye-context-propagation/deployment/src/test/java/io/quarkus/smallrye/context/deployment/test/AllClearedConfigTest.java @@ -0,0 +1,31 @@ +package io.quarkus.smallrye.context.deployment.test; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.context.SmallRyeThreadContext; +import io.smallrye.context.impl.ThreadContextProviderPlan; + +public class AllClearedConfigTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)) + .overrideConfigKey("mp.context.ThreadContext.propagated", ""); + + @Inject + SmallRyeThreadContext ctx; + + @Test + public void test() { + ThreadContextProviderPlan plan = ctx.getPlan(); + Assertions.assertTrue(plan.propagatedProviders.isEmpty()); + Assertions.assertFalse(plan.clearedProviders.isEmpty()); + Assertions.assertTrue(plan.unchangedProviders.isEmpty()); + } +} diff --git a/extensions/smallrye-context-propagation/deployment/src/test/java/io/quarkus/smallrye/context/deployment/test/AllUnchangedConfigTest.java b/extensions/smallrye-context-propagation/deployment/src/test/java/io/quarkus/smallrye/context/deployment/test/AllUnchangedConfigTest.java new file mode 100644 index 00000000000000..25f9615d385abb --- /dev/null +++ b/extensions/smallrye-context-propagation/deployment/src/test/java/io/quarkus/smallrye/context/deployment/test/AllUnchangedConfigTest.java @@ -0,0 +1,32 @@ +package io.quarkus.smallrye.context.deployment.test; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.context.SmallRyeThreadContext; +import io.smallrye.context.impl.ThreadContextProviderPlan; + +public class AllUnchangedConfigTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)) + .overrideConfigKey("mp.context.ThreadContext.unchanged", "Remaining") + .overrideConfigKey("mp.context.ThreadContext.propagated", ""); + + @Inject + SmallRyeThreadContext ctx; + + @Test + public void test() { + ThreadContextProviderPlan plan = ctx.getPlan(); + Assertions.assertTrue(plan.propagatedProviders.isEmpty()); + Assertions.assertTrue(plan.clearedProviders.isEmpty()); + Assertions.assertFalse(plan.unchangedProviders.isEmpty()); + } +} diff --git a/extensions/smallrye-context-propagation/deployment/src/test/java/io/quarkus/smallrye/context/deployment/test/NoConfigTest.java b/extensions/smallrye-context-propagation/deployment/src/test/java/io/quarkus/smallrye/context/deployment/test/NoConfigTest.java new file mode 100644 index 00000000000000..f24efdd7ec7478 --- /dev/null +++ b/extensions/smallrye-context-propagation/deployment/src/test/java/io/quarkus/smallrye/context/deployment/test/NoConfigTest.java @@ -0,0 +1,30 @@ +package io.quarkus.smallrye.context.deployment.test; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.context.SmallRyeThreadContext; +import io.smallrye.context.impl.ThreadContextProviderPlan; + +public class NoConfigTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Inject + SmallRyeThreadContext ctx; + + @Test + public void test() { + ThreadContextProviderPlan plan = ctx.getPlan(); + Assertions.assertFalse(plan.propagatedProviders.isEmpty()); + Assertions.assertTrue(plan.clearedProviders.isEmpty()); + Assertions.assertTrue(plan.unchangedProviders.isEmpty()); + } +} diff --git a/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationProvider.java b/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationProvider.java index 8324186dbca2ea..d47a7f087cd2ac 100644 --- a/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationProvider.java +++ b/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationProvider.java @@ -16,7 +16,8 @@ public class SmallRyeContextPropagationProvider { @Singleton @DefaultBean public SmallRyeThreadContext getAllThreadContext() { - return (SmallRyeThreadContext) ThreadContext.builder().propagated(ThreadContext.ALL_REMAINING).cleared().unchanged() + // Make sure we use the default values, which use the MP Config keys to allow users to override them + return (SmallRyeThreadContext) ThreadContext.builder() .build(); } diff --git a/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationRecorder.java b/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationRecorder.java index 2c541aceada9c8..86179875d4ec47 100644 --- a/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationRecorder.java +++ b/extensions/smallrye-context-propagation/runtime/src/main/java/io/quarkus/smallrye/context/runtime/SmallRyeContextPropagationRecorder.java @@ -50,7 +50,8 @@ public void configureRuntime(ExecutorService executorService, ShutdownContext sh SmallRyeContextManager contextManager = builder.build(); contextManagerProvider.registerContextManager(contextManager, Thread.currentThread().getContextClassLoader()); - shutdownContext.addShutdownTask(new Runnable() { + //needs to be late, as running threads can re-create an implicit one + shutdownContext.addLastShutdownTask(new Runnable() { @Override public void run() { contextManagerProvider.releaseContextManager(contextManager); diff --git a/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/GraphQLClientConfigurationMergerBean.java b/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/GraphQLClientConfigurationMergerBean.java index b14d2983bc62ad..20eb364655d009 100644 --- a/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/GraphQLClientConfigurationMergerBean.java +++ b/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/GraphQLClientConfigurationMergerBean.java @@ -13,14 +13,13 @@ * On startup, this beans takes Quarkus-specific configuration of GraphQL clients (quarkus.* properties) * and merges this configuration with the configuration parsed by SmallRye GraphQL itself (CLIENT/mp-graphql/* properties) * - * The resulting merged configuration resides in the application-scoped `io.smallrye.graphql.client.GraphQLClientConfiguration` + * The resulting merged configuration resides in `io.smallrye.graphql.client.GraphQLClientsConfiguration` * * Quarkus configuration overrides SmallRye configuration where applicable. */ @Singleton public class GraphQLClientConfigurationMergerBean { - @Inject GraphQLClientsConfiguration upstreamConfiguration; @Inject @@ -31,6 +30,7 @@ public class GraphQLClientConfigurationMergerBean { @PostConstruct void enhanceGraphQLConfiguration() { + upstreamConfiguration = GraphQLClientsConfiguration.getInstance(); for (Map.Entry client : quarkusConfiguration.clients.entrySet()) { // the raw config key provided in the config, this might be a short class name, // so translate that into the fully qualified name if applicable @@ -43,14 +43,14 @@ void enhanceGraphQLConfiguration() { GraphQLClientConfig quarkusConfig = client.getValue(); // if SmallRye configuration does not contain this client, simply use it - if (!upstreamConfiguration.getClients().containsKey(configKey)) { + if (upstreamConfiguration.getClient(configKey) == null) { GraphQLClientConfiguration transformed = new GraphQLClientConfiguration(); transformed.setHeaders(quarkusConfig.headers); quarkusConfig.url.ifPresent(transformed::setUrl); - upstreamConfiguration.getClients().put(configKey, transformed); + upstreamConfiguration.addClient(configKey, transformed); } else { // if SmallRye configuration already contains this client, override it with the Quarkus configuration - GraphQLClientConfiguration upstreamConfig = upstreamConfiguration.getClients().get(configKey); + GraphQLClientConfiguration upstreamConfig = upstreamConfiguration.getClient(configKey); quarkusConfig.url.ifPresent(upstreamConfig::setUrl); // merge the headers if (quarkusConfig.headers != null) { diff --git a/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/SmallRyeGraphQLClientRecorder.java b/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/SmallRyeGraphQLClientRecorder.java index 041c43129fd9c9..a7bffb28e752aa 100644 --- a/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/SmallRyeGraphQLClientRecorder.java +++ b/extensions/smallrye-graphql-client/runtime/src/main/java/io/quarkus/smallrye/graphql/client/runtime/SmallRyeGraphQLClientRecorder.java @@ -22,7 +22,8 @@ public Supplier typesafeClientSupplier(Class targetClassName) { } public void setTypesafeApiClasses(List apiClassNames) { - GraphQLClientsConfiguration configBean = Arc.container().instance(GraphQLClientsConfiguration.class).get(); + GraphQLClientsConfiguration.setSingleApplication(true); + GraphQLClientsConfiguration configBean = GraphQLClientsConfiguration.getInstance(); List> classes = apiClassNames.stream().map(className -> { try { return Class.forName(className, true, Thread.currentThread().getContextClassLoader()); @@ -30,7 +31,7 @@ public void setTypesafeApiClasses(List apiClassNames) { throw new RuntimeException(e); } }).collect(Collectors.toList()); - configBean.apiClasses(classes, true); + configBean.addTypesafeClientApis(classes); } public RuntimeValue clientSupport(Map shortNamesToQualifiedNames) { diff --git a/extensions/smallrye-graphql/deployment/pom.xml b/extensions/smallrye-graphql/deployment/pom.xml index cf6474ef198151..cd7dd722d20ea4 100644 --- a/extensions/smallrye-graphql/deployment/pom.xml +++ b/extensions/smallrye-graphql/deployment/pom.xml @@ -18,7 +18,6 @@ io.quarkus quarkus-smallrye-graphql - ${project.version} diff --git a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/OverridableIndex.java b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/OverridableIndex.java new file mode 100644 index 00000000000000..6b87a8d21ee6ef --- /dev/null +++ b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/OverridableIndex.java @@ -0,0 +1,250 @@ +package io.quarkus.smallrye.graphql.deployment; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Set; +import java.util.TreeSet; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.MethodParameterInfo; +import org.jboss.jandex.ModuleInfo; +import org.jboss.jandex.RecordComponentInfo; +import org.jboss.jandex.Type; + +public class OverridableIndex implements IndexView { + + private final IndexView original; + private final IndexView override; + + private OverridableIndex(IndexView original, IndexView override) { + this.original = original; + this.override = override; + } + + public static OverridableIndex create(IndexView original, IndexView override) { + return new OverridableIndex(original, override); + } + + @Override + public Collection getKnownClasses() { + return overrideCollection(original.getKnownClasses(), override.getKnownClasses(), classInfoComparator); + } + + @Override + public ClassInfo getClassByName(DotName dn) { + return overrideObject(original.getClassByName(dn), override.getClassByName(dn)); + } + + @Override + public Collection getKnownDirectSubclasses(DotName dn) { + return overrideCollection(original.getKnownDirectSubclasses(dn), override.getKnownDirectSubclasses(dn), + classInfoComparator); + } + + @Override + public Collection getAllKnownSubclasses(DotName dn) { + return overrideCollection(original.getAllKnownSubclasses(dn), override.getAllKnownSubclasses(dn), classInfoComparator); + } + + @Override + public Collection getKnownDirectImplementors(DotName dn) { + return overrideCollection(original.getKnownDirectImplementors(dn), override.getKnownDirectImplementors(dn), + classInfoComparator); + } + + @Override + public Collection getAllKnownImplementors(DotName dn) { + return overrideCollection(original.getAllKnownImplementors(dn), override.getAllKnownImplementors(dn), + classInfoComparator); + } + + @Override + public Collection getAnnotations(DotName dn) { + return overrideCollection(original.getAnnotations(dn), override.getAnnotations(dn), annotationInstanceComparator); + } + + @Override + public Collection getAnnotationsWithRepeatable(DotName dn, IndexView iv) { + return overrideCollection(original.getAnnotationsWithRepeatable(dn, iv), override.getAnnotationsWithRepeatable(dn, iv), + annotationInstanceComparator); + } + + @Override + public Collection getKnownModules() { + return overrideCollection(original.getKnownModules(), override.getKnownModules(), moduleInfoComparator); + } + + @Override + public ModuleInfo getModuleByName(DotName dn) { + return overrideObject(original.getModuleByName(dn), override.getModuleByName(dn)); + } + + @Override + public Collection getKnownUsers(DotName dn) { + return overrideCollection(original.getKnownUsers(dn), override.getKnownUsers(dn), classInfoComparator); + } + + private Comparator classInfoComparator = new Comparator() { + @Override + public int compare(ClassInfo t, ClassInfo t1) { + return t.name().toString().compareTo(t1.name().toString()); + } + }; + + private Comparator typeComparator = new Comparator() { + @Override + public int compare(Type t, Type t1) { + return t.name().toString().compareTo(t1.name().toString()); + } + }; + + private Comparator moduleInfoComparator = new Comparator() { + @Override + public int compare(ModuleInfo t, ModuleInfo t1) { + return t.name().toString().compareTo(t1.name().toString()); + } + }; + + private Comparator fieldInfoComparator = new Comparator() { + @Override + public int compare(FieldInfo t, FieldInfo t1) { + if (classInfoComparator.compare(t.declaringClass(), t1.declaringClass()) == 0) { // Same class + return t.name().toString().compareTo(t1.name().toString()); + } + return -1; + } + }; + + private Comparator recordComponentInfoComparator = new Comparator() { + @Override + public int compare(RecordComponentInfo t, RecordComponentInfo t1) { + if (classInfoComparator.compare(t.declaringClass(), t1.declaringClass()) == 0) { // Same class + return t.name().toString().compareTo(t1.name().toString()); + } + return -1; + } + }; + + private Comparator methodInfoComparator = new Comparator() { + @Override + public int compare(MethodInfo t, MethodInfo t1) { + if (classInfoComparator.compare(t.declaringClass(), t1.declaringClass()) == 0) { // Same class + if (t.name().toString().compareTo(t1.name().toString()) == 0) { // Same method name + if (t.parameters().size() == t1.parameters().size()) { // Same number of parameters + for (int i = 0; i < t.parameters().size(); i++) { + int typeTheSame = typeComparator.compare(t.parameters().get(i), t1.parameters().get(i)); + if (typeTheSame != 0) { + return typeTheSame; + } + } + // All parameter type are the same + return 0; + } + } + } + return -1; + } + }; + + private Comparator methodParameterInfoComparator = new Comparator() { + @Override + public int compare(MethodParameterInfo t, MethodParameterInfo t1) { + if (methodInfoComparator.compare(t.method(), t1.method()) == 0 && // Same method + t.kind().equals(t1.kind()) && // Same kind + t.name().equals(t1.name()) && // Same name + t.position() == t1.position()) { // Same position + return 0; + } + return -1; + } + }; + + private Comparator annotationInstanceComparator = new Comparator() { + @Override + public int compare(AnnotationInstance t, AnnotationInstance t1) { + if (t.name().equals(t1.name())) { + // Class Info + if (t.target().kind().equals(AnnotationTarget.Kind.CLASS) + && t1.target().kind().equals(AnnotationTarget.Kind.CLASS)) { + return classInfoComparator.compare(t.target().asClass(), t1.target().asClass()); + } + + // Field Info + if (t.target().kind().equals(AnnotationTarget.Kind.FIELD) + && t1.target().kind().equals(AnnotationTarget.Kind.FIELD)) { + return fieldInfoComparator.compare(t.target().asField(), t1.target().asField()); + } + + // Type + if (t.target().kind().equals(AnnotationTarget.Kind.TYPE) + && t1.target().kind().equals(AnnotationTarget.Kind.TYPE)) { + return typeComparator.compare(t.target().asType().target(), t1.target().asType().target()); + } + + // Method Info + if (t.target().kind().equals(AnnotationTarget.Kind.METHOD) + && t1.target().kind().equals(AnnotationTarget.Kind.METHOD)) { + return methodInfoComparator.compare(t.target().asMethod(), t1.target().asMethod()); + } + + // Method Parameter + if (t.target().kind().equals(AnnotationTarget.Kind.METHOD_PARAMETER) + && t1.target().kind().equals(AnnotationTarget.Kind.METHOD_PARAMETER)) { + return methodParameterInfoComparator.compare(t.target().asMethodParameter(), + t1.target().asMethodParameter()); + } + + // Record + if (t.target().kind().equals(AnnotationTarget.Kind.RECORD_COMPONENT) + && t1.target().kind().equals(AnnotationTarget.Kind.RECORD_COMPONENT)) { + return recordComponentInfoComparator.compare(t.target().asRecordComponent(), + t1.target().asRecordComponent()); + } + } + return -1; + } + }; + + private Collection overrideCollection(Collection originalCollection, Collection overrideCollection, + Comparator comparator) { + if (originalCollection == null && overrideCollection == null) { + return null; + } + + if (originalCollection == null) { + return overrideCollection; + } + if (overrideCollection == null) { + return originalCollection; + } + + if (originalCollection.isEmpty() && overrideCollection.isEmpty()) { + return originalCollection; + } + + if (originalCollection.isEmpty()) { + return overrideCollection; + } + if (overrideCollection.isEmpty()) { + return originalCollection; + } + + Set newCollection = new TreeSet<>(comparator); + newCollection.addAll(overrideCollection); + newCollection.addAll(originalCollection); // Won't add if it's already there. + return newCollection; + } + + private T overrideObject(T originalObject, T overrideObject) { + if (overrideObject != null) { + return overrideObject; + } + return originalObject; + } +} diff --git a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLIndexBuildItem.java b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLIndexBuildItem.java new file mode 100644 index 00000000000000..c6c3c0e3bea1d2 --- /dev/null +++ b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLIndexBuildItem.java @@ -0,0 +1,18 @@ +package io.quarkus.smallrye.graphql.deployment; + +import java.util.Map; + +import io.quarkus.builder.item.SimpleBuildItem; + +final class SmallRyeGraphQLIndexBuildItem extends SimpleBuildItem { + + private final Map modifiedClases; + + public SmallRyeGraphQLIndexBuildItem(Map modifiedClases) { + this.modifiedClases = modifiedClases; + } + + public Map getModifiedClases() { + return modifiedClases; + } +} diff --git a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java index 52ac9f9278bfc4..22de82bc20aad3 100644 --- a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java +++ b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java @@ -1,9 +1,11 @@ package io.quarkus.smallrye.graphql.deployment; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -13,7 +15,7 @@ import java.util.stream.Stream; import org.eclipse.microprofile.config.ConfigProvider; -import org.jboss.jandex.IndexView; +import org.jboss.jandex.Indexer; import org.jboss.logging.Logger; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; @@ -38,6 +40,7 @@ import io.quarkus.deployment.builditem.LiveReloadBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; +import io.quarkus.deployment.builditem.TransformedClassesBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem; @@ -158,6 +161,22 @@ void registerNativeImageResources(BuildProducer servic .produce(ServiceProviderBuildItem.allProvidersFromClassPath(SmallRyeGraphQLConfigMapping.class.getName())); } + @BuildStep + SmallRyeGraphQLIndexBuildItem createIndex(TransformedClassesBuildItem transformedClassesBuildItem) { + Map modifiedClasses = new HashMap<>(); + Map> transformedClassesByJar = transformedClassesBuildItem + .getTransformedClassesByJar(); + for (Map.Entry> transformedClassesByJarEntrySet : transformedClassesByJar + .entrySet()) { + + Set transformedClasses = transformedClassesByJarEntrySet.getValue(); + for (TransformedClassesBuildItem.TransformedClass transformedClass : transformedClasses) { + modifiedClasses.put(transformedClass.getClassName(), transformedClass.getData()); + } + } + return new SmallRyeGraphQLIndexBuildItem(modifiedClasses); + } + @Record(ExecutionTime.STATIC_INIT) @BuildStep void buildExecutionService( @@ -165,13 +184,25 @@ void buildExecutionService( BuildProducer reflectiveHierarchyProducer, BuildProducer graphQLInitializedProducer, SmallRyeGraphQLRecorder recorder, + SmallRyeGraphQLIndexBuildItem graphQLIndexBuildItem, BeanContainerBuildItem beanContainer, CombinedIndexBuildItem combinedIndex, SmallRyeGraphQLConfig graphQLConfig) { - IndexView index = combinedIndex.getIndex(); + Indexer indexer = new Indexer(); + Map modifiedClases = graphQLIndexBuildItem.getModifiedClases(); + + for (Map.Entry kv : modifiedClases.entrySet()) { + try (ByteArrayInputStream bais = new ByteArrayInputStream(kv.getValue())) { + indexer.index(bais); + } catch (IOException ex) { + LOG.warn("Could not index [" + kv.getKey() + "] - " + ex.getMessage()); + } + } + + OverridableIndex overridableIndex = OverridableIndex.create(combinedIndex.getIndex(), indexer.complete()); - Schema schema = SchemaBuilder.build(index, graphQLConfig.autoNameStrategy); + Schema schema = SchemaBuilder.build(overridableIndex, graphQLConfig.autoNameStrategy); RuntimeValue initialized = recorder.createExecutionService(beanContainer.getValue(), schema); graphQLInitializedProducer.produce(new SmallRyeGraphQLInitializedBuildItem(initialized)); diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/HotReloadTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/HotReloadTest.java index 5976f6ffed18df..8b25292acd725d 100644 --- a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/HotReloadTest.java +++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/HotReloadTest.java @@ -1,5 +1,8 @@ package io.quarkus.smallrye.graphql.deployment; +import static io.quarkus.smallrye.graphql.deployment.AbstractGraphQLTest.MEDIATYPE_JSON; + +import org.hamcrest.CoreMatchers; import org.jboss.logging.Logger; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.EmptyAsset; @@ -9,6 +12,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; /** * Test Hot reload after a code change @@ -25,6 +29,79 @@ public class HotReloadTest extends AbstractGraphQLTest { .addAsResource(new StringAsset(getPropertyAsString()), "application.properties") .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")); + @Test + public void testAddAndRemoveFieldChange() { + + String fooRequest = getPayload("{\n" + + " foo {\n" + + " message\n" + + " randomNumber{\n" + + " value\n" + + " }\n" + + " foo\n" + + " list\n" + + " }\n" + + "}"); + + // Do a request + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .body(CoreMatchers.containsString( + "{\"errors\":[{\"message\":\"Validation error of type FieldUndefined: Field 'foo' in type 'TestPojo' is undefined @ 'foo/foo'\",\"locations\":[{\"line\":7,\"column\":5}],\"extensions\":{\"classification\":\"ValidationError\"}}],\"data\":null}")); + LOG.info("Initial request done"); + + // Make a code change (add a field) + TEST.modifySourceFile("TestPojo.java", s -> s.replace("// ", + "private String foo = \"bar\";\n" + + " public String getFoo(){\n" + + " return foo;\n" + + " }")); + LOG.info("Code change done - field added"); + + // Do the request again + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .body(CoreMatchers.containsString( + "{\"data\":{\"foo\":{\"message\":\"bar\",\"randomNumber\":{\"value\":123.0},\"foo\":\"bar\",\"list\":[\"a\",\"b\",\"c\"]}}}")); + LOG.info("Hot reload done"); + + // Make a code change again (remove) + TEST.modifySourceFile("TestPojo.java", s -> s.replace("private String foo = \"bar\";\n" + + " public String getFoo(){\n" + + " return foo;\n" + + " }", "// ")); + + // Do the request yet again + RestAssured.given().when() + .accept(MEDIATYPE_JSON) + .contentType(MEDIATYPE_JSON) + .body(fooRequest) + .post("/graphql") + .then() + .assertThat() + .statusCode(200) + .and() + .body(CoreMatchers.containsString( + "{\"errors\":[{\"message\":\"Validation error of type FieldUndefined: Field 'foo' in type 'TestPojo' is undefined @ 'foo/foo'\",\"locations\":[{\"line\":7,\"column\":5}],\"extensions\":{\"classification\":\"ValidationError\"}}],\"data\":null}")); + + LOG.info("Code change done - field removed"); + + } + @Test public void testCodeChange() { // Do a request diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/TestPojo.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/TestPojo.java index 65ff1aea165485..ad707a3149bfd6 100644 --- a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/TestPojo.java +++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/TestPojo.java @@ -45,6 +45,8 @@ public void setNumber(Number number) { this.number = number; } + // + @Override public String toString() { return "TestPojo{" + "message=" + message + ", list=" + list + ", number=" + number + '}'; diff --git a/extensions/smallrye-jwt/deployment/src/main/java/io/quarkus/smallrye/jwt/deployment/SmallRyeJwtProcessor.java b/extensions/smallrye-jwt/deployment/src/main/java/io/quarkus/smallrye/jwt/deployment/SmallRyeJwtProcessor.java index a4aeafe88a9819..3fe6668cc1282c 100644 --- a/extensions/smallrye-jwt/deployment/src/main/java/io/quarkus/smallrye/jwt/deployment/SmallRyeJwtProcessor.java +++ b/extensions/smallrye-jwt/deployment/src/main/java/io/quarkus/smallrye/jwt/deployment/SmallRyeJwtProcessor.java @@ -37,6 +37,7 @@ import io.quarkus.smallrye.jwt.runtime.auth.JwtPrincipalProducer; import io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator; import io.quarkus.smallrye.jwt.runtime.auth.RawOptionalClaimCreator; +import io.quarkus.vertx.http.deployment.SecurityInformationBuildItem; import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm; import io.smallrye.jwt.algorithm.SignatureAlgorithm; import io.smallrye.jwt.auth.cdi.ClaimValueProducer; @@ -69,6 +70,11 @@ EnableAllSecurityServicesBuildItem security() { return new EnableAllSecurityServicesBuildItem(); } + @BuildStep(onlyIf = IsEnabled.class) + public void provideSecurityInformation(BuildProducer securityInformationProducer) { + securityInformationProducer.produce(SecurityInformationBuildItem.JWT()); + } + /** * Register the CDI beans that are needed by the MP-JWT extension * diff --git a/extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java b/extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java index ee2c9295eb5833..22a8546f728d44 100644 --- a/extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java +++ b/extensions/smallrye-openapi-common/deployment/src/main/java/io/quarkus/smallrye/openapi/common/deployment/SmallRyeOpenApiConfig.java @@ -70,6 +70,12 @@ public final class SmallRyeOpenApiConfig { @ConfigItem(defaultValue = "true") public boolean autoAddTags; + /** + * This will automatically add security based on the security extension included (if any). + */ + @ConfigItem(defaultValue = "true") + public boolean autoAddSecurity; + /** * Add a scheme value to the Basic HTTP Security Scheme */ diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java index 3689aef33ab9e4..12e0526b51802b 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java @@ -76,9 +76,14 @@ import io.quarkus.smallrye.openapi.runtime.OpenApiDocumentService; import io.quarkus.smallrye.openapi.runtime.OpenApiRecorder; import io.quarkus.smallrye.openapi.runtime.OpenApiRuntimeConfig; +import io.quarkus.smallrye.openapi.runtime.filter.AutoBasicSecurityFilter; +import io.quarkus.smallrye.openapi.runtime.filter.AutoJWTSecurityFilter; +import io.quarkus.smallrye.openapi.runtime.filter.AutoUrl; +import io.quarkus.smallrye.openapi.runtime.filter.OpenIDConnectSecurityFilter; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.deployment.SecurityInformationBuildItem; import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.openapi.api.OpenApiConfig; @@ -168,9 +173,11 @@ RouteBuildItem handler(LaunchModeBuildItem launch, BuildProducer displayableEndpoints, OpenApiRecorder recorder, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + List securityInformationBuildItems, OpenApiRuntimeConfig openApiRuntimeConfig, ShutdownContextBuildItem shutdownContext, SmallRyeOpenApiConfig openApiConfig, + OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem, HttpConfiguration httpConfiguration) { /* * Ugly Hack @@ -187,7 +194,17 @@ RouteBuildItem handler(LaunchModeBuildItem launch, recorder.setupClDevMode(shutdownContext); } - Handler handler = recorder.handler(openApiRuntimeConfig, httpConfiguration); + OASFilter autoSecurityFilter = null; + if (openApiConfig.autoAddSecurity) { + // Only add the security if there are secured endpoints + OASFilter autoRolesAllowedFilter = getAutoRolesAllowedFilter(openApiConfig.securitySchemeName, + apiFilteredIndexViewBuildItem, openApiConfig); + if (autoRolesAllowedFilter != null) { + autoSecurityFilter = getAutoSecurityFilter(securityInformationBuildItems, openApiConfig); + } + } + + Handler handler = recorder.handler(openApiRuntimeConfig, httpConfiguration, autoSecurityFilter); return nonApplicationRootPathBuildItem.routeBuilder() .route(openApiConfig.path) .routeConfigKey("quarkus.smallrye-openapi.path") @@ -277,6 +294,64 @@ void addSecurityFilter(BuildProducer addToOpenA } + private OASFilter getAutoSecurityFilter(List securityInformationBuildItems, + SmallRyeOpenApiConfig config) { + + // Auto add a security from security extension(s) + if (!config.securityScheme.isPresent() && securityInformationBuildItems != null + && !securityInformationBuildItems.isEmpty()) { + // This needs to be a filter in runtime as the config we use to auto configure is in runtime + for (SecurityInformationBuildItem securityInformationBuildItem : securityInformationBuildItems) { + SecurityInformationBuildItem.SecurityModel securityModel = securityInformationBuildItem.getSecurityModel(); + switch (securityModel) { + case jwt: + return new AutoJWTSecurityFilter( + config.securitySchemeName, + config.securitySchemeDescription, + config.jwtSecuritySchemeValue, + config.jwtBearerFormat); + case basic: + return new AutoBasicSecurityFilter( + config.securitySchemeName, + config.securitySchemeDescription, + config.basicSecuritySchemeValue); + case oidc: + Optional maybeInfo = securityInformationBuildItem + .getOpenIDConnectInformation(); + + if (maybeInfo.isPresent()) { + SecurityInformationBuildItem.OpenIDConnectInformation info = maybeInfo.get(); + + AutoUrl authorizationUrl = new AutoUrl( + config.oidcOpenIdConnectUrl.orElse(null), + info.getUrlConfigKey(), + "/protocol/openid-connect/auth"); + + AutoUrl refreshUrl = new AutoUrl( + config.oidcOpenIdConnectUrl.orElse(null), + info.getUrlConfigKey(), + "/protocol/openid-connect/token"); + + AutoUrl tokenUrl = new AutoUrl( + config.oidcOpenIdConnectUrl.orElse(null), + info.getUrlConfigKey(), + "/protocol/openid-connect/token/introspect"); + + return new OpenIDConnectSecurityFilter( + config.securitySchemeName, + config.securitySchemeDescription, + authorizationUrl, refreshUrl, tokenUrl); + + } + break; + default: + break; + } + } + } + return null; + } + private OASFilter getAutoRolesAllowedFilter(String securitySchemeName, OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem, SmallRyeOpenApiConfig config) { @@ -597,7 +672,7 @@ private OpenAPI generateAnnotationModel(IndexView indexView, Capabilities capabi HttpRootPathBuildItem httpRootPathBuildItem, Optional resteasyJaxrsConfig) { Config config = ConfigProvider.getConfig(); - OpenApiConfig openApiConfig = new OpenApiConfigImpl(config); + OpenApiConfig openApiConfig = OpenApiConfigImpl.fromConfig(config); List extensions = new ArrayList<>(); @@ -610,6 +685,7 @@ private OpenAPI generateAnnotationModel(IndexView indexView, Capabilities capabi } } else if (capabilities.isPresent(Capability.RESTEASY_REACTIVE)) { extensions.add(new RESTEasyExtension(indexView)); + openApiConfig.doAllowNakedPathParameter(); Optional maybePath = config.getOptionalValue("quarkus.resteasy-reactive.path", String.class); if (maybePath.isPresent()) { defaultPath = maybePath.get(); diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentService.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentService.java index c9625b408becfe..95f19a6f1f1bf3 100644 --- a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentService.java +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiDocumentService.java @@ -4,7 +4,6 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; -import javax.annotation.PostConstruct; import javax.enterprise.context.ApplicationScoped; import org.eclipse.microprofile.config.Config; @@ -30,16 +29,15 @@ public class OpenApiDocumentService implements OpenApiDocumentHolder { private OpenApiDocumentHolder documentHolder; - @PostConstruct - void create() throws IOException { + void init(OASFilter autoSecurityFilter) { Config config = ConfigProvider.getConfig(); this.alwaysRunFilter = config.getOptionalValue("quarkus.smallrye-openapi.always-run-filter", Boolean.class) .orElse(Boolean.FALSE); if (alwaysRunFilter) { - this.documentHolder = new DynamicDocument(config); + this.documentHolder = new DynamicDocument(config, autoSecurityFilter); } else { - this.documentHolder = new StaticDocument(config); + this.documentHolder = new StaticDocument(config, autoSecurityFilter); } } @@ -59,7 +57,7 @@ class StaticDocument implements OpenApiDocumentHolder { private byte[] jsonDocument; private byte[] yamlDocument; - StaticDocument(Config config) { + StaticDocument(Config config, OASFilter autoFilter) { ClassLoader cl = OpenApiConstants.classLoader == null ? Thread.currentThread().getContextClassLoader() : OpenApiConstants.classLoader; try (InputStream is = cl.getResourceAsStream(OpenApiConstants.BASE_NAME + Format.JSON)) { @@ -72,6 +70,9 @@ class StaticDocument implements OpenApiDocumentHolder { document.reset(); document.config(openApiConfig); document.modelFromStaticFile(OpenApiProcessor.modelFromStaticFile(staticFile)); + if (autoFilter != null) { + document.filter(autoFilter); + } document.filter(OpenApiProcessor.getFilter(openApiConfig, cl)); document.initialize(); @@ -104,9 +105,10 @@ class DynamicDocument implements OpenApiDocumentHolder { private OpenAPI generatedOnBuild; private OpenApiConfig openApiConfig; - private OASFilter filter; + private OASFilter userFilter; + private OASFilter autoFilter; - DynamicDocument(Config config) { + DynamicDocument(Config config, OASFilter autoFilter) { ClassLoader cl = OpenApiConstants.classLoader == null ? Thread.currentThread().getContextClassLoader() : OpenApiConstants.classLoader; try (InputStream is = cl.getResourceAsStream(OpenApiConstants.BASE_NAME + Format.JSON)) { @@ -114,7 +116,8 @@ class DynamicDocument implements OpenApiDocumentHolder { try (OpenApiStaticFile staticFile = new OpenApiStaticFile(is, Format.JSON)) { this.generatedOnBuild = OpenApiProcessor.modelFromStaticFile(staticFile); this.openApiConfig = new OpenApiConfigImpl(config); - this.filter = OpenApiProcessor.getFilter(openApiConfig, cl); + this.userFilter = OpenApiProcessor.getFilter(openApiConfig, cl); + this.autoFilter = autoFilter; } } } catch (IOException ex) { @@ -153,7 +156,10 @@ private OpenApiDocument getOpenApiDocument() { document.reset(); document.config(this.openApiConfig); document.modelFromStaticFile(this.generatedOnBuild); - document.filter(this.filter); + if (this.autoFilter != null) { + document.filter(this.autoFilter); + } + document.filter(this.userFilter); document.initialize(); return document; } diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiHandler.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiHandler.java index 9be329c3afea77..c226b42b47fc3b 100644 --- a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiHandler.java +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiHandler.java @@ -2,6 +2,8 @@ import java.util.List; +import org.eclipse.microprofile.openapi.OASFilter; + import io.quarkus.arc.Arc; import io.smallrye.openapi.runtime.io.Format; import io.vertx.core.Handler; @@ -32,9 +34,11 @@ public class OpenApiHandler implements Handler { } final boolean corsEnabled; + final OASFilter autoSecurityFilter; - public OpenApiHandler(boolean corsEnabled) { + public OpenApiHandler(boolean corsEnabled, OASFilter autoSecurityFilter) { this.corsEnabled = corsEnabled; + this.autoSecurityFilter = autoSecurityFilter; } @Override @@ -77,6 +81,7 @@ public void handle(RoutingContext event) { private OpenApiDocumentService getOpenApiDocumentService() { if (this.openApiDocumentService == null) { this.openApiDocumentService = Arc.container().instance(OpenApiDocumentService.class).get(); + this.openApiDocumentService.init(this.autoSecurityFilter); } return this.openApiDocumentService; } diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiRecorder.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiRecorder.java index be5250c3b0075e..3d3e3c196ee282 100644 --- a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiRecorder.java +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/OpenApiRecorder.java @@ -5,6 +5,7 @@ import java.net.URL; import java.util.Enumeration; +import org.eclipse.microprofile.openapi.OASFilter; import org.eclipse.microprofile.openapi.spi.OASFactoryResolver; import io.quarkus.runtime.ShutdownContext; @@ -16,9 +17,10 @@ @Recorder public class OpenApiRecorder { - public Handler handler(OpenApiRuntimeConfig runtimeConfig, HttpConfiguration configuration) { + public Handler handler(OpenApiRuntimeConfig runtimeConfig, HttpConfiguration configuration, + OASFilter autoSecurityFilter) { if (runtimeConfig.enable) { - return new OpenApiHandler(configuration.corsEnabled); + return new OpenApiHandler(configuration.corsEnabled, autoSecurityFilter); } else { return new OpenApiNotFoundHandler(); } diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoBasicSecurityFilter.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoBasicSecurityFilter.java new file mode 100644 index 00000000000000..fc1850052c0504 --- /dev/null +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoBasicSecurityFilter.java @@ -0,0 +1,37 @@ +package io.quarkus.smallrye.openapi.runtime.filter; + +import org.eclipse.microprofile.openapi.OASFactory; +import org.eclipse.microprofile.openapi.models.security.SecurityScheme; + +/** + * Add Basic Authentication security automatically based on the added security extensions + */ +public class AutoBasicSecurityFilter extends AutoSecurityFilter { + private String basicSecuritySchemeValue; + + public AutoBasicSecurityFilter() { + super(); + } + + public AutoBasicSecurityFilter(String securitySchemeName, String securitySchemeDescription, + String basicSecuritySchemeValue) { + super(securitySchemeName, securitySchemeDescription); + this.basicSecuritySchemeValue = basicSecuritySchemeValue; + } + + public String getBasicSecuritySchemeValue() { + return basicSecuritySchemeValue; + } + + public void setBasicSecuritySchemeValue(String basicSecuritySchemeValue) { + this.basicSecuritySchemeValue = basicSecuritySchemeValue; + } + + @Override + protected SecurityScheme getSecurityScheme() { + SecurityScheme securityScheme = OASFactory.createSecurityScheme(); + securityScheme.setType(SecurityScheme.Type.HTTP); + securityScheme.setScheme(basicSecuritySchemeValue); + return securityScheme; + } +} \ No newline at end of file diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoJWTSecurityFilter.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoJWTSecurityFilter.java new file mode 100644 index 00000000000000..c246fce55b844a --- /dev/null +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoJWTSecurityFilter.java @@ -0,0 +1,48 @@ +package io.quarkus.smallrye.openapi.runtime.filter; + +import org.eclipse.microprofile.openapi.OASFactory; +import org.eclipse.microprofile.openapi.models.security.SecurityScheme; + +/** + * Add JWT Authentication security automatically based on the added security extensions + */ +public class AutoJWTSecurityFilter extends AutoSecurityFilter { + private String jwtSecuritySchemeValue; + private String jwtBearerFormat; + + public AutoJWTSecurityFilter() { + super(); + } + + public AutoJWTSecurityFilter(String securitySchemeName, String securitySchemeDescription, String jwtSecuritySchemeValue, + String jwtBearerFormat) { + super(securitySchemeName, securitySchemeDescription); + this.jwtSecuritySchemeValue = jwtSecuritySchemeValue; + this.jwtBearerFormat = jwtBearerFormat; + } + + public String getJwtSecuritySchemeValue() { + return jwtSecuritySchemeValue; + } + + public void setJwtSecuritySchemeValue(String jwtSecuritySchemeValue) { + this.jwtSecuritySchemeValue = jwtSecuritySchemeValue; + } + + public String getJwtBearerFormat() { + return jwtBearerFormat; + } + + public void setJwtBearerFormat(String jwtBearerFormat) { + this.jwtBearerFormat = jwtBearerFormat; + } + + @Override + protected SecurityScheme getSecurityScheme() { + SecurityScheme securityScheme = OASFactory.createSecurityScheme(); + securityScheme.setType(SecurityScheme.Type.HTTP); + securityScheme.setScheme(jwtSecuritySchemeValue); + securityScheme.setBearerFormat(jwtBearerFormat); + return securityScheme; + } +} diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoSecurityFilter.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoSecurityFilter.java new file mode 100644 index 00000000000000..00f4cd8c597cfb --- /dev/null +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoSecurityFilter.java @@ -0,0 +1,82 @@ +package io.quarkus.smallrye.openapi.runtime.filter; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.openapi.OASFactory; +import org.eclipse.microprofile.openapi.OASFilter; +import org.eclipse.microprofile.openapi.models.OpenAPI; +import org.eclipse.microprofile.openapi.models.security.SecurityScheme; +import org.jboss.logging.Logger; + +/** + * Auto add security + */ +public abstract class AutoSecurityFilter implements OASFilter { + private static final Logger log = Logger.getLogger(AutoSecurityFilter.class); + + private String securitySchemeName; + private String securitySchemeDescription; + + public AutoSecurityFilter() { + + } + + public AutoSecurityFilter(String securitySchemeName, String securitySchemeDescription) { + this.securitySchemeName = securitySchemeName; + this.securitySchemeDescription = securitySchemeDescription; + } + + public String getSecuritySchemeName() { + return securitySchemeName; + } + + public void setSecuritySchemeName(String securitySchemeName) { + this.securitySchemeName = securitySchemeName; + } + + public String getSecuritySchemeDescription() { + return securitySchemeDescription; + } + + public void setSecuritySchemeDescription(String securitySchemeDescription) { + this.securitySchemeDescription = securitySchemeDescription; + } + + @Override + public void filterOpenAPI(OpenAPI openAPI) { + // Make sure components are created + if (openAPI.getComponents() == null) { + openAPI.setComponents(OASFactory.createComponents()); + } + + Map securitySchemes = new HashMap<>(); + + // Add any existing security + if (openAPI.getComponents().getSecuritySchemes() != null + && !openAPI.getComponents().getSecuritySchemes().isEmpty()) { + securitySchemes.putAll(openAPI.getComponents().getSecuritySchemes()); + } + + SecurityScheme securityScheme = getSecurityScheme(); + securityScheme.setDescription(securitySchemeDescription); + securitySchemes.put(securitySchemeName, securityScheme); + openAPI.getComponents().setSecuritySchemes(securitySchemes); + } + + protected abstract SecurityScheme getSecurityScheme(); + + protected String getUrl(String configKey, String defaultValue, String shouldEndWith) { + Config c = ConfigProvider.getConfig(); + + String u = c.getOptionalValue(configKey, String.class).orElse(defaultValue); + + if (u != null && !u.endsWith(shouldEndWith)) { + u = u + shouldEndWith; + } + return u; + } + +} \ No newline at end of file diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoUrl.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoUrl.java new file mode 100644 index 00000000000000..8ee97b65c624d6 --- /dev/null +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/AutoUrl.java @@ -0,0 +1,60 @@ +package io.quarkus.smallrye.openapi.runtime.filter; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + +/** + * Create a URL from a config, or a default value + */ +public class AutoUrl { + + private String defaultValue; + private String configKey; + private String path; + + public AutoUrl() { + } + + public AutoUrl(String defaultValue, String configKey, String path) { + this.defaultValue = defaultValue; + this.configKey = configKey; + this.path = path; + } + + public String getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + + public String getConfigKey() { + return configKey; + } + + public void setConfigKey(String configKey) { + this.configKey = configKey; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getFinalUrlValue() { + Config c = ConfigProvider.getConfig(); + + String u = c.getOptionalValue(this.configKey, String.class).orElse(defaultValue); + + if (u != null && path != null && !u.endsWith(path)) { + u = u + path; + } + + return u; + } + +} diff --git a/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/OpenIDConnectSecurityFilter.java b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/OpenIDConnectSecurityFilter.java new file mode 100644 index 00000000000000..cbc18a5c320b73 --- /dev/null +++ b/extensions/smallrye-openapi/runtime/src/main/java/io/quarkus/smallrye/openapi/runtime/filter/OpenIDConnectSecurityFilter.java @@ -0,0 +1,71 @@ +package io.quarkus.smallrye.openapi.runtime.filter; + +import org.eclipse.microprofile.openapi.OASFactory; +import org.eclipse.microprofile.openapi.models.security.OAuthFlow; +import org.eclipse.microprofile.openapi.models.security.OAuthFlows; +import org.eclipse.microprofile.openapi.models.security.SecurityScheme; + +/** + * Add OAuth 2 (Implicit) Authentication security automatically based on the added security extensions + */ +public class OpenIDConnectSecurityFilter extends AutoSecurityFilter { + + private AutoUrl authorizationUrl; + private AutoUrl refreshUrl; + private AutoUrl tokenUrl; + + public OpenIDConnectSecurityFilter() { + super(); + } + + public OpenIDConnectSecurityFilter(String securitySchemeName, String securitySchemeDescription, + AutoUrl authorizationUrl, + AutoUrl refreshUrl, + AutoUrl tokenUrl) { + super(securitySchemeName, securitySchemeDescription); + this.authorizationUrl = authorizationUrl; + this.refreshUrl = refreshUrl; + this.tokenUrl = tokenUrl; + } + + public AutoUrl getAuthorizationUrl() { + return authorizationUrl; + } + + public void setAuthorizationUrl(AutoUrl authorizationUrl) { + this.authorizationUrl = authorizationUrl; + } + + public AutoUrl getRefreshUrl() { + return refreshUrl; + } + + public void setRefreshUrl(AutoUrl refreshUrl) { + this.refreshUrl = refreshUrl; + } + + public AutoUrl getTokenUrl() { + return tokenUrl; + } + + public void setTokenUrl(AutoUrl tokenUrl) { + this.tokenUrl = tokenUrl; + } + + @Override + protected SecurityScheme getSecurityScheme() { + SecurityScheme securityScheme = OASFactory.createSecurityScheme(); + + securityScheme.setType(SecurityScheme.Type.OAUTH2); + OAuthFlows oAuthFlows = OASFactory.createOAuthFlows(); + OAuthFlow oAuthFlow = OASFactory.createOAuthFlow(); + oAuthFlow.authorizationUrl(this.authorizationUrl.getFinalUrlValue()); + oAuthFlow.refreshUrl(this.refreshUrl.getFinalUrlValue()); + oAuthFlow.tokenUrl(this.tokenUrl.getFinalUrlValue()); + oAuthFlows.setImplicit(oAuthFlow); + securityScheme.setFlows(oAuthFlows); + + return securityScheme; + } + +} \ No newline at end of file diff --git a/extensions/smallrye-reactive-messaging-amqp/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/amqp/deployment/AmqpDevServicesProcessor.java b/extensions/smallrye-reactive-messaging-amqp/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/amqp/deployment/AmqpDevServicesProcessor.java index ca48170751f0f8..35483f64c40947 100644 --- a/extensions/smallrye-reactive-messaging-amqp/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/amqp/deployment/AmqpDevServicesProcessor.java +++ b/extensions/smallrye-reactive-messaging-amqp/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/amqp/deployment/AmqpDevServicesProcessor.java @@ -2,6 +2,7 @@ import java.io.Closeable; import java.util.Objects; +import java.util.Optional; import java.util.function.Supplier; import org.eclipse.microprofile.config.Config; @@ -19,7 +20,10 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesConfigResultBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.console.ConsoleInstalledBuildItem; +import io.quarkus.deployment.console.StartupLogCompressor; import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.deployment.logging.LoggingSetupBuildItem; import io.quarkus.devservices.common.ContainerLocator; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.configuration.ConfigUtils; @@ -60,7 +64,9 @@ public class AmqpDevServicesProcessor { public DevServicesAmqpBrokerBuildItem startAmqpDevService( LaunchModeBuildItem launchMode, AmqpBuildTimeConfig amqpClientBuildTimeConfig, - BuildProducer devServicePropertiesProducer) { + BuildProducer devServicePropertiesProducer, + Optional consoleInstalledBuildItem, + LoggingSetupBuildItem loggingSetupBuildItem) { AmqpDevServiceCfg configuration = getConfiguration(amqpClientBuildTimeConfig); @@ -73,25 +79,35 @@ public DevServicesAmqpBrokerBuildItem startAmqpDevService( cfg = null; } - AmqpBroker broker = startAmqpBroker(configuration, launchMode); + AmqpBroker broker; DevServicesAmqpBrokerBuildItem artemis = null; - if (broker != null) { - closeable = broker.getCloseable(); - devServicePropertiesProducer.produce(new DevServicesConfigResultBuildItem(AMQP_HOST_PROP, broker.host)); - devServicePropertiesProducer - .produce(new DevServicesConfigResultBuildItem(AMQP_PORT_PROP, Integer.toString(broker.port))); - devServicePropertiesProducer.produce(new DevServicesConfigResultBuildItem(AMQP_USER_PROP, broker.user)); - devServicePropertiesProducer.produce(new DevServicesConfigResultBuildItem(AMQP_PASSWORD_PROP, broker.password)); - - artemis = new DevServicesAmqpBrokerBuildItem(broker.host, broker.port, broker.user, broker.password); - - if (broker.isOwner()) { - log.info("Dev Services for AMQP started."); - log.infof("Other Quarkus applications in dev mode will find the " - + "broker automatically. For Quarkus applications in production mode, you can connect to" - + " this by starting your application with -Damqp.host=%s -Damqp.port=%d -Damqp.user=%s -Damqp.password=%s", - broker.host, broker.port, broker.user, broker.password); + StartupLogCompressor compressor = new StartupLogCompressor( + (launchMode.isTest() ? "(test) " : "") + "AMQP Dev Services Starting:", consoleInstalledBuildItem, + loggingSetupBuildItem); + try { + broker = startAmqpBroker(configuration, launchMode); + if (broker != null) { + closeable = broker.getCloseable(); + devServicePropertiesProducer.produce(new DevServicesConfigResultBuildItem(AMQP_HOST_PROP, broker.host)); + devServicePropertiesProducer + .produce(new DevServicesConfigResultBuildItem(AMQP_PORT_PROP, Integer.toString(broker.port))); + devServicePropertiesProducer.produce(new DevServicesConfigResultBuildItem(AMQP_USER_PROP, broker.user)); + devServicePropertiesProducer.produce(new DevServicesConfigResultBuildItem(AMQP_PASSWORD_PROP, broker.password)); + + artemis = new DevServicesAmqpBrokerBuildItem(broker.host, broker.port, broker.user, broker.password); + + if (broker.isOwner()) { + log.info("Dev Services for AMQP started."); + log.infof("Other Quarkus applications in dev mode will find the " + + "broker automatically. For Quarkus applications in production mode, you can connect to" + + " this by starting your application with -Damqp.host=%s -Damqp.port=%d -Damqp.user=%s -Damqp.password=%s", + broker.host, broker.port, broker.user, broker.password); + } } + compressor.close(); + } catch (Throwable t) { + compressor.closeAndDumpCaptured(); + throw new RuntimeException(t); } // Configure the watch dog diff --git a/extensions/smallrye-reactive-messaging-amqp/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/smallrye-reactive-messaging-amqp/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 8558a6b2a48770..977c0d3ae2981b 100644 --- a/extensions/smallrye-reactive-messaging-amqp/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/smallrye-reactive-messaging-amqp/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -9,7 +9,7 @@ metadata: guide: "https://quarkus.io/guides/amqp" categories: - "messaging" - status: "preview" + status: "stable" config: - "mp.messaging." - "quarkus.reactive-messaging." diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/pom.xml b/extensions/smallrye-reactive-messaging-kafka/deployment/pom.xml index bcde6e700bec72..8d8bff7aceca62 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/pom.xml +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/pom.xml @@ -83,12 +83,6 @@ awaitility test - - io.quarkus - quarkus-vertx-http-deployment - test-jar - test - diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/testing/KafkaDevServicesContinuousTestingTestCase.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/testing/KafkaDevServicesContinuousTestingTestCase.java index e4a232da8146b0..8387a6674986f5 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/testing/KafkaDevServicesContinuousTestingTestCase.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/testing/KafkaDevServicesContinuousTestingTestCase.java @@ -9,8 +9,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.test.ContinuousTestingTestUtils; import io.quarkus.test.QuarkusDevModeTest; -import io.quarkus.vertx.http.testrunner.ContinuousTestingTestUtils; public class KafkaDevServicesContinuousTestingTestCase { diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/testing/KafkaDevServicesContinuousTestingWorkingAppPropsTestCase.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/testing/KafkaDevServicesContinuousTestingWorkingAppPropsTestCase.java index e4fdcca5abe1a2..57c98b27fdf751 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/testing/KafkaDevServicesContinuousTestingWorkingAppPropsTestCase.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/testing/KafkaDevServicesContinuousTestingWorkingAppPropsTestCase.java @@ -9,8 +9,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.test.ContinuousTestingTestUtils; import io.quarkus.test.QuarkusDevModeTest; -import io.quarkus.vertx.http.testrunner.ContinuousTestingTestUtils; public class KafkaDevServicesContinuousTestingWorkingAppPropsTestCase { diff --git a/extensions/spring-data-rest/deployment/pom.xml b/extensions/spring-data-rest/deployment/pom.xml index e3abeaec086897..cab1280cca1534 100644 --- a/extensions/spring-data-rest/deployment/pom.xml +++ b/extensions/spring-data-rest/deployment/pom.xml @@ -38,7 +38,7 @@ io.quarkus - quarkus-resteasy-jsonb-deployment + quarkus-resteasy-reactive-jsonb-deployment test diff --git a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java index 52c7b52114d217..16037fb3ef9428 100644 --- a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java +++ b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java @@ -7,6 +7,8 @@ import java.util.LinkedList; import java.util.List; +import javax.ws.rs.Priorities; + import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; @@ -24,10 +26,11 @@ import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.gizmo.ClassOutput; +import io.quarkus.rest.data.panache.RestDataPanacheException; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.RestDataResourceBuildItem; import io.quarkus.rest.data.panache.deployment.properties.ResourcePropertiesBuildItem; -import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; +import io.quarkus.resteasy.reactive.spi.ExceptionMapperBuildItem; import io.quarkus.spring.data.rest.deployment.crud.CrudMethodsImplementor; import io.quarkus.spring.data.rest.deployment.crud.CrudPropertiesProvider; import io.quarkus.spring.data.rest.deployment.paging.PagingAndSortingMethodsImplementor; @@ -55,8 +58,9 @@ FeatureBuildItem feature() { } @BuildStep - ResteasyJaxrsProviderBuildItem registerRestDataPanacheExceptionMapper() { - return new ResteasyJaxrsProviderBuildItem(RestDataPanacheExceptionMapper.class.getName()); + ExceptionMapperBuildItem registerRestDataPanacheExceptionMapper() { + return new ExceptionMapperBuildItem(RestDataPanacheExceptionMapper.class.getName(), + RestDataPanacheException.class.getName(), Priorities.USER + 100, false); } @BuildStep diff --git a/extensions/spring-data-rest/runtime/pom.xml b/extensions/spring-data-rest/runtime/pom.xml index f1c159c2fb303e..e812673dd80d45 100644 --- a/extensions/spring-data-rest/runtime/pom.xml +++ b/extensions/spring-data-rest/runtime/pom.xml @@ -27,6 +27,10 @@ io.quarkus quarkus-spring-data-rest-api + + jakarta.validation + jakarta.validation-api + diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java index 730b858eea0931..32f006d18ccc35 100644 --- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java @@ -523,7 +523,11 @@ public void run() { } public void addServletContextAttribute(RuntimeValue deployment, String key, Object value1) { - deployment.getValue().addServletContextAttribute(key, value1); + if (value1 instanceof RuntimeValue) { + deployment.getValue().addServletContextAttribute(key, ((RuntimeValue) value1).getValue()); + } else { + deployment.getValue().addServletContextAttribute(key, value1); + } } public void addServletExtension(RuntimeValue deployment, ServletExtension extension) { diff --git a/extensions/vault/deployment/src/main/java/io/quarkus/vault/deployment/DevServicesVaultProcessor.java b/extensions/vault/deployment/src/main/java/io/quarkus/vault/deployment/DevServicesVaultProcessor.java index 0d897b7f310753..d65562542a50ef 100644 --- a/extensions/vault/deployment/src/main/java/io/quarkus/vault/deployment/DevServicesVaultProcessor.java +++ b/extensions/vault/deployment/src/main/java/io/quarkus/vault/deployment/DevServicesVaultProcessor.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.OptionalInt; import org.apache.commons.lang3.RandomStringUtils; @@ -18,7 +19,11 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.DevServicesConfigResultBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.console.ConsoleInstalledBuildItem; +import io.quarkus.deployment.console.StartupLogCompressor; import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.deployment.logging.LoggingSetupBuildItem; import io.quarkus.runtime.configuration.ConfigUtils; import io.quarkus.vault.runtime.VaultVersions; import io.quarkus.vault.runtime.config.DevServicesConfig; @@ -38,7 +43,10 @@ public class DevServicesVaultProcessor { private final IsDockerWorking isDockerWorking = new IsDockerWorking(true); @BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) - public void startVaultContainers(BuildProducer devConfig, VaultBuildTimeConfig config) { + public void startVaultContainers(BuildProducer devConfig, VaultBuildTimeConfig config, + Optional consoleInstalledBuildItem, + LaunchModeBuildItem launchMode, + LoggingSetupBuildItem loggingSetupBuildItem) { DevServicesConfig currentDevServicesConfiguration = config.devservices; @@ -62,7 +70,18 @@ public void startVaultContainers(BuildProducer capturedDevServicesConfiguration = currentDevServicesConfiguration; - StartResult startResult = startContainer(currentDevServicesConfiguration); + StartResult startResult; + + StartupLogCompressor compressor = new StartupLogCompressor( + (launchMode.isTest() ? "(test) " : "") + "Vault Dev Services Starting:", consoleInstalledBuildItem, + loggingSetupBuildItem); + try { + startResult = startContainer(currentDevServicesConfiguration); + compressor.close(); + } catch (Throwable t) { + compressor.closeAndDumpCaptured(); + throw new RuntimeException(t); + } if (startResult == null) { return; } diff --git a/extensions/vertx-http/deployment/pom.xml b/extensions/vertx-http/deployment/pom.xml index daa32430e8a055..ed795df314e552 100644 --- a/extensions/vertx-http/deployment/pom.xml +++ b/extensions/vertx-http/deployment/pom.xml @@ -119,17 +119,6 @@ - - org.apache.maven.plugins - maven-jar-plugin - - - - test-jar - - - - maven-compiler-plugin diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java index e6a7e0d54c6c51..a7b7956bf0068a 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java @@ -89,7 +89,8 @@ SyntheticBeanBuildItem initMtlsClientAuth( @Record(ExecutionTime.RUNTIME_INIT) SyntheticBeanBuildItem initBasicAuth( HttpSecurityRecorder recorder, - HttpBuildTimeConfig buildTimeConfig) { + HttpBuildTimeConfig buildTimeConfig, + BuildProducer securityInformationProducer) { if ((buildTimeConfig.auth.form.enabled || isMtlsClientAuthenticationEnabled(buildTimeConfig)) && !buildTimeConfig.auth.basic) { //if form auth is enabled and we are not then we don't install @@ -105,6 +106,7 @@ SyntheticBeanBuildItem initBasicAuth( && !buildTimeConfig.auth.basic) { //if not explicitly enabled we make this a default bean, so it is the fallback if nothing else is defined configurator.defaultBean(); + securityInformationProducer.produce(SecurityInformationBuildItem.BASIC()); } return configurator.done(); @@ -119,7 +121,8 @@ void setupAuthenticationMechanisms( Capabilities capabilities, BuildProducer beanContainerListenerBuildItemBuildProducer, HttpBuildTimeConfig buildTimeConfig, - List httpSecurityPolicyBuildItemList) { + List httpSecurityPolicyBuildItemList, + BuildProducer securityInformationProducer) { Map> policyMap = new HashMap<>(); for (HttpSecurityPolicyBuildItem e : httpSecurityPolicyBuildItemList) { if (policyMap.containsKey(e.getName())) { @@ -131,6 +134,7 @@ void setupAuthenticationMechanisms( if (buildTimeConfig.auth.form.enabled) { } else if (buildTimeConfig.auth.basic) { beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(BasicAuthenticationMechanism.class)); + securityInformationProducer.produce(SecurityInformationBuildItem.BASIC()); } if (capabilities.isPresent(Capability.SECURITY)) { diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RequireVirtualHttpBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RequireVirtualHttpBuildItem.java index 7e0046c2d1f541..a6f43103b6b912 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RequireVirtualHttpBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/RequireVirtualHttpBuildItem.java @@ -6,5 +6,16 @@ * Marker class to turn on virtual http channel */ public final class RequireVirtualHttpBuildItem extends SimpleBuildItem { - public static final RequireVirtualHttpBuildItem MARKER = new RequireVirtualHttpBuildItem(); + public static final RequireVirtualHttpBuildItem MARKER = new RequireVirtualHttpBuildItem(false); + public static final RequireVirtualHttpBuildItem ALWAYS_VIRTUAL = new RequireVirtualHttpBuildItem(true); + + private boolean alwaysVirtual; + + private RequireVirtualHttpBuildItem(boolean alwaysVirtual) { + this.alwaysVirtual = alwaysVirtual; + } + + public boolean isAlwaysVirtual() { + return alwaysVirtual; + } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/SecurityInformationBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/SecurityInformationBuildItem.java new file mode 100644 index 00000000000000..f1b6c64aec8a43 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/SecurityInformationBuildItem.java @@ -0,0 +1,59 @@ +package io.quarkus.vertx.http.deployment; + +import java.util.Optional; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Contains information on the security model used in the application + */ +public final class SecurityInformationBuildItem extends MultiBuildItem { + + private final SecurityModel securityModel; + private final Optional openIDConnectInformation; + + public static SecurityInformationBuildItem BASIC() { + return new SecurityInformationBuildItem(SecurityModel.basic, Optional.empty()); + } + + public static SecurityInformationBuildItem JWT() { + return new SecurityInformationBuildItem(SecurityModel.jwt, Optional.empty()); + } + + public static SecurityInformationBuildItem OPENIDCONNECT(String urlConfigKey) { + return new SecurityInformationBuildItem(SecurityModel.oidc, + Optional.of(new OpenIDConnectInformation(urlConfigKey))); + } + + public SecurityInformationBuildItem(SecurityModel securityModel, + Optional openIDConnectInformation) { + this.securityModel = securityModel; + this.openIDConnectInformation = openIDConnectInformation; + } + + public SecurityModel getSecurityModel() { + return securityModel; + } + + public Optional getOpenIDConnectInformation() { + return openIDConnectInformation; + } + + public enum SecurityModel { + basic, + jwt, + oidc + } + + public static class OpenIDConnectInformation { + private final String urlConfigKey; + + public OpenIDConnectInformation(String urlConfigKey) { + this.urlConfigKey = urlConfigKey; + } + + public String getUrlConfigKey() { + return urlConfigKey; + } + } +} \ No newline at end of file diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java index a1ef76ed0c7c73..08f032956fb343 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java @@ -290,8 +290,8 @@ void openSocket(ApplicationStartBuildItem start, reflectiveClass .produce(new ReflectiveClassBuildItem(true, false, false, VirtualServerChannel.class)); } - // start http socket in dev/test mode even if virtual http is required - boolean startSocket = !startVirtual || launchMode.getLaunchMode() != LaunchMode.NORMAL; + boolean startSocket = (!startVirtual || launchMode.getLaunchMode() != LaunchMode.NORMAL) + && (requireVirtual.isEmpty() || !requireVirtual.get().isAlwaysVirtual()); recorder.startServer(vertx.getVertx(), shutdown, httpBuildTimeConfig, httpConfiguration, launchMode.getLaunchMode(), startVirtual, startSocket, eventLoopCount.getEventLoopCount(), diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfigEditorProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfigEditorProcessor.java index 1d963b996fd4d1..a793dc8ab3cd64 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfigEditorProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfigEditorProcessor.java @@ -7,6 +7,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -45,14 +46,19 @@ public void config(BuildProducer devCons Optional devServicesLauncherConfig) { List configDescriptions = new ArrayList<>(); for (ConfigDescriptionBuildItem item : configDescriptionBuildItems) { - configDescriptions.add( - new ConfigDescription(item.getPropertyName(), item.getDocs(), item.getDefaultValue(), - isSetByDevServices(devServicesLauncherConfig, item.getPropertyName()))); + new ConfigDescription(item.getPropertyName(), + item.getDocs(), + item.getDefaultValue(), + isSetByDevServices(devServicesLauncherConfig, item.getPropertyName()), + item.getValueTypeName(), + item.getAllowedValues(), + item.getConfigPhase().name())); } - devConsoleRuntimeTemplateProducer.produce( - new DevConsoleRuntimeTemplateInfoBuildItem("config", new ConfigDescriptionsSupplier(configDescriptions))); + devConsoleRuntimeTemplateProducer.produce(new DevConsoleRuntimeTemplateInfoBuildItem("config", + new ConfigDescriptionsSupplier(configDescriptions))); + devConsoleRuntimeTemplateProducer.produce(new DevConsoleRuntimeTemplateInfoBuildItem("hasDevServices", new HasDevServicesSupplier(devServicesLauncherConfig.isPresent() && devServicesLauncherConfig.get().getConfig() != null @@ -75,22 +81,13 @@ protected void handlePost(RoutingContext event, MultiMap form) throws Exception Map values = Collections.singletonMap(name, value); updateConfig(values); - } else if (action.equals("copyTestDevServices") && devServicesLauncherConfig.isPresent()) { + } else if (action.equals("copyDevServices") && devServicesLauncherConfig.isPresent()) { + String environment = event.request().getFormAttribute("environment"); + String filter = event.request().getParam("filterConfigKeys"); + List configFilter = getConfigFilter(filter); Map autoconfig = devServicesLauncherConfig.get().getConfig(); - autoconfig = autoconfig.entrySet().stream() - .collect(Collectors.toMap( - e -> appendProfile("test", e.getKey()), - Map.Entry::getValue)); - - updateConfig(autoconfig); - } else if (action.equals("copyProdDevServices") && devServicesLauncherConfig.isPresent()) { - Map autoconfig = devServicesLauncherConfig.get().getConfig(); - - autoconfig = autoconfig.entrySet().stream() - .collect(Collectors.toMap( - e -> appendProfile("prod", e.getKey()), - Map.Entry::getValue)); + autoconfig = filterAndApplyProfile(autoconfig, configFilter, environment.toLowerCase()); updateConfig(autoconfig); } else if (action.equals("updateProperties")) { @@ -106,6 +103,37 @@ protected void handlePost(RoutingContext event, MultiMap form) throws Exception })); } + private Map filterAndApplyProfile(Map autoconfig, List configFilter, + String profile) { + return autoconfig.entrySet().stream() + .filter((t) -> { + if (configFilter != null && !configFilter.isEmpty()) { + for (String sw : configFilter) { + if (t.getKey().startsWith(sw)) { + return true; + } + } + } else { + return true; + } + return false; + }) + .collect(Collectors.toMap( + e -> appendProfile(profile, e.getKey()), + Map.Entry::getValue)); + } + + private List getConfigFilter(String filter) { + if (filter != null && !filter.isEmpty()) { + if (filter.contains(",")) { + return Arrays.asList(filter.split(",")); + } else { + return List.of(filter); + } + } + return Collections.EMPTY_LIST; + } + private String appendProfile(String profile, String originalKey) { return String.format("%%%s.%s", profile, originalKey); } @@ -132,45 +160,47 @@ static byte[] getConfig() { } static void updateConfig(Map values) { - try { - Path configPath = getConfigPath(); - String profile = ProfileManager.getActiveProfile(); - List lines = Files.readAllLines(configPath); - for (Map.Entry entry : values.entrySet()) { - String name = entry.getKey(); - String value = entry.getValue(); - name = !profile.equals(DEVELOPMENT.getDefaultProfile()) ? "%" + profile + "." + name : name; - int nameLine = -1; - for (int i = 0, linesSize = lines.size(); i < linesSize; i++) { - final String line = lines.get(i); - if (line.startsWith(name + "=")) { - nameLine = i; - break; + if (values != null && !values.isEmpty()) { + try { + Path configPath = getConfigPath(); + String profile = ProfileManager.getActiveProfile(); + List lines = Files.readAllLines(configPath); + for (Map.Entry entry : values.entrySet()) { + String name = entry.getKey(); + String value = entry.getValue(); + name = !profile.equals(DEVELOPMENT.getDefaultProfile()) ? "%" + profile + "." + name : name; + int nameLine = -1; + for (int i = 0, linesSize = lines.size(); i < linesSize; i++) { + final String line = lines.get(i); + if (line.startsWith(name + "=")) { + nameLine = i; + break; + } } - } - if (nameLine != -1) { - if (value.isEmpty()) { - lines.remove(nameLine); + if (nameLine != -1) { + if (value.isEmpty()) { + lines.remove(nameLine); + } else { + lines.set(nameLine, name + "=" + value); + } } else { - lines.set(nameLine, name + "=" + value); - } - } else { - if (!value.isEmpty()) { - lines.add(name + "=" + value); + if (!value.isEmpty()) { + lines.add(name + "=" + value); + } } } - } - try (BufferedWriter writer = Files.newBufferedWriter(configPath)) { - for (String i : lines) { - writer.write(i); - writer.newLine(); + try (BufferedWriter writer = Files.newBufferedWriter(configPath)) { + for (String i : lines) { + writer.write(i); + writer.newLine(); + } } + preventKill(); + } catch (Throwable t) { + throw new RuntimeException(t); } - preventKill(); - } catch (Throwable t) { - throw new RuntimeException(t); } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ContinuousTestingWebSocketListener.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ContinuousTestingWebSocketListener.java deleted file mode 100644 index 703dfe9514f448..00000000000000 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ContinuousTestingWebSocketListener.java +++ /dev/null @@ -1,95 +0,0 @@ -package io.quarkus.vertx.http.deployment.devmode.console; - -import java.util.function.Consumer; - -import org.junit.platform.launcher.TestIdentifier; - -import io.quarkus.deployment.dev.testing.TestController; -import io.quarkus.deployment.dev.testing.TestListener; -import io.quarkus.deployment.dev.testing.TestResult; -import io.quarkus.deployment.dev.testing.TestRunListener; -import io.quarkus.deployment.dev.testing.TestRunResults; -import io.quarkus.dev.testing.ContinuousTestingWebsocketListener; - -public class ContinuousTestingWebSocketListener implements TestListener { - - @Override - public void listenerRegistered(TestController testController) { - - } - - @Override - public void testsEnabled() { - ContinuousTestingWebsocketListener - .setLastState( - new ContinuousTestingWebsocketListener.State(true, true, 0L, 0L, 0L, 0L, false, false, false, true)); - } - - @Override - public void testsDisabled() { - ContinuousTestingWebsocketListener.setRunning(false); - } - - @Override - public void testRunStarted(Consumer listenerConsumer) { - ContinuousTestingWebsocketListener.setInProgress(true); - listenerConsumer.accept(new TestRunListener() { - @Override - public void runStarted(long toRun) { - - } - - @Override - public void testComplete(TestResult result) { - - } - - @Override - public void runComplete(TestRunResults testRunResults) { - ContinuousTestingWebsocketListener.setLastState( - new ContinuousTestingWebsocketListener.State(true, false, - testRunResults.getPassedCount() + - testRunResults.getFailedCount() + - testRunResults.getSkippedCount(), - testRunResults.getPassedCount(), - testRunResults.getFailedCount(), testRunResults.getSkippedCount(), - ContinuousTestingWebsocketListener.getLastState().isBrokenOnly, - ContinuousTestingWebsocketListener.getLastState().isTestOutput, - ContinuousTestingWebsocketListener.getLastState().isInstrumentationBasedReload, - ContinuousTestingWebsocketListener.getLastState().isLiveReload)); - } - - @Override - public void runAborted() { - ContinuousTestingWebsocketListener.setInProgress(false); - } - - @Override - public void testStarted(TestIdentifier testIdentifier, String className) { - - } - }); - - } - - @Override - public void setBrokenOnly(boolean bo) { - ContinuousTestingWebsocketListener.setBrokenOnly(bo); - } - - @Override - public void setTestOutput(boolean to) { - ContinuousTestingWebsocketListener.setTestOutput(to); - } - - @Override - public void setInstrumentationBasedReload(boolean ibr) { - ContinuousTestingWebsocketListener.setInstrumentationBasedReload(ibr); - } - - @Override - public void setLiveReloadEnabled(boolean lre) { - ContinuousTestingWebsocketListener.setLiveReloadEnabled(lre); - } - -} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsole.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsole.java index e251597a536a1f..b7942c1ddd55bb 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsole.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsole.java @@ -175,9 +175,11 @@ public void sendMainPage(RoutingContext event) { Template simpleTemplate = engine.getTemplate(namespace + "/embedded.html"); boolean hasConsoleEntry = simpleTemplate != null; boolean hasGuide = metadata.containsKey("guide"); + boolean isUnlisted = metadata.containsKey("unlisted") + && (metadata.get("unlisted").equals(true) || metadata.get("unlisted").equals("true")); loaded.put("hasConsoleEntry", hasConsoleEntry); loaded.put("hasGuide", hasGuide); - if (hasConsoleEntry || hasGuide) { + if (!isUnlisted) { if (hasConsoleEntry) { Map data = new HashMap<>(); data.putAll(globalData); diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestsProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestsProcessor.java index fe0a98105eb8bc..8b58de97ce3ff5 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestsProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestsProcessor.java @@ -23,7 +23,6 @@ import io.quarkus.devconsole.spi.DevConsoleTemplateInfoBuildItem; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; -import io.quarkus.vertx.http.deployment.devmode.console.ContinuousTestingWebSocketListener; import io.quarkus.vertx.http.runtime.devmode.DevConsoleRecorder; import io.quarkus.vertx.http.runtime.devmode.Json; import io.vertx.core.Handler; @@ -61,7 +60,6 @@ public void setupTestRoutes( .route("dev/test") .handler(recorder.continousTestHandler(shutdownContextBuildItem)) .build()); - testListenerBuildItemBuildProducer.produce(new TestListenerBuildItem(new ContinuousTestingWebSocketListener())); } } diff --git a/extensions/vertx-http/deployment/src/main/resources/dev-static/js/logstream.js b/extensions/vertx-http/deployment/src/main/resources/dev-static/js/logstream.js index aa3e4cd1f74f29..86fcb0883c56d6 100644 --- a/extensions/vertx-http/deployment/src/main/resources/dev-static/js/logstream.js +++ b/extensions/vertx-http/deployment/src/main/resources/dev-static/js/logstream.js @@ -736,7 +736,7 @@ function populateLoggerLevelModal(loggerNamesArray, levelNamesArray){ tbodyLevels.append(row); } - $('select').on('change', function() { + $('.logleveldropdown').on('change', function() { changeLogLevel(this.value, $(this).find('option:selected').text()); }); @@ -764,7 +764,7 @@ function getTextClass(level){ function createDropdown(name, level, levelNamesArray){ - var dd = ""; // Populate the dropdown for (var i = 0; i < levelNamesArray.length; i++) { var selected = ""; diff --git a/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/config.html b/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/config.html index 8b7601c90d7cdd..43419922e74892 100644 --- a/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/config.html +++ b/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/config.html @@ -69,11 +69,32 @@ hideEmptyTables(); } -function changeInputValue(name){ +function changeSelectValue(element, name){ + var $el = $("select[id='" + name + "']"); + var $tr = $("tr[id='tr-" + name + "']"); + var value = element.options[element.selectedIndex].text; + + postChangeConfigValue(name, value, $el); +} + +function changeCheckboxValue(element, name){ var $el = $("input[id='" + name + "']"); var $tr = $("tr[id='tr-" + name + "']"); + var value = element.checked; + postChangeConfigValue(name, value, $el); +} + +function changeInputValue(name){ + var $el = $("input[id='" + name + "']"); + var $tr = $("tr[id='tr-" + name + "']"); var value = $el.val(); + + postChangeConfigValue(name, value, $el); +} + +function postChangeConfigValue(name, value, $el){ + $el.prop('disabled', true); $.post("", { action: "updateProperty", @@ -90,6 +111,7 @@ hideEmptyTables(); changeBackgroundColor("#ff6366", $el); } + $el.prop('disabled', false); }); } @@ -168,10 +190,11 @@ } function copyDevServices(environment){ - var action = "copy" + environment + "DevServices"; $.post("", { - action: action + action: "copyDevServices", + environment: environment, + filter: configfilter }, function(data, status){ if(status === "success"){ @@ -280,9 +303,22 @@ }); $('#configFilterModal').modal('hide'); + + showHideDevServicesButton(); + hideEmptyTables(); } +function showHideDevServicesButton(){ + // Check if there is any dev services visible on the page + var numberOfMagicConfig = $('.fa-magic:visible').length; + if(numberOfMagicConfig === 0){ + $('.devservices').hide(); + }else { + $('.devservices').show(); + } +} + function hideEmptyTables(){ $('.filterableTable').filter(function(index){ @@ -306,6 +342,7 @@ $(this).parent().show(); }); clearFilterInput(); + showHideDevServicesButton(); } {/script} @@ -336,7 +373,7 @@
{#if info:hasDevServices} -
+
@@ -379,19 +416,60 @@ + {#if item.configPhase?? && (item.configPhase == "BUILD_AND_RUN_TIME_FIXED" || item.configPhase == "BUILD_TIME")} + + {#else} + + {/if} + {item.configValue.name} + {#if item.autoFromDevServices} {/if} + {#if configsource.key.editable} -
- -
- + + {#if item.typeName && item.typeName == "java.lang.Boolean"} +
+
+ + +
+
+ {#else if item.typeName && (item.typeName == "java.lang.Integer" || item.typeName == "java.lang.Long")} +
+ +
+ +
+
+ {#else if item.typeName && (item.typeName == "java.lang.Float" || item.typeName == "java.lang.Double")} +
+ +
+ +
+
+ {#else if item.typeName && (item.typeName == "java.lang.Enum" || item.typeName == "java.util.logging.Level")} +
+
-
+ {#else} +
+ +
+ +
+
+ {/if} {#else} {item.configValue.value} {/if} @@ -428,7 +506,7 @@ {#if info:hasDevServices} -