diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 7aaec6dcc712a..2be0032ec7c0a 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -12,7 +12,7 @@ com.gradle quarkus-build-caching-extension - 1.2 + 1.6 io.quarkus.develocity diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index 1c1d8f54b73e1..54c11d2cc2921 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -788,6 +788,7 @@ public NativeImageInvokerInfo build() { * control its actual inclusion which will depend on the usual analysis. */ nativeImageArgs.add("-J--add-exports=java.security.jgss/sun.security.krb5=ALL-UNNAMED"); + nativeImageArgs.add("-J--add-exports=java.security.jgss/sun.security.jgss=ALL-UNNAMED"); //address https://github.com/quarkusio/quarkus-quickstarts/issues/993 nativeImageArgs.add("-J--add-opens=java.base/java.text=ALL-UNNAMED"); diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java index c090d3b1701a7..4c0bc2fdee5ab 100644 --- a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ConditionalDependenciesEnabler.java @@ -170,8 +170,7 @@ private void queueConditionalDependency(ExtensionDependency extension, Depend private Configuration createConditionalDependenciesConfiguration(Project project, Dependency conditionalDep) { // previously we used a detached configuration here but apparently extendsFrom(enforcedPlatforms) // wouldn't actually enforce platforms on a detached configuration - final String name = conditionalDep.getGroup() + ":" + conditionalDep.getName() + ":" + conditionalDep.getVersion() - + "Configuration"; + var name = getConditionalConfigurationName(conditionalDep); var config = project.getConfigurations().findByName(name); if (config == null) { project.getConfigurations().register(name, configuration -> { @@ -184,6 +183,31 @@ private Configuration createConditionalDependenciesConfiguration(Project project return config; } + private static String getConditionalConfigurationName(Dependency conditionalDep) { + var name = new StringBuilder().append("quarkusConditional"); + appendCapitalized(name, conditionalDep.getGroup()); + appendCapitalized(name, conditionalDep.getName()); + appendCapitalized(name, conditionalDep.getVersion()); + return name.append("Configuration").toString(); + } + + private static void appendCapitalized(StringBuilder sb, String part) { + if (part != null && !part.isEmpty()) { + boolean toUpperCase = true; + for (int i = 0; i < part.length(); ++i) { + var c = part.charAt(i); + if (toUpperCase) { + sb.append(Character.toUpperCase(c)); + toUpperCase = false; + } else if (c == '.' || c == '-') { + toUpperCase = true; + } else { + sb.append(c); + } + } + } + } + private void enableConditionalDependency(ModuleVersionIdentifier dependency) { final Set> extensions = featureVariants.remove(getFeatureKey(dependency)); if (extensions == null) { diff --git a/docs/src/main/asciidoc/rest.adoc b/docs/src/main/asciidoc/rest.adoc index 5531dbb5c0001..f40b865e34917 100644 --- a/docs/src/main/asciidoc/rest.adoc +++ b/docs/src/main/asciidoc/rest.adoc @@ -1420,10 +1420,11 @@ public class Person { @SecureField(rolesAllowed = "${role:admin}") <1> private String address; - public Person(Long id, String first, String last) { + public Person(Long id, String first, String last, String address) { this.id = id; this.first = first; this.last = last; + this.address = address; } public Long getId() { @@ -1466,7 +1467,7 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Response; @Path("person") -public class Person { +public class PersonResource { @Path("{id}") @GET diff --git a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc index 6bceac124346a..6f73aa97eeadb 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client-reference.adoc @@ -363,8 +363,7 @@ import io.quarkus.runtime.StartupEvent; import io.smallrye.mutiny.Uni; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; -import jakarta.i -nject.Inject; +import jakarta.inject.Inject; @ApplicationScoped public class OidcClientCreator { diff --git a/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/AbstractInputCollectionOutputCollection.java b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/AbstractInputCollectionOutputCollection.java new file mode 100644 index 0000000000000..26edbfe29cb43 --- /dev/null +++ b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/AbstractInputCollectionOutputCollection.java @@ -0,0 +1,20 @@ +package io.quarkus.amazon.lambda.deployment.testing; + +import java.util.ArrayList; +import java.util.List; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +import io.quarkus.amazon.lambda.deployment.testing.model.InputPerson; +import io.quarkus.amazon.lambda.deployment.testing.model.OutputPerson; + +public abstract class AbstractInputCollectionOutputCollection implements RequestHandler, List> { + + @Override + public List handleRequest(List inputPeronList, Context context) { + List personList = new ArrayList<>(); + inputPeronList.forEach(person -> personList.add(new OutputPerson(person.getName()))); + return personList; + } +} diff --git a/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/AbstractInputCollectionOutputCollectionLambdaImpl.java b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/AbstractInputCollectionOutputCollectionLambdaImpl.java new file mode 100644 index 0000000000000..6b8aff7356e9f --- /dev/null +++ b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/AbstractInputCollectionOutputCollectionLambdaImpl.java @@ -0,0 +1,5 @@ +package io.quarkus.amazon.lambda.deployment.testing; + +public class AbstractInputCollectionOutputCollectionLambdaImpl extends AbstractInputCollectionOutputCollection { + +} diff --git a/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/AbstractInputCollectionOutputCollectionLambdaImplTest.java b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/AbstractInputCollectionOutputCollectionLambdaImplTest.java new file mode 100644 index 0000000000000..ce5f9843936b5 --- /dev/null +++ b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/AbstractInputCollectionOutputCollectionLambdaImplTest.java @@ -0,0 +1,46 @@ +package io.quarkus.amazon.lambda.deployment.testing; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.hasEntry; + +import java.util.ArrayList; +import java.util.List; + +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.amazon.lambda.deployment.testing.model.InputPerson; +import io.quarkus.amazon.lambda.deployment.testing.model.OutputPerson; +import io.quarkus.test.QuarkusUnitTest; + +public class AbstractInputCollectionOutputCollectionLambdaImplTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> ShrinkWrap + .create(JavaArchive.class) + .addClasses(AbstractInputCollectionOutputCollectionLambdaImpl.class, AbstractInputCollectionOutputCollection.class, + InputPerson.class, OutputPerson.class)); + + @Test + void abstractRequestHandler_InputCollectionInputPerson_OutputCollectionOutputPerson() { + + List personList = new ArrayList<>(); + personList.add(new InputPerson("Chris")); + personList.add(new InputPerson("Fred")); + + given() + .body(personList) + .when() + .post() + .then() + .statusCode(200) + .body("", hasItem(hasEntry("outputname", "Chris"))) // OutputPerson serializes name with key outputname + .body("", hasItem(hasEntry("outputname", "Fred"))) + .body("", not(hasItem(hasEntry("name", "Chris")))) // make sure that there is no key name + .body("", not(hasItem(hasEntry("name", "Fred")))); + } +} 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 index f62a7ef7eef95..7b2ed22d57b92 100644 --- 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 @@ -3,10 +3,12 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; -public class GreetingLambda implements RequestHandler { +import io.quarkus.amazon.lambda.deployment.testing.model.InputPerson; + +public class GreetingLambda implements RequestHandler { @Override - public String handleRequest(Person input, Context context) { + public String handleRequest(InputPerson input, Context context) { return "Hey " + input.getName(); } } diff --git a/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/GreetingLambdaTest.java b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/GreetingLambdaTest.java new file mode 100644 index 0000000000000..899e2febbff0e --- /dev/null +++ b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/GreetingLambdaTest.java @@ -0,0 +1,38 @@ +package io.quarkus.amazon.lambda.deployment.testing; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.containsString; + +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.amazon.lambda.deployment.testing.model.InputPerson; +import io.quarkus.test.QuarkusUnitTest; + +class GreetingLambdaTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> ShrinkWrap + .create(JavaArchive.class) + .addClasses(GreetingLambda.class, InputPerson.class)); + + @Test + public void requestHandler_InputPerson_OutputString() throws Exception { + // you test your lambdas by invoking on http://localhost:8081 + // this works in dev mode too + + InputPerson in = new InputPerson("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/InputCollectionOutputCollectionLambda.java b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/InputCollectionOutputCollectionLambda.java new file mode 100644 index 0000000000000..468cd6f255a6a --- /dev/null +++ b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/InputCollectionOutputCollectionLambda.java @@ -0,0 +1,24 @@ +package io.quarkus.amazon.lambda.deployment.testing; + +import java.util.ArrayList; +import java.util.List; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +import io.quarkus.amazon.lambda.deployment.testing.model.InputPerson; +import io.quarkus.amazon.lambda.deployment.testing.model.OutputPerson; + +public class InputCollectionOutputCollectionLambda implements RequestHandler, List> { + + @Override + public List handleRequest(List people, Context context) { + + List outputPeople = new ArrayList<>(); + people.forEach((person) -> { + outputPeople.add(new OutputPerson(person.getName())); + }); + + return outputPeople; + } +} diff --git a/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/InputCollectionOutputCollectionLambdaTest.java b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/InputCollectionOutputCollectionLambdaTest.java new file mode 100644 index 0000000000000..907827ece866a --- /dev/null +++ b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/InputCollectionOutputCollectionLambdaTest.java @@ -0,0 +1,45 @@ +package io.quarkus.amazon.lambda.deployment.testing; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.hasEntry; + +import java.util.ArrayList; +import java.util.List; + +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.amazon.lambda.deployment.testing.model.InputPerson; +import io.quarkus.amazon.lambda.deployment.testing.model.OutputPerson; +import io.quarkus.test.QuarkusUnitTest; + +public class InputCollectionOutputCollectionLambdaTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> ShrinkWrap + .create(JavaArchive.class) + .addClasses(InputCollectionOutputCollectionLambda.class, InputPerson.class, OutputPerson.class)); + + @Test + void requestHandler_InputCollectionInputPerson_OutputCollectionOutputPerson() { + + List personList = new ArrayList<>(); + personList.add(new InputPerson("Chris")); + personList.add(new InputPerson("Fred")); + + given() + .body(personList) + .when() + .post() + .then() + .statusCode(200) + .body("", hasItem(hasEntry("outputname", "Chris"))) // OutputPerson serializes name with key outputname + .body("", hasItem(hasEntry("outputname", "Fred"))) + .body("", not(hasItem(hasEntry("name", "Chris")))) // make sure that there is no key name + .body("", not(hasItem(hasEntry("name", "Fred")))); + } +} 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 index cef9a2bda6761..60b108b89b094 100644 --- 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 @@ -10,6 +10,8 @@ import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.amazon.lambda.deployment.testing.model.InputPerson; +import io.quarkus.amazon.lambda.deployment.testing.model.OutputPerson; import io.quarkus.test.ContinuousTestingTestUtils; import io.quarkus.test.QuarkusDevModeTest; @@ -21,7 +23,7 @@ public class LambdaDevServicesContinuousTestingTestCase { @Override public JavaArchive get() { return ShrinkWrap.create(JavaArchive.class) - .addClasses(GreetingLambda.class, Person.class) + .addClasses(GreetingLambda.class, InputPerson.class, OutputPerson.class) .addAsResource( new StringAsset(ContinuousTestingTestUtils.appProperties( "quarkus.log.category.\"io.quarkus.amazon.lambda.runtime\".level=DEBUG")), @@ -30,7 +32,7 @@ public JavaArchive get() { }).setTestArchiveProducer(new Supplier<>() { @Override public JavaArchive get() { - return ShrinkWrap.create(JavaArchive.class).addClass(LambdaHandlerET.class); + return ShrinkWrap.create(JavaArchive.class).addClass(GreetingLambdaTest.class); } }); @@ -45,7 +47,7 @@ public void testLambda() throws Exception { result = utils.waitForNextCompletion(); Assertions.assertEquals(0, result.getTotalTestsPassed()); Assertions.assertEquals(1, result.getTotalTestsFailed()); - test.modifyTestSourceFile(LambdaHandlerET.class, s -> s.replace("Hey", "Yo")); + test.modifyTestSourceFile(GreetingLambdaTest.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 deleted file mode 100644 index 135b02bac0547..0000000000000 --- a/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/LambdaHandlerET.java +++ /dev/null @@ -1,31 +0,0 @@ -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 lambdas 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 deleted file mode 100644 index d2a4066a77dc2..0000000000000 --- a/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/Person.java +++ /dev/null @@ -1,15 +0,0 @@ -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/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/model/InputPerson.java b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/model/InputPerson.java new file mode 100644 index 0000000000000..b74c9482d8c3c --- /dev/null +++ b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/model/InputPerson.java @@ -0,0 +1,22 @@ +package io.quarkus.amazon.lambda.deployment.testing.model; + +public class InputPerson { + + public InputPerson() { + } + + public InputPerson(String name) { + this.name = name; + } + + private String name; + + public String getName() { + return name; + } + + public InputPerson setName(String name) { + this.name = name; + return this; + } +} diff --git a/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/model/OutputPerson.java b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/model/OutputPerson.java new file mode 100644 index 0000000000000..f448321ffdd5f --- /dev/null +++ b/extensions/amazon-lambda/deployment/src/test/java/io/quarkus/amazon/lambda/deployment/testing/model/OutputPerson.java @@ -0,0 +1,20 @@ +package io.quarkus.amazon.lambda.deployment.testing.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class OutputPerson { + + public OutputPerson() { + } + + public OutputPerson(String name) { + this.name = name; + } + + @JsonProperty("outputname") + private String name; + + public String getName() { + return name; + } +} diff --git a/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AmazonLambdaRecorder.java b/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AmazonLambdaRecorder.java index f8fb6d5b76568..49dc6d9d92a2b 100644 --- a/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AmazonLambdaRecorder.java +++ b/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/AmazonLambdaRecorder.java @@ -4,6 +4,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Method; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; @@ -16,6 +17,7 @@ import com.amazonaws.services.lambda.runtime.events.S3Event; import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.amazon.lambda.runtime.handlers.CollectionInputReader; import io.quarkus.amazon.lambda.runtime.handlers.S3EventInputReader; import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.runtime.LaunchMode; @@ -53,11 +55,15 @@ static void initializeHandlerClass(Class> handler ObjectMapper objectMapper = AmazonLambdaMapperRecorder.objectMapper; Method handlerMethod = discoverHandlerMethod(handlerClass); Class parameterType = handlerMethod.getParameterTypes()[0]; + if (parameterType.equals(S3Event.class)) { objectReader = new S3EventInputReader(objectMapper); + } else if (Collection.class.isAssignableFrom(parameterType)) { + objectReader = new CollectionInputReader<>(objectMapper, handlerMethod); } else { objectReader = new JacksonInputReader(objectMapper.readerFor(parameterType)); } + objectWriter = new JacksonOutputWriter(objectMapper.writerFor(handlerMethod.getReturnType())); } diff --git a/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/handlers/CollectionInputReader.java b/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/handlers/CollectionInputReader.java new file mode 100644 index 0000000000000..a698bcdbf400f --- /dev/null +++ b/extensions/amazon-lambda/runtime/src/main/java/io/quarkus/amazon/lambda/runtime/handlers/CollectionInputReader.java @@ -0,0 +1,28 @@ +package io.quarkus.amazon.lambda.runtime.handlers; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Collection; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; + +import io.quarkus.amazon.lambda.runtime.LambdaInputReader; + +public class CollectionInputReader implements LambdaInputReader> { + final ObjectReader reader; + + public CollectionInputReader(ObjectMapper mapper, Method handler) { + Type genericParameterType = handler.getGenericParameterTypes()[0]; + JavaType constructParameterType = mapper.getTypeFactory().constructType(genericParameterType); + this.reader = mapper.readerFor(constructParameterType); + } + + @Override + public Collection readValue(InputStream is) throws IOException { + return this.reader.readValue(is); + } +} diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java index a52ff559827ff..47a397108eaa1 100644 --- a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java +++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java @@ -303,6 +303,22 @@ InfinispanPropertiesBuildItem setup(ApplicationArchivesBuildItem applicationArch "org.infinispan.client.hotrod.impl.consistenthash.SegmentConsistentHash") .build()); + // Elytron Classes + String[] elytronClasses = new String[] { + "org.wildfly.security.sasl.plain.PlainSaslClientFactory", + "org.wildfly.security.sasl.scram.ScramSaslClientFactory", + "org.wildfly.security.credential.BearerTokenCredential", + "org.wildfly.security.credential.GSSKerberosCredential", + "org.wildfly.security.credential.KeyPairCredential", + "org.wildfly.security.credential.PasswordCredential", + "org.wildfly.security.credential.PublicKeyCredential", + "org.wildfly.security.credential.SecretKeyCredential", + "org.wildfly.security.credential.SSHCredential", + "org.wildfly.security.credential.X509CertificateChainPrivateCredential", + "org.wildfly.security.credential.X509CertificateChainPublicCredential" + }; + + reflectiveClass.produce(ReflectiveClassBuildItem.builder(elytronClasses).build()); return new InfinispanPropertiesBuildItem(propertiesMap); } diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ScheduledMethodTimeZoneTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/ScheduledMethodTimeZoneTest.java similarity index 98% rename from extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ScheduledMethodTimeZoneTest.java rename to extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/ScheduledMethodTimeZoneTest.java index 95d4d47724c53..38b42d218c2a6 100644 --- a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/ScheduledMethodTimeZoneTest.java +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/ScheduledMethodTimeZoneTest.java @@ -1,4 +1,4 @@ -package io.quarkus.quartz.test; +package io.quarkus.quartz.test.timezone; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerNextFireTimeZoneTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerNextFireTimeZoneTest.java new file mode 100644 index 0000000000000..4e588223254a3 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerNextFireTimeZoneTest.java @@ -0,0 +1,74 @@ +package io.quarkus.quartz.test.timezone; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.ScheduledExecution; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.Trigger; +import io.quarkus.test.QuarkusUnitTest; + +public class TriggerNextFireTimeZoneTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Jobs.class); + }); + + @Inject + Scheduler scheduler; + + @Test + public void testScheduledJobs() throws InterruptedException { + Trigger prague = scheduler.getScheduledJob("prague"); + Trigger boston = scheduler.getScheduledJob("boston"); + Trigger ulaanbaatar = scheduler.getScheduledJob("ulaanbaatar"); + assertNotNull(prague); + assertNotNull(boston); + assertNotNull(ulaanbaatar); + Instant pragueNext = prague.getNextFireTime(); + Instant bostonNext = boston.getNextFireTime(); + Instant ulaanbaatarNext = ulaanbaatar.getNextFireTime(); + assertTime(pragueNext.atZone(ZoneId.of("Europe/Prague"))); + assertTime(bostonNext.atZone(ZoneId.of("America/New_York"))); + assertTime(ulaanbaatarNext.atZone(ZoneId.of("Asia/Ulaanbaatar"))); + } + + private static void assertTime(ZonedDateTime time) { + assertEquals(20, time.getHour()); + assertEquals(30, time.getMinute()); + assertEquals(0, time.getSecond()); + } + + static class Jobs { + + @Scheduled(identity = "prague", cron = "0 30 20 * * ?", timeZone = "Europe/Prague") + void withPragueTimezone(ScheduledExecution execution) { + assertNotEquals(execution.getFireTime(), execution.getScheduledFireTime()); + assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Europe/Prague"))); + } + + @Scheduled(identity = "boston", cron = "0 30 20 * * ?", timeZone = "America/New_York") + void withBostonTimezone() { + } + + @Scheduled(identity = "ulaanbaatar", cron = "0 30 20 * * ?", timeZone = "Asia/Ulaanbaatar") + void withIstanbulTimezone(ScheduledExecution execution) { + assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Asia/Ulaanbaatar"))); + } + + } + +} diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerPrevFireTimeZoneTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerPrevFireTimeZoneTest.java new file mode 100644 index 0000000000000..4da6a548d7e43 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/timezone/TriggerPrevFireTimeZoneTest.java @@ -0,0 +1,93 @@ +package io.quarkus.quartz.test.timezone; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.Trigger; +import io.quarkus.test.QuarkusUnitTest; + +public class TriggerPrevFireTimeZoneTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime prague = now.withZoneSameInstant(ZoneId.of("Europe/Prague")); + ZonedDateTime istanbul = now.withZoneSameInstant(ZoneId.of("Europe/Istanbul")); + // For example, the current date-time is 2024-07-09 10:08:00; + // the default time zone is Europe/London + // then the config should look like: + // simpleJobs1.cron=0/1 * 11 * * ? + // simpleJobs2.cron=0/1 * 12 * * ? + String properties = String.format( + "simpleJobs1.cron=0/1 * %s * * ?\n" + + "simpleJobs1.hour=%s\n" + + "simpleJobs2.cron=0/1 * %s * * ?\n" + + "simpleJobs2.hour=%s", + prague.getHour(), prague.getHour(), istanbul.getHour(), istanbul.getHour()); + root.addClasses(Jobs.class) + .addAsResource( + new StringAsset(properties), + "application.properties"); + }); + + @ConfigProperty(name = "simpleJobs1.hour") + int pragueHour; + + @ConfigProperty(name = "simpleJobs2.hour") + int istanbulHour; + + @Inject + Scheduler scheduler; + + @Test + public void testScheduledJobs() throws InterruptedException { + assertTrue(Jobs.PRAGUE_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(Jobs.ISTANBUL_LATCH.await(5, TimeUnit.SECONDS)); + Trigger prague = scheduler.getScheduledJob("prague"); + Trigger istanbul = scheduler.getScheduledJob("istanbul"); + assertNotNull(prague); + assertNotNull(istanbul); + Instant praguePrev = prague.getPreviousFireTime(); + Instant istanbulPrev = istanbul.getPreviousFireTime(); + assertNotNull(praguePrev); + assertNotNull(istanbulPrev); + assertEquals(praguePrev, istanbulPrev); + assertEquals(pragueHour, praguePrev.atZone(ZoneId.of("Europe/Prague")).getHour()); + assertEquals(istanbulHour, istanbulPrev.atZone(ZoneId.of("Europe/Istanbul")).getHour()); + } + + static class Jobs { + + static final CountDownLatch PRAGUE_LATCH = new CountDownLatch(1); + static final CountDownLatch ISTANBUL_LATCH = new CountDownLatch(1); + + @Scheduled(identity = "prague", cron = "{simpleJobs1.cron}", timeZone = "Europe/Prague") + void withPragueTimezone() { + PRAGUE_LATCH.countDown(); + } + + @Scheduled(identity = "istanbul", cron = "{simpleJobs2.cron}", timeZone = "Europe/Istanbul") + void withIstanbulTimezone() { + ISTANBUL_LATCH.countDown(); + } + + } + +} diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java index 5f69240af667c..fda1ac26fec88 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/ScheduledExecution.java @@ -14,6 +14,9 @@ public interface ScheduledExecution { Trigger getTrigger(); /** + * The returned {@code Instant} is converted from the date-time in the default timezone. A timezone of a cron-based job + * is not taken into account. + *

* Unlike {@link Trigger#getPreviousFireTime()} this method always returns the same value. * * @return the time the associated trigger was fired @@ -21,6 +24,12 @@ public interface ScheduledExecution { Instant getFireTime(); /** + * If the trigger represents a cron-based job with a timezone, then the returned {@code Instant} takes the timezone into + * account. + *

+ * For example, if there is a job with cron expression {@code 0 30 20 ? * * *} with timezone {@code Europe/Berlin}, + * then the return value looks like {@code 2024-07-08T18:30:00Z}. And {@link Instant#atZone(java.time.ZoneId)} for + * {@code Europe/Berlin} would yield {@code 2024-07-08T20:30+02:00[Europe/Berlin]}. * * @return the time the action was scheduled for */ diff --git a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java index c076e5712bc0e..0a5f94d48ffb3 100644 --- a/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java +++ b/extensions/scheduler/api/src/main/java/io/quarkus/scheduler/Trigger.java @@ -21,12 +21,24 @@ public interface Trigger { String getId(); /** + * If the trigger represents a cron-based job with a timezone, then the returned {@code Instant} takes the timezone into + * account. + *

+ * For example, if there is a job with cron expression {@code 0 30 20 ? * * *} with timezone {@code Europe/Berlin}, then the + * return value looks like {@code 2024-07-08T18:30:00Z}. And {@link Instant#atZone(java.time.ZoneId)} for + * {@code Europe/Berlin} would yield {@code 2024-07-08T20:30+02:00[Europe/Berlin]}. * * @return the next time at which the trigger is scheduled to fire, or {@code null} if it will not fire again */ Instant getNextFireTime(); /** + * If the trigger represents a cron-based job with a timezone, then the returned {@code Instant} takes the timezone into + * account. + *

+ * For example, if there is a job with cron expression {@code 0 30 20 ? * * *} with timezone {@code Europe/Berlin}, then the + * return value looks like {@code 2024-07-08T18:30:00Z}. And {@link Instant#atZone(java.time.ZoneId)} for + * {@code Europe/Berlin} would yield {@code 2024-07-08T20:30+02:00[Europe/Berlin]}. * * @return the previous time at which the trigger fired, or {@code null} if it has not fired yet */ diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ScheduledMethodTimeZoneTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/ScheduledMethodTimeZoneTest.java similarity index 97% rename from extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ScheduledMethodTimeZoneTest.java rename to extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/ScheduledMethodTimeZoneTest.java index aba645216812a..9db547b46e7a0 100644 --- a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/ScheduledMethodTimeZoneTest.java +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/ScheduledMethodTimeZoneTest.java @@ -1,4 +1,4 @@ -package io.quarkus.scheduler.test; +package io.quarkus.scheduler.test.timezone; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -41,7 +41,6 @@ public class ScheduledMethodTimeZoneTest { + "simpleJobs2.cron=0/1 * %s * * ?\n" + "simpleJobs2.timeZone=%s", now.getHour(), timeZone, job2Hour, timeZone); - // System.out.println(properties); jar.addClasses(Jobs.class) .addAsResource( new StringAsset(properties), diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerNextFireTimeZoneTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerNextFireTimeZoneTest.java new file mode 100644 index 0000000000000..d301a0e1cff5d --- /dev/null +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerNextFireTimeZoneTest.java @@ -0,0 +1,74 @@ +package io.quarkus.scheduler.test.timezone; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.ScheduledExecution; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.Trigger; +import io.quarkus.test.QuarkusUnitTest; + +public class TriggerNextFireTimeZoneTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Jobs.class); + }); + + @Inject + Scheduler scheduler; + + @Test + public void testScheduledJobs() throws InterruptedException { + Trigger prague = scheduler.getScheduledJob("prague"); + Trigger boston = scheduler.getScheduledJob("boston"); + Trigger ulaanbaatar = scheduler.getScheduledJob("ulaanbaatar"); + assertNotNull(prague); + assertNotNull(boston); + assertNotNull(ulaanbaatar); + Instant pragueNext = prague.getNextFireTime(); + Instant bostonNext = boston.getNextFireTime(); + Instant ulaanbaatarNext = ulaanbaatar.getNextFireTime(); + assertTime(pragueNext.atZone(ZoneId.of("Europe/Prague"))); + assertTime(bostonNext.atZone(ZoneId.of("America/New_York"))); + assertTime(ulaanbaatarNext.atZone(ZoneId.of("Asia/Ulaanbaatar"))); + } + + private static void assertTime(ZonedDateTime time) { + assertEquals(20, time.getHour()); + assertEquals(30, time.getMinute()); + assertEquals(0, time.getSecond()); + } + + static class Jobs { + + @Scheduled(identity = "prague", cron = "0 30 20 * * ?", timeZone = "Europe/Prague") + void withPragueTimezone(ScheduledExecution execution) { + assertNotEquals(execution.getFireTime(), execution.getScheduledFireTime()); + assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Europe/Prague"))); + } + + @Scheduled(identity = "boston", cron = "0 30 20 * * ?", timeZone = "America/New_York") + void withBostonTimezone() { + } + + @Scheduled(identity = "ulaanbaatar", cron = "0 30 20 * * ?", timeZone = "Asia/Ulaanbaatar") + void withIstanbulTimezone(ScheduledExecution execution) { + assertTime(execution.getScheduledFireTime().atZone(ZoneId.of("Asia/Ulaanbaatar"))); + } + + } + +} diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerPrevFireTimeZoneTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerPrevFireTimeZoneTest.java new file mode 100644 index 0000000000000..ed1ef873b77b4 --- /dev/null +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/timezone/TriggerPrevFireTimeZoneTest.java @@ -0,0 +1,93 @@ +package io.quarkus.scheduler.test.timezone; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.scheduler.Trigger; +import io.quarkus.test.QuarkusUnitTest; + +public class TriggerPrevFireTimeZoneTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime prague = now.withZoneSameInstant(ZoneId.of("Europe/Prague")); + ZonedDateTime istanbul = now.withZoneSameInstant(ZoneId.of("Europe/Istanbul")); + // For example, the current date-time is 2024-07-09 10:08:00; + // the default time zone is Europe/London + // then the config should look like: + // simpleJobs1.cron=0/1 * 11 * * ? + // simpleJobs2.cron=0/1 * 12 * * ? + String properties = String.format( + "simpleJobs1.cron=0/1 * %s * * ?\n" + + "simpleJobs1.hour=%s\n" + + "simpleJobs2.cron=0/1 * %s * * ?\n" + + "simpleJobs2.hour=%s", + prague.getHour(), prague.getHour(), istanbul.getHour(), istanbul.getHour()); + root.addClasses(Jobs.class) + .addAsResource( + new StringAsset(properties), + "application.properties"); + }); + + @ConfigProperty(name = "simpleJobs1.hour") + int pragueHour; + + @ConfigProperty(name = "simpleJobs2.hour") + int istanbulHour; + + @Inject + Scheduler scheduler; + + @Test + public void testScheduledJobs() throws InterruptedException { + assertTrue(Jobs.PRAGUE_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(Jobs.ISTANBUL_LATCH.await(5, TimeUnit.SECONDS)); + Trigger prague = scheduler.getScheduledJob("prague"); + Trigger istanbul = scheduler.getScheduledJob("istanbul"); + assertNotNull(prague); + assertNotNull(istanbul); + Instant praguePrev = prague.getPreviousFireTime(); + Instant istanbulPrev = istanbul.getPreviousFireTime(); + assertNotNull(praguePrev); + assertNotNull(istanbulPrev); + assertEquals(praguePrev, istanbulPrev); + assertEquals(pragueHour, praguePrev.atZone(ZoneId.of("Europe/Prague")).getHour()); + assertEquals(istanbulHour, istanbulPrev.atZone(ZoneId.of("Europe/Istanbul")).getHour()); + } + + static class Jobs { + + static final CountDownLatch PRAGUE_LATCH = new CountDownLatch(1); + static final CountDownLatch ISTANBUL_LATCH = new CountDownLatch(1); + + @Scheduled(identity = "prague", cron = "{simpleJobs1.cron}", timeZone = "Europe/Prague") + void withPragueTimezone() { + PRAGUE_LATCH.countDown(); + } + + @Scheduled(identity = "istanbul", cron = "{simpleJobs2.cron}", timeZone = "Europe/Istanbul") + void withIstanbulTimezone() { + ISTANBUL_LATCH.countDown(); + } + + } + +} diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java index ce6d7482341da..687c081bbfe50 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/runtime/SimpleScheduler.java @@ -589,28 +589,29 @@ static class CronTrigger extends SimpleTrigger { super(id, start, description); this.cron = cron; this.executionTime = ExecutionTime.forCron(cron); - this.lastFireTime = start; this.gracePeriod = gracePeriod; this.timeZone = timeZone; + // The last fire time stores the zoned time + this.lastFireTime = zoned(start); } @Override public Instant getNextFireTime() { - Optional nextFireTime = executionTime.nextExecution(lastFireTime); - return nextFireTime.isPresent() ? nextFireTime.get().toInstant() : null; + return executionTime.nextExecution(lastFireTime).map(ZonedDateTime::toInstant).orElse(null); } + @Override ZonedDateTime evaluate(ZonedDateTime now) { if (now.isBefore(start)) { return null; } - ZonedDateTime zonedNow = timeZone == null ? now : now.withZoneSameInstant(timeZone); - Optional lastExecution = executionTime.lastExecution(zonedNow); + now = zoned(now); + Optional lastExecution = executionTime.lastExecution(now); if (lastExecution.isPresent()) { ZonedDateTime lastTruncated = lastExecution.get().truncatedTo(ChronoUnit.SECONDS); - if (zonedNow.isAfter(lastTruncated) && lastFireTime.isBefore(lastTruncated)) { + if (now.isAfter(lastTruncated) && lastFireTime.isBefore(lastTruncated)) { LOG.tracef("%s fired, last=%s", this, lastTruncated); - lastFireTime = zonedNow; + lastFireTime = now; return lastTruncated; } } @@ -623,9 +624,9 @@ public boolean isOverdue() { if (now.isBefore(start)) { return false; } - ZonedDateTime zonedNow = timeZone == null ? now : now.withZoneSameInstant(timeZone); + now = zoned(now); Optional nextFireTime = executionTime.nextExecution(lastFireTime); - return nextFireTime.isEmpty() || nextFireTime.get().plus(gracePeriod).isBefore(zonedNow); + return nextFireTime.isEmpty() || nextFireTime.get().plus(gracePeriod).isBefore(now); } @Override @@ -634,6 +635,10 @@ public String toString() { + timeZone + "]"; } + private ZonedDateTime zoned(ZonedDateTime time) { + return timeZone == null ? time : time.withZoneSameInstant(timeZone); + } + } static class SimpleScheduledExecution implements ScheduledExecution { diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/BlockingChecksVertxContextDuplicationTest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/BlockingChecksVertxContextDuplicationTest.java new file mode 100644 index 0000000000000..6fd84159bd3f4 --- /dev/null +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/BlockingChecksVertxContextDuplicationTest.java @@ -0,0 +1,65 @@ +package io.quarkus.smallrye.health.test; + +import static org.hamcrest.Matchers.is; + +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Liveness; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +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.restassured.RestAssured; +import io.restassured.parsing.Parser; +import io.vertx.core.Context; +import io.vertx.core.Vertx; + +class BlockingChecksVertxContextDuplicationTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(ContextCaptureCheck1.class, ContextCaptureCheck2.class) + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")); + + @Test + void testBlockingChecksPropagateVertxContext() { + try { + RestAssured.defaultParser = Parser.JSON; + RestAssured.when().get("/q/health").then() + .body("status", is("UP"), + "checks.size()", is(2)); + + Assertions.assertNotEquals(ContextCaptureCheck1.capturedContext, ContextCaptureCheck2.capturedContext, + "Expected different contexts to be propagated into different blocking health checks"); + } finally { + RestAssured.reset(); + } + } + + @Liveness + public static class ContextCaptureCheck1 implements HealthCheck { + + public static Context capturedContext = null; + + @Override + public HealthCheckResponse call() { + capturedContext = Vertx.currentContext(); + return HealthCheckResponse.up("ContextCaptureCheck1"); + } + } + + @Liveness + public static class ContextCaptureCheck2 implements HealthCheck { + + public static Context capturedContext = null; + + @Override + public HealthCheckResponse call() { + capturedContext = Vertx.currentContext(); + return HealthCheckResponse.up("ContextCaptureCheck2"); + } + } +} diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/QuarkusAsyncHealthCheckFactory.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/QuarkusAsyncHealthCheckFactory.java index cfec52c933b1b..bd7c236dce2e2 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/QuarkusAsyncHealthCheckFactory.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/QuarkusAsyncHealthCheckFactory.java @@ -1,14 +1,19 @@ package io.quarkus.smallrye.health.runtime; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; + import jakarta.inject.Singleton; import org.eclipse.microprofile.health.HealthCheck; import org.eclipse.microprofile.health.HealthCheckResponse; +import io.smallrye.common.vertx.VertxContext; import io.smallrye.health.AsyncHealthCheckFactory; import io.smallrye.health.api.AsyncHealthCheck; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.vertx.MutinyHelper; +import io.vertx.core.Context; import io.vertx.core.Vertx; /** @@ -27,7 +32,19 @@ public QuarkusAsyncHealthCheckFactory(Vertx vertx) { @Override public Uni callSync(HealthCheck healthCheck) { Uni healthCheckResponseUni = super.callSync(healthCheck); - return healthCheckResponseUni.runSubscriptionOn(MutinyHelper.blockingExecutor(vertx, false)); + return healthCheckResponseUni.runSubscriptionOn(new Executor() { + @Override + public void execute(Runnable command) { + Context duplicatedContext = VertxContext.createNewDuplicatedContext(vertx.getOrCreateContext()); + duplicatedContext.executeBlocking(new Callable() { + @Override + public Void call() throws Exception { + command.run(); + return null; + } + }, false); + } + }); } @Override diff --git a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java index cc0bb85cce758..c35577a1d37d7 100644 --- a/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java +++ b/extensions/smallrye-health/runtime/src/main/java/io/quarkus/smallrye/health/runtime/SmallRyeHealthHandlerBase.java @@ -63,7 +63,7 @@ private void doHandle(RoutingContext ctx, ManagedContext requestContext) { .set(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8") .set(HttpHeaders.CACHE_CONTROL, "no-store"); Buffer buffer = Buffer.buffer(256); // this size seems to cover the basic health checks - try (BufferOutputStream outputStream = new BufferOutputStream(buffer);) { + try (BufferOutputStream outputStream = new BufferOutputStream(buffer)) { reporter.reportHealth(outputStream, health); resp.end(buffer); } catch (IOException e) { diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java index 26bf46d6421a8..12a2b327fa6b1 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/Endpoints.java @@ -291,7 +291,7 @@ private static boolean isSecurityFailure(Throwable throwable) { || throwable instanceof ForbiddenException; } - private static boolean isWebSocketIsClosedFailure(Throwable throwable, WebSocketConnectionBase connection) { + static boolean isWebSocketIsClosedFailure(Throwable throwable, WebSocketConnectionBase connection) { if (!connection.isClosed()) { return false; } diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java index d1d4cad07638e..de23dd4779d78 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java @@ -219,18 +219,26 @@ public Uni sendPong(Buffer data) { throw new UnsupportedOperationException(); } - private Uni doSend(BiFunction> function, M message) { + private Uni doSend(BiFunction> sendFunction, M message) { Set connections = connectionManager.getConnections(generatedEndpointClass); if (connections.isEmpty()) { return Uni.createFrom().voidItem(); } List> unis = new ArrayList<>(connections.size()); for (WebSocketConnection connection : connections) { - if (connection.isOpen() && (filter == null || filter.test(connection))) { - unis.add(function.apply(connection, message)); + if (connection.isOpen() + && (filter == null || filter.test(connection))) { + unis.add(sendFunction.apply(connection, message) + // Intentionally ignore 'WebSocket is closed' failures + // It might happen that the connection is closed in the mean time + .onFailure(t -> Endpoints.isWebSocketIsClosedFailure(t, (WebSocketConnectionBase) connection)) + .recoverWithNull()); } } - return unis.isEmpty() ? Uni.createFrom().voidItem() : Uni.join().all(unis).andFailFast().replaceWithVoid(); + if (unis.isEmpty()) { + return Uni.createFrom().voidItem(); + } + return Uni.join().all(unis).andCollectFailures().replaceWithVoid(); } } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/IoUtils.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/IoUtils.java index 0c4006fc3dc74..18513222b7384 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/IoUtils.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/util/IoUtils.java @@ -1,21 +1,23 @@ package io.quarkus.bootstrap.util; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.StringWriter; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.SecureDirectoryStream; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet; import java.util.Objects; @@ -29,8 +31,6 @@ */ public class IoUtils { - private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; - private static final Path TMP_DIR = Paths.get(PropertyUtils.getProperty("java.io.tmpdir")); private static final Logger log = Logger.getLogger(IoUtils.class); @@ -60,40 +60,36 @@ public static Path mkdirs(Path dir) { return dir; } + /** + * Recursively delete the file or directory given by {@code root}. + * The implementation will attempt to do so in a secure manner. + * Any problems encountered will be logged at {@code DEBUG} level. + * + * @param root the root path (must not be {@code null}) + */ public static void recursiveDelete(Path root) { - log.debugf("Recursively delete directory %s", root); + log.debugf("Recursively delete path %s", root); if (root == null || !Files.exists(root)) { return; } try { - Files.walkFileTree(root, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) - throws IOException { - try { - Files.delete(file); - } catch (IOException ex) { - log.debugf(ex, "Unable to delete file " + file); - } - return FileVisitResult.CONTINUE; + if (Files.isDirectory(root)) { + try (DirectoryStream ds = Files.newDirectoryStream(root)) { + recursiveDelete(ds); } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException e) - throws IOException { - if (e == null) { - try { - Files.delete(dir); - } catch (IOException ex) { - log.debugf(ex, "Unable to delete directory " + dir); - } - return FileVisitResult.CONTINUE; - } else { - // directory iteration failed - throw e; - } + try { + Files.delete(root); + } catch (IOException e) { + log.debugf(e, "Unable to delete directory %s", root); } - }); + } else { + log.debugf("Delete file %s", root); + try { + Files.delete(root); + } catch (IOException e) { + log.debugf(e, "Unable to delete file %s", root); + } + } } catch (IOException e) { log.debugf(e, "Error recursively deleting directory"); } @@ -101,9 +97,10 @@ public FileVisitResult postVisitDirectory(Path dir, IOException e) /** * Creates a new empty directory or empties an existing one. + * Any problems encountered while emptying the directory will be logged at {@code DEBUG} level. * * @param dir directory - * @throws IOException in case of a failure + * @throws IOException if creating or accessing the directory itself fails */ public static void createOrEmptyDir(Path dir) throws IOException { log.debugf("Create or empty directory %s", dir); @@ -113,17 +110,51 @@ public static void createOrEmptyDir(Path dir) throws IOException { Files.createDirectories(dir); return; } - if (!Files.isDirectory(dir)) { - throw new IllegalArgumentException(dir + " is not a directory"); + // recursively delete the *contents* of the directory, if any (keep the directory itself) + try (DirectoryStream ds = Files.newDirectoryStream(dir)) { + recursiveDelete(ds); + } + } + + private static void recursiveDelete(DirectoryStream ds) { + if (ds instanceof SecureDirectoryStream sds) { + // best, fastest, and most likely path for most OSes + recursiveDeleteSecure(sds); + } else { + // this may not work well on e.g. NFS, so we avoid this path if possible + for (Path p : ds) { + recursiveDelete(p); + } } - log.debugf("Iterate over contents of %s to delete its contents", dir); - try (DirectoryStream stream = Files.newDirectoryStream(dir)) { - for (Path p : stream) { - if (Files.isDirectory(p)) { - recursiveDelete(p); - } else { - log.debugf("Delete file %s", p); - Files.delete(p); + } + + private static void recursiveDeleteSecure(SecureDirectoryStream sds) { + for (Path p : sds) { + Path file = p.getFileName(); + BasicFileAttributes attrs; + try { + attrs = sds.getFileAttributeView(file, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS) + .readAttributes(); + } catch (IOException e) { + log.debugf(e, "Unable to query file type of %s", p); + continue; + } + if (attrs.isDirectory()) { + try { + try (SecureDirectoryStream nested = sds.newDirectoryStream(file)) { + recursiveDeleteSecure(nested); + } + sds.deleteDirectory(file); + } catch (IOException e) { + log.debugf(e, "Unable to delete directory %s", p); + } + } else { + // log the whole path, not the file name + log.debugf("Delete file %s", p); + try { + sds.deleteFile(file); + } catch (IOException e) { + log.debugf(e, "Unable to delete file %s", p); } } } @@ -163,24 +194,43 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) return target; } + /** + * Read the contents of a file as a string. + * + * @param file the file to read (must not be {@code null}) + * @return the file content, as a string (not {@code null}) + * @throws IOException if an error occurs when reading the file + * @deprecated Use {@link Files#readString(Path, Charset)} instead. + */ + @Deprecated(forRemoval = true) public static String readFile(Path file) throws IOException { - final char[] charBuffer = new char[DEFAULT_BUFFER_SIZE]; - int n = 0; - final StringWriter output = new StringWriter(); - try (BufferedReader input = Files.newBufferedReader(file)) { - while ((n = input.read(charBuffer)) != -1) { - output.write(charBuffer, 0, n); - } - } - return output.getBuffer().toString(); + return Files.readString(file, StandardCharsets.UTF_8); } + /** + * Copy the input stream to the given output stream. + * Calling this method is identical to calling {@code in.transferTo(out)}. + * + * @param out the output stream (must not be {@code null}) + * @param in the input stream (must not be {@code null}) + * @throws IOException if an error occurs during the copy + * @see InputStream#transferTo(OutputStream) + */ public static void copy(OutputStream out, InputStream in) throws IOException { in.transferTo(out); } + /** + * Write a string to a file using UTF-8 encoding. + * The file will be created if it does not exist, and truncated if it is not empty. + * + * @param file the file to write (must not be {@code null}) + * @param content the string to write to the file (must not be {@code null}) + * @throws IOException if an error occurs when writing the file + */ public static void writeFile(Path file, String content) throws IOException { - Files.write(file, content.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE); + Files.writeString(file, content, StandardCharsets.UTF_8, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); } } diff --git a/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/serializers/ServerMutinyAsyncFileMessageBodyWriter.java b/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/serializers/ServerMutinyAsyncFileMessageBodyWriter.java index 2262c937af40e..ec06ca2cc35bd 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/serializers/ServerMutinyAsyncFileMessageBodyWriter.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/main/java/org/jboss/resteasy/reactive/server/vertx/serializers/ServerMutinyAsyncFileMessageBodyWriter.java @@ -65,7 +65,9 @@ public void writeResponse(AsyncFile file, Type genericType, ServerRequestContext file.endHandler(new Runnable() { @Override public void run() { - file.close(); + // we don't need to wait for the file to be closed, we just need to make sure it does get closed + //noinspection ResultOfMethodCallIgnored + file.close().subscribeAsCompletionStage(); response.end(); // Not sure if I need to resume, actually ctx.resume();