diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ExecutionMode.java b/core/runtime/src/main/java/io/quarkus/runtime/ExecutionMode.java index 5dfd5bb68d2d1..27ddbc6ffb0b8 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/ExecutionMode.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/ExecutionMode.java @@ -6,7 +6,7 @@ public enum ExecutionMode { /** - * Static initializiation. + * Static initialization. */ STATIC_INIT, diff --git a/devtools/maven/src/main/java/io/quarkus/maven/GenerateCodeMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/GenerateCodeMojo.java index 4af62b872b353..8f5804e761f3f 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/GenerateCodeMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/GenerateCodeMojo.java @@ -2,7 +2,10 @@ import java.io.IOException; import java.lang.reflect.Method; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Properties; import java.util.function.Consumer; @@ -21,6 +24,7 @@ import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.util.BootstrapUtils; import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.paths.PathCollection; import io.quarkus.paths.PathList; import io.quarkus.runtime.LaunchMode; @@ -108,6 +112,9 @@ void generateCode(PathCollection sourceParents, Consumer sourceRegistrar, // the resolver and re-resolving it as part of the test bootstrap if (test && curatedApplication != null) { var appModel = curatedApplication.getApplicationModel(); + + injectTestJvmArgsFromDependencies(appModel.getRuntimeDependencies()); + closeApplication(LaunchMode.TEST); if (isSerializeTestModel()) { final int workspaceId = getWorkspaceId(); @@ -154,4 +161,79 @@ protected PathCollection getParentDirs(List sourceDirs) { private Path generatedSourcesDir(boolean test) { return test ? buildDir().toPath().resolve("generated-test-sources") : buildDir().toPath().resolve("generated-sources"); } + + private static final String QUARKUS_TEST_JVM_CONFIG = "META-INF/quarkus-test-jvm-config.properties"; + private static final String XX_PREFIX = "xx."; + private static final String STD_PREFIX = "std."; + + private void injectTestJvmArgsFromDependencies(Collection dependencies) { + List args = collectTestJvmArgs(dependencies); + if (args.isEmpty()) { + return; + } + Properties properties = mavenProject().getProperties(); + String argLine = properties.getProperty("argLine", ""); + StringBuilder sb = new StringBuilder(argLine); + for (String arg : args) { + if (!argLine.contains(arg)) { + sb.append(" ").append(arg); + } + } + String newArgLine = sb.toString().trim(); + if (!newArgLine.equals(argLine)) { + properties.setProperty("argLine", newArgLine); + if (getLog().isDebugEnabled()) { + getLog().debug("Injected test JVM args from dependencies: " + args); + } + } + } + + private List collectTestJvmArgs(Collection dependencies) { + List args = new ArrayList<>(); + for (ResolvedDependency dep : dependencies) { + dep.getContentTree().accept(QUARKUS_TEST_JVM_CONFIG, visit -> { + if (visit == null) { + return; + } + try (var is = Files.newInputStream(visit.getPath())) { + Properties jvmConfig = new Properties(); + jvmConfig.load(is); + if (getLog().isDebugEnabled()) { + getLog().debug("Found " + QUARKUS_TEST_JVM_CONFIG + " in " + + dep.getGroupId() + ":" + dep.getArtifactId()); + } + args.addAll(parseJvmConfigProperties(jvmConfig)); + } catch (IOException e) { + getLog().debug("Failed to read " + QUARKUS_TEST_JVM_CONFIG + " from " + + dep.getGroupId() + ":" + dep.getArtifactId() + ": " + e.getMessage()); + } + }); + } + return args; + } + + private static List parseJvmConfigProperties(Properties jvmConfig) { + List args = new ArrayList<>(); + for (String name : jvmConfig.stringPropertyNames()) { + String value = jvmConfig.getProperty(name).trim(); + if (name.startsWith(XX_PREFIX)) { + String optionName = name.substring(XX_PREFIX.length()); + if ("true".equalsIgnoreCase(value)) { + args.add("-XX:+" + optionName); + } else if ("false".equalsIgnoreCase(value)) { + args.add("-XX:-" + optionName); + } else if (!value.isEmpty()) { + args.add("-XX:" + optionName + "=" + value); + } + } else if (name.startsWith(STD_PREFIX)) { + String optionName = name.substring(STD_PREFIX.length()); + if (value.isEmpty()) { + args.add("--" + optionName); + } else { + args.add("--" + optionName + "=" + value); + } + } + } + return args; + } } diff --git a/docs/src/main/asciidoc/deploying-to-openshift-s2i-howto.adoc b/docs/src/main/asciidoc/deploying-to-openshift-s2i-howto.adoc index ead11551e969e..cb56600de8fc0 100644 --- a/docs/src/main/asciidoc/deploying-to-openshift-s2i-howto.adoc +++ b/docs/src/main/asciidoc/deploying-to-openshift-s2i-howto.adoc @@ -51,7 +51,7 @@ You can deploy {project-name} applications to {openshift} with Java {jdk-version . Create a directory called `.s2i` at the same level as the `pom.xml` file. . Create a file called `environment` in the `.s2i` directory and add the following content: + -[source] +[source,text] ---- MAVEN_S2I_ARTIFACT_DIRS=target/quarkus-app S2I_SOURCE_DEPLOYMENTS_FILTER=app lib quarkus quarkus-run.jar @@ -66,20 +66,20 @@ JAVA_APP_JAR=/deployments/quarkus-run.jar + * Java {jdk-version-earliest}: + -[source,terminal,subs="attributes+",options="nowrap"] +[source,shell,subs="attributes+",options="nowrap"] ---- oc import-image {name-image-ubi9-open-jdk-17-short} --from={name-image-ubi9-open-jdk-17} --confirm ---- * Java {jdk-version-other}: + -[source,terminal,subs="attributes+",options="nowrap"] +[source,shell,subs="attributes+",options="nowrap"] ---- oc import-image {name-image-ubi9-open-jdk-21-short} --from={name-image-ubi9-open-jdk-21} --confirm ---- + * Java {jdk-version-latest}: + -[source,terminal,subs="attributes+",options="nowrap"] +[source,shell,subs="attributes+",options="nowrap"] ---- oc import-image {name-image-ubi9-open-jdk-25-short} --from={name-image-ubi9-open-jdk-25} --confirm ---- @@ -100,31 +100,31 @@ For information about this image, see link:https://catalog.redhat.com/en/softwar + * Java {jdk-version-earliest}: + -[source,terminal,subs="attributes+",options="nowrap"] +[source,shell,subs="attributes+",options="nowrap"] ---- -oc new-app registry.access.redhat.com/ubi9/openjdk-17~ --name= +oc new-app {name-image-ubi9-open-jdk-17}~ --name= ---- * Java {jdk-version-other}: + -[source,terminal,subs="attributes+",options="nowrap"] +[source,shell,subs="attributes+",options="nowrap"] ---- -oc new-app registry.access.redhat.com/ubi9/openjdk-21~ --name= +oc new-app {name-image-ubi9-open-jdk-21}~ --name= ---- + * Java {jdk-version-latest}: + -[source,terminal,subs="attributes+",options="nowrap"] +[source,shell,subs="attributes+",options="nowrap"] ---- -oc new-app registry.access.redhat.com/ubi9/openjdk-25~ --name= +oc new-app {name-image-ubi9-open-jdk-25}~ --name= ---- + .. Replace `` with the path of the Git repository that hosts your Quarkus project. + For example, for Java {jdk-version-other}: + -[source,terminal,subs="attributes+",options="nowrap"] +[source,shell,subs="attributes+",options="nowrap"] ---- -oc new-app registry.access.redhat.com/ubi9/openjdk-21~https://github.com/johndoe/code-with-quarkus.git --name=code-with-quarkus +oc new-app {name-image-ubi9-open-jdk-21}~https://github.com/johndoe/code-with-quarkus.git --name=code-with-quarkus ---- {nbsp} + @@ -139,14 +139,14 @@ If you are deploying on IBM Z infrastructure, enter `oc new-app ubi9/openjdk-21~ . To deploy an updated version of the project, push changes to the Git repository, and then run: + -[source,terminal,subs="attributes+",options="nowrap"] +[source,shell,options="nowrap"] ---- oc start-build ---- + . To expose a route to the application, run the following command: + -[source,terminal,subs="attributes+"] +[source,shell] ---- oc expose svc ---- @@ -155,13 +155,13 @@ oc expose svc . List the pods associated with your current {openshift} project: + -[source,terminal,subs="attributes+"] +[source,shell] ---- oc get pods ---- . To get the log output for your application's pod, run the following command, replacing `` with the name of the latest pod prefixed by your application name: + -[source,terminal,subs="attributes+"] +[source,shell] ---- oc logs -f ---- diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ApplySecuritySettingsDecorator.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ApplySecuritySettingsDecorator.java index 42ab7566a7f04..f0256817a1e65 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ApplySecuritySettingsDecorator.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/ApplySecuritySettingsDecorator.java @@ -1,22 +1,18 @@ package io.quarkus.kubernetes.deployment; -import java.util.Optional; +import java.util.function.Predicate; import io.dekorate.kubernetes.decorator.Decorator; import io.dekorate.kubernetes.decorator.NamedResourceDecorator; import io.dekorate.kubernetes.decorator.ResourceProvidingDecorator; +import io.fabric8.kubernetes.api.model.ContainerBuilder; import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.fabric8.kubernetes.api.model.PodSecurityContextBuilder; import io.fabric8.kubernetes.api.model.PodSpecFluent; -import io.fabric8.kubernetes.api.model.SELinuxOptions; -import io.fabric8.kubernetes.api.model.SELinuxOptionsBuilder; -import io.fabric8.kubernetes.api.model.SysctlBuilder; -import io.fabric8.kubernetes.api.model.WindowsSecurityContextOptions; -import io.fabric8.kubernetes.api.model.WindowsSecurityContextOptionsBuilder; -public class ApplySecuritySettingsDecorator extends NamedResourceDecorator { +public class ApplySecuritySettingsDecorator extends NamedResourceDecorator> { private final SecurityContextConfig securityContext; + private final Predicate hasNamedContainer = cb -> cb.getName().equals(name); public ApplySecuritySettingsDecorator(String resourceName, SecurityContextConfig securityContext) { super(resourceName); @@ -24,55 +20,28 @@ public ApplySecuritySettingsDecorator(String resourceName, SecurityContextConfig } @Override - public void andThenVisit(PodSpecFluent podSpec, ObjectMeta resourceMeta) { - PodSecurityContextBuilder securityContextBuilder = new PodSecurityContextBuilder(); - - securityContext.runAsUser().ifPresent(securityContextBuilder::withRunAsUser); - securityContext.runAsGroup().ifPresent(securityContextBuilder::withRunAsGroup); - securityContext.runAsNonRoot().ifPresent(securityContextBuilder::withRunAsNonRoot); - securityContext.supplementalGroups().ifPresent(securityContextBuilder::addAllToSupplementalGroups); - securityContext.fsGroup().ifPresent(securityContextBuilder::withFsGroup); - securityContext.sysctls().entrySet().stream() - .map(entry -> new SysctlBuilder().withName(entry.getKey()).withValue(entry.getValue()).build()) - .forEach(securityContextBuilder::addToSysctls); - securityContext.fsGroupChangePolicy().map(e -> e.name()).ifPresent(securityContextBuilder::withFsGroupChangePolicy); - buildSeLinuxOptions().ifPresent(securityContextBuilder::withSeLinuxOptions); - buildWindowsOptions().ifPresent(securityContextBuilder::withWindowsOptions); - - podSpec.withSecurityContext(securityContextBuilder.build()); + public void andThenVisit(PodSpecFluent podSpec, ObjectMeta resourceMeta) { + podSpec.withSecurityContext(securityContext.buildSecurityContext()); + + // configure application container with security options if present + final var maybeReadOnly = securityContext.readOnlyRootFilesystem(); + final var maybeEscalation = securityContext.allowPrivilegeEscalation(); + if (maybeReadOnly.isPresent() || maybeEscalation.isPresent()) { + // create container if absent + if (!podSpec.hasMatchingContainer(hasNamedContainer)) { + podSpec.addNewContainer().withName(name).endContainer(); + } + + final var containerSecContext = podSpec.editMatchingContainer(hasNamedContainer) + .editOrNewSecurityContext(); + maybeReadOnly.ifPresent(containerSecContext::withReadOnlyRootFilesystem); + maybeEscalation.ifPresent(containerSecContext::withAllowPrivilegeEscalation); + containerSecContext.endSecurityContext().endContainer(); + } } @Override public Class[] after() { return new Class[] { ResourceProvidingDecorator.class }; } - - private Optional buildWindowsOptions() { - WindowsSecurityContextOptions item = null; - if (securityContext.windowsOptions().isAnyPropertySet()) { - WindowsSecurityContextOptionsBuilder builder = new WindowsSecurityContextOptionsBuilder(); - securityContext.windowsOptions().gmsaCredentialSpec().ifPresent(builder::withGmsaCredentialSpec); - securityContext.windowsOptions().gmsaCredentialSpecName().ifPresent(builder::withGmsaCredentialSpecName); - securityContext.windowsOptions().hostProcess().ifPresent(builder::withHostProcess); - securityContext.windowsOptions().runAsUserName().ifPresent(builder::withRunAsUserName); - item = builder.build(); - } - - return Optional.ofNullable(item); - } - - private Optional buildSeLinuxOptions() { - SELinuxOptions item = null; - if (securityContext.seLinuxOptions().isAnyPropertySet()) { - SELinuxOptionsBuilder builder = new SELinuxOptionsBuilder(); - securityContext.seLinuxOptions().user().ifPresent(builder::withUser); - securityContext.seLinuxOptions().role().ifPresent(builder::withRole); - securityContext.seLinuxOptions().level().ifPresent(builder::withLevel); - securityContext.seLinuxOptions().type().ifPresent(builder::withType); - item = builder.build(); - } - - return Optional.ofNullable(item); - } - } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/SecurityContextConfig.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/SecurityContextConfig.java index 539b89bde7071..3f780477b3cd5 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/SecurityContextConfig.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/SecurityContextConfig.java @@ -4,6 +4,13 @@ import java.util.Map; import java.util.Optional; +import io.fabric8.kubernetes.api.model.PodSecurityContext; +import io.fabric8.kubernetes.api.model.PodSecurityContextBuilder; +import io.fabric8.kubernetes.api.model.SELinuxOptions; +import io.fabric8.kubernetes.api.model.SELinuxOptionsBuilder; +import io.fabric8.kubernetes.api.model.Sysctl; +import io.fabric8.kubernetes.api.model.WindowsSecurityContextOptions; +import io.fabric8.kubernetes.api.model.WindowsSecurityContextOptionsBuilder; import io.quarkus.runtime.annotations.ConfigDocMapKey; public interface SecurityContextConfig { @@ -55,10 +62,27 @@ public interface SecurityContextConfig { */ Optional fsGroupChangePolicy(); + /** + * Controls whether a process can gain more privileges than its parent process. + * This directly controls whether the {@code no_new_privs} flag gets set on the container process. + * Always true when the container: + *
    + *
  • is run as privileged, or
  • + *
  • has {@code CAP_SYS_ADMIN}
  • + *
+ */ + Optional allowPrivilegeEscalation(); + + /** + * Mounts the container's root filesystem as read-only + */ + Optional readOnlyRootFilesystem(); + default boolean isAnyPropertySet() { return seLinuxOptions().isAnyPropertySet() || windowsOptions().isAnyPropertySet() || runAsUser().isPresent() || runAsGroup().isPresent() || runAsNonRoot().isPresent() || supplementalGroups().isPresent() - || fsGroup().isPresent() || !sysctls().isEmpty() || fsGroupChangePolicy().isPresent(); + || fsGroup().isPresent() || !sysctls().isEmpty() || fsGroupChangePolicy().isPresent() + || allowPrivilegeEscalation().isPresent() || readOnlyRootFilesystem().isPresent(); } interface SeLinuxOptions { @@ -128,4 +152,50 @@ enum PodFSGroupChangePolicy { */ Always; } + + default PodSecurityContext buildSecurityContext() { + PodSecurityContextBuilder securityContextBuilder = new PodSecurityContextBuilder(); + + runAsUser().ifPresent(securityContextBuilder::withRunAsUser); + runAsGroup().ifPresent(securityContextBuilder::withRunAsGroup); + runAsNonRoot().ifPresent(securityContextBuilder::withRunAsNonRoot); + supplementalGroups().ifPresent(securityContextBuilder::addAllToSupplementalGroups); + fsGroup().ifPresent(securityContextBuilder::withFsGroup); + sysctls().entrySet().stream() + .map(e -> new Sysctl(e.getKey(), e.getValue())) + .forEach(securityContextBuilder::addToSysctls); + fsGroupChangePolicy().map(Enum::name).ifPresent(securityContextBuilder::withFsGroupChangePolicy); + buildSeLinuxOptions().ifPresent(securityContextBuilder::withSeLinuxOptions); + buildWindowsOptions().ifPresent(securityContextBuilder::withWindowsOptions); + + return securityContextBuilder.build(); + } + + default Optional buildWindowsOptions() { + final var windowsOptions = windowsOptions(); + if (windowsOptions.isAnyPropertySet()) { + WindowsSecurityContextOptionsBuilder builder = new WindowsSecurityContextOptionsBuilder(); + windowsOptions.gmsaCredentialSpec().ifPresent(builder::withGmsaCredentialSpec); + windowsOptions.gmsaCredentialSpecName().ifPresent(builder::withGmsaCredentialSpecName); + windowsOptions.hostProcess().ifPresent(builder::withHostProcess); + windowsOptions.runAsUserName().ifPresent(builder::withRunAsUserName); + return Optional.of(builder.build()); + } + + return Optional.empty(); + } + + default Optional buildSeLinuxOptions() { + final var seLinuxOptions = seLinuxOptions(); + if (seLinuxOptions.isAnyPropertySet()) { + SELinuxOptionsBuilder builder = new SELinuxOptionsBuilder(); + seLinuxOptions.user().ifPresent(builder::withUser); + seLinuxOptions.role().ifPresent(builder::withRole); + seLinuxOptions.level().ifPresent(builder::withLevel); + seLinuxOptions.type().ifPresent(builder::withType); + return Optional.of(builder.build()); + } + + return Optional.empty(); + } } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/AbstractGeneratedAnnotationTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/AbstractGeneratedAnnotationTest.java new file mode 100644 index 0000000000000..c5f80e72c82d6 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/AbstractGeneratedAnnotationTest.java @@ -0,0 +1,534 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; + +import org.hamcrest.CoreMatchers; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import io.restassured.RestAssured; + +public abstract class AbstractGeneratedAnnotationTest { + + // --- @JsonProperty + @JsonIgnore --- + + @Test + public void testPropertyIgnoreSerialization() { + RestAssured.get("/generated/property-ignore") + .then() + .statusCode(200) + .contentType("application/json") + .body("display_name", Matchers.is("Alice")) + .body("years_old", Matchers.is(25)) + .body(not(containsString("secret"))) + .body(not(containsString("hidden-value"))); + } + + @Test + public void testPropertyIgnoreRoundTrip() { + given() + .contentType("application/json") + .body("{\"display_name\":\"Bob\",\"years_old\":30}") + .when() + .post("/generated/property-ignore") + .then() + .statusCode(200) + .contentType("application/json") + .body("display_name", Matchers.is("Bob")) + .body("years_old", Matchers.is(30)) + .body(not(containsString("secret"))); + } + + @Test + public void testPropertyIgnoreList() { + given() + .contentType("application/json") + .body("[{\"display_name\":\"A\",\"years_old\":1},{\"display_name\":\"B\",\"years_old\":2}]") + .when() + .post("/generated/property-ignore-list") + .then() + .statusCode(200) + .contentType("application/json") + .body("[0].display_name", Matchers.is("A")) + .body("[0].years_old", Matchers.is(1)) + .body("[1].display_name", Matchers.is("B")) + .body("[1].years_old", Matchers.is(2)) + .body(not(containsString("secret"))); + } + + // --- @JsonNaming + @JsonProperty + @JsonIgnore --- + + @Test + public void testNamingWithOverrideSerialization() { + RestAssured.get("/generated/naming-override") + .then() + .statusCode(200) + .contentType("application/json") + .body("first_name", Matchers.is("John")) + .body("last_name", Matchers.is("Doe")) + .body("email", Matchers.is("john@example.com")) + .body(not(containsString("internalId"))) + .body(not(containsString("internal_id"))) + .body(not(containsString("INT-001"))); + } + + @Test + public void testNamingWithOverrideRoundTrip() { + given() + .contentType("application/json") + .body("{\"first_name\":\"Jane\",\"last_name\":\"Roe\",\"email\":\"jane@test.com\"}") + .when() + .post("/generated/naming-override") + .then() + .statusCode(200) + .contentType("application/json") + .body("first_name", Matchers.is("Jane")) + .body("last_name", Matchers.is("Roe")) + .body("email", Matchers.is("jane@test.com")) + .body(not(containsString("internalId"))) + .body(not(containsString("internal_id"))); + } + + // --- @JsonCreator + @JsonAlias + @JsonProperty --- + + @Test + public void testCreatorAliasWithPrimaryNames() { + given() + .contentType("application/json") + .body("{\"name\":\"Alice\",\"code\":\"A1\"}") + .when() + .post("/generated/creator-alias") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", Matchers.is("Alice")) + .body("code", Matchers.is("A1")); + } + + @Test + public void testCreatorAliasWithAliasNames() { + given() + .contentType("application/json") + .body("{\"fullName\":\"Bob\",\"identifier\":\"B2\"}") + .when() + .post("/generated/creator-alias") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", Matchers.is("Bob")) + .body("code", Matchers.is("B2")); + } + + @Test + public void testCreatorAliasWithSecondAlias() { + given() + .contentType("application/json") + .body("{\"display_name\":\"Charlie\",\"code\":\"C3\"}") + .when() + .post("/generated/creator-alias") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", Matchers.is("Charlie")) + .body("code", Matchers.is("C3")); + } + + // --- @JsonView + @JsonIgnore --- + + @Test + public void testViewIgnoreWithoutView() { + RestAssured.get("/generated/view-ignore") + .then() + .statusCode(200) + .contentType("application/json") + .body("publicField", Matchers.is("visible")) + .body("privateField", Matchers.is(42)) + .body(not(containsString("ignoredField"))) + .body(not(containsString("ignored-value"))); + } + + @Test + public void testViewIgnorePublicView() { + RestAssured.get("/generated/view-ignore-public") + .then() + .statusCode(200) + .contentType("application/json") + .body("publicField", Matchers.is("visible")) + .body(not(containsString("privateField"))) + .body(not(containsString("ignoredField"))); + } + + @Test + public void testViewIgnorePrivateView() { + RestAssured.get("/generated/view-ignore-private") + .then() + .statusCode(200) + .contentType("application/json") + .body("publicField", Matchers.is("visible")) + .body("privateField", Matchers.is(42)) + .body(not(containsString("ignoredField"))); + } + + // --- @JsonUnwrapped + @JsonProperty + @JsonIgnore --- + + @Test + public void testUnwrappedWithRenameSerialization() { + RestAssured.get("/generated/unwrapped-rename") + .then() + .statusCode(200) + .contentType("application/json") + .body("label", Matchers.is("test")) + .body("city", Matchers.is("NYC")) + .body("zip_code", Matchers.is("10001")) + .body(not(containsString("hidden"))) + .body(not(containsString("secret"))) + .body(not(containsString("address"))); + } + + // --- @JsonAnySetter + @JsonIgnoreProperties + @JsonProperty --- + + @Test + public void testAnySetterIgnoreProperties() { + given() + .contentType("application/json") + .body("{\"id\":\"123\",\"name\":\"test\",\"extra1\":\"val1\",\"removed\":\"gone\",\"deleted\":\"gone\"}") + .when() + .post("/generated/any-setter-ignore-props") + .then() + .statusCode(200) + .contentType("application/json") + .body("id", Matchers.is("123")) + .body("name", Matchers.is("test")) + .body("extras_size", Matchers.is(1)); + } + + @Test + public void testAnySetterIgnorePropertiesOnlyExtras() { + given() + .contentType("application/json") + .body("{\"id\":\"456\",\"name\":\"test2\",\"extra1\":\"a\",\"extra2\":\"b\",\"extra3\":\"c\"}") + .when() + .post("/generated/any-setter-ignore-props") + .then() + .statusCode(200) + .contentType("application/json") + .body("id", Matchers.is("456")) + .body("name", Matchers.is("test2")) + .body("extras_size", Matchers.is(3)); + } + + // --- @JsonTypeInfo + @JsonSubTypes + @JsonTypeName + @JsonProperty --- + + @Test + public void testPolymorphicTextSerialization() { + RestAssured.get("/generated/polymorphic-text") + .then() + .statusCode(200) + .contentType("application/json") + .body("kind", Matchers.is("text")) + .body("text_value", Matchers.is("hello")) + .body("format", Matchers.is("plain")); + } + + @Test + public void testPolymorphicNumberSerialization() { + RestAssured.get("/generated/polymorphic-number") + .then() + .statusCode(200) + .contentType("application/json") + .body("kind", Matchers.is("number")) + .body("num_value", Matchers.is(42)); + } + + @Test + public void testPolymorphicListSerialization() { + RestAssured.get("/generated/polymorphic-list") + .then() + .statusCode(200) + .contentType("application/json") + .body("[0].kind", Matchers.is("text")) + .body("[0].text_value", Matchers.is("first")) + .body("[0].format", Matchers.is("html")) + .body("[1].kind", Matchers.is("number")) + .body("[1].num_value", Matchers.is(99)); + } + + // --- @JsonProperty + @JsonAlias + @JsonIgnoreProperties --- + + @Test + public void testMultiAnnotationRecordWithPrimaryNames() { + given() + .contentType("application/json") + .body("{\"title\":\"My Title\",\"summary\":\"A summary\",\"is_active\":true}") + .when() + .post("/generated/multi-annotation-record") + .then() + .statusCode(200) + .contentType("application/json") + .body("title", Matchers.is("My Title")) + .body("summary", Matchers.is("A summary")) + .body("is_active", Matchers.is(true)); + } + + @Test + public void testMultiAnnotationRecordWithAlias() { + given() + .contentType("application/json") + .body("{\"title\":\"Title\",\"desc\":\"A description\",\"is_active\":false}") + .when() + .post("/generated/multi-annotation-record") + .then() + .statusCode(200) + .contentType("application/json") + .body("title", Matchers.is("Title")) + .body("summary", Matchers.is("A description")) + .body("is_active", Matchers.is(false)); + } + + @Test + public void testMultiAnnotationRecordWithDescriptionAlias() { + given() + .contentType("application/json") + .body("{\"title\":\"T\",\"description\":\"Full desc\",\"is_active\":true}") + .when() + .post("/generated/multi-annotation-record") + .then() + .statusCode(200) + .contentType("application/json") + .body("title", Matchers.is("T")) + .body("summary", Matchers.is("Full desc")) + .body("is_active", Matchers.is(true)); + } + + @Test + public void testMultiAnnotationRecordIgnoresUnknown() { + given() + .contentType("application/json") + .body("{\"title\":\"T\",\"summary\":\"S\",\"is_active\":true,\"unknown_field\":\"ignored\"}") + .when() + .post("/generated/multi-annotation-record") + .then() + .statusCode(200) + .contentType("application/json") + .body("title", Matchers.is("T")) + .body("summary", Matchers.is("S")) + .body("is_active", Matchers.is(true)); + } + + // --- @JsonCreator + @JsonIgnore + @JsonProperty --- + + @Test + public void testCreatorIgnoreRoundTrip() { + given() + .contentType("application/json") + .body("{\"name\":\"test\",\"value\":42}") + .when() + .post("/generated/creator-ignore") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", Matchers.is("test")) + .body("value", Matchers.is(42)) + .body(not(containsString("computed"))) + .body(not(containsString("test:42"))); + } + + // --- @JsonValue + @JsonCreator --- + + @Test + public void testValueCreatorSerialization() { + RestAssured.get("/generated/value-creator") + .then() + .statusCode(200) + .body(Matchers.equalTo("\"hello world\"")); + } + + // --- @JsonNaming + @JsonAlias --- + + @Test + public void testNamingAliasSerialization() { + RestAssured.get("/generated/naming-alias") + .then() + .statusCode(200) + .contentType("application/json") + .body("first_name", Matchers.is("John")) + .body("last_name", Matchers.is("Doe")); + } + + @Test + public void testNamingAliasWithNamingStrategyKeys() { + given() + .contentType("application/json") + .body("{\"first_name\":\"Jane\",\"last_name\":\"Roe\"}") + .when() + .post("/generated/naming-alias") + .then() + .statusCode(200) + .contentType("application/json") + .body("first_name", Matchers.is("Jane")) + .body("last_name", Matchers.is("Roe")); + } + + @Test + public void testNamingAliasWithSurnameAlias() { + given() + .contentType("application/json") + .body("{\"first_name\":\"Jane\",\"surname\":\"Roe\"}") + .when() + .post("/generated/naming-alias") + .then() + .statusCode(200) + .contentType("application/json") + .body("first_name", Matchers.is("Jane")) + .body("last_name", Matchers.is("Roe")); + } + + @Test + public void testNamingAliasWithFamilyNameAlias() { + given() + .contentType("application/json") + .body("{\"first_name\":\"Jane\",\"familyName\":\"Roe\"}") + .when() + .post("/generated/naming-alias") + .then() + .statusCode(200) + .contentType("application/json") + .body("first_name", Matchers.is("Jane")) + .body("last_name", Matchers.is("Roe")); + } + + // --- @JsonProperty + @JsonView --- + + @Test + public void testPropertyViewWithoutView() { + RestAssured.get("/generated/property-view") + .then() + .statusCode(200) + .contentType("application/json") + .body("display_name", Matchers.is("Alice")) + .body("secret_code", Matchers.is("SECRET")) + .body("category", Matchers.is("A")); + } + + @Test + public void testPropertyViewPublic() { + RestAssured.get("/generated/property-view-public") + .then() + .statusCode(200) + .contentType("application/json") + .body("display_name", Matchers.is("Alice")) + .body("category", Matchers.is("A")) + .body(not(containsString("secret_code"))) + .body(not(containsString("SECRET"))); + } + + @Test + public void testPropertyViewPrivate() { + RestAssured.get("/generated/property-view-private") + .then() + .statusCode(200) + .contentType("application/json") + .body("display_name", Matchers.is("Alice")) + .body("secret_code", Matchers.is("SECRET")) + .body("category", Matchers.is("A")); + } + + // --- @JsonNaming + @JsonView + @JsonProperty --- + + @Test + public void testNamingViewWithoutView() { + RestAssured.get("/generated/naming-view") + .then() + .statusCode(200) + .contentType("application/json") + .body("first_name", Matchers.is("Jane")) + .body("last_name", Matchers.is("Smith")) + .body("e_mail", Matchers.is("jane@example.com")); + } + + @Test + public void testNamingViewPublic() { + RestAssured.get("/generated/naming-view-public") + .then() + .statusCode(200) + .contentType("application/json") + .body("first_name", Matchers.is("Jane")) + .body("e_mail", Matchers.is("jane@example.com")) + .body(not(containsString("last_name"))) + .body(not(containsString("Smith"))); + } + + @Test + public void testNamingViewPrivate() { + RestAssured.get("/generated/naming-view-private") + .then() + .statusCode(200) + .contentType("application/json") + .body("first_name", Matchers.is("Jane")) + .body("last_name", Matchers.is("Smith")) + .body("e_mail", Matchers.is("jane@example.com")); + } + + // --- @JsonIgnoreProperties + @JsonProperty --- + + @Test + public void testIgnorePropertiesCreatorRecordRoundTrip() { + given() + .contentType("application/json") + .body("{\"name\":\"item\",\"value\":10,\"temp\":\"ignored\",\"debug\":\"ignored\"}") + .when() + .post("/generated/ignore-props-creator") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", Matchers.is("item")) + .body("value", Matchers.is(10)) + .body(not(containsString("temp"))) + .body(not(containsString("debug"))); + } + + @Test + public void testIgnorePropertiesCreatorRecordRejectsUnknown() { + given() + .contentType("application/json") + .body("{\"name\":\"item\",\"value\":10,\"truly_unknown\":\"fail\"}") + .when() + .post("/generated/ignore-props-creator") + .then() + .statusCode(400); + } + + // --- @JsonIgnore + @JsonAnySetter + @JsonProperty --- + + @Test + public void testIgnoreAnySetterWithExtras() { + given() + .contentType("application/json") + .body("{\"name\":\"test\",\"extra1\":\"a\",\"extra2\":\"b\"}") + .when() + .post("/generated/ignore-any-setter") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", CoreMatchers.is("test")) + .body("others_size", CoreMatchers.is(2)); + } + + @Test + public void testIgnoreAnySetterHiddenFieldCapturedByAnySetter() { + // @JsonIgnore makes the property unknown to regular deserialization, + // so @JsonAnySetter captures it along with other unknown keys + given() + .contentType("application/json") + .body("{\"name\":\"test\",\"hidden\":\"secret\",\"extra1\":\"a\"}") + .when() + .post("/generated/ignore-any-setter") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", CoreMatchers.is("test")) + .body("others_size", CoreMatchers.is(2)); + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/AbstractUnsupportedAnnotationTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/AbstractUnsupportedAnnotationTest.java new file mode 100644 index 0000000000000..424b8a5f7d162 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/AbstractUnsupportedAnnotationTest.java @@ -0,0 +1,212 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.hamcrest.CoreMatchers; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import io.restassured.RestAssured; + +public abstract class AbstractUnsupportedAnnotationTest { + + // --- @JsonAnyGetter --- + + @Test + public void testAnyGetterSerialization() { + // @JsonAnyGetter serializes map entries as flat top-level properties + RestAssured.get("/unsupported/any-getter") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", Matchers.is("test")) + .body("color", Matchers.is("red")) + .body("size", Matchers.is("large")) + .body(not(containsString("properties"))); + } + + @Test + public void testAnyGetterDeserialization() { + // Flat properties are captured by @JsonAnySetter + given() + .contentType("application/json") + .body("{\"name\":\"hello\",\"key1\":\"val1\",\"key2\":\"val2\"}") + .when() + .post("/unsupported/any-getter") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", CoreMatchers.is("hello")) + .body("props_size", CoreMatchers.is(2)); + } + + // --- @JsonAutoDetect --- + + @Test + public void testAutoDetectFieldVisibility() { + // With fieldVisibility=ANY and getterVisibility=NONE, fields are serialized directly + RestAssured.get("/unsupported/auto-detect") + .then() + .statusCode(200) + .contentType("application/json") + .body("visibleField", Matchers.is("hello")) + .body("count", Matchers.is(42)); + } + + // --- @JsonManagedReference + @JsonBackReference --- + + @Test + public void testManagedBackReferenceSerialization() { + // Parent serializes child, but child does NOT serialize back-reference to parent + RestAssured.get("/unsupported/managed-reference") + .then() + .statusCode(200) + .contentType("application/json") + .body("parentName", Matchers.is("parent")) + .body("child.childName", Matchers.is("child")) + .body("child", not(hasKey("parent"))); + } + + // --- @JsonFormat --- + + @Test + public void testFormatEnumAsNumberSerialization() { + // @JsonFormat(shape=NUMBER) serializes enum as ordinal + RestAssured.get("/unsupported/format") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", Matchers.is("shape-test")) + .body("shape", Matchers.is(1)); + } + + @Test + public void testFormatEnumAsNumberDeserialization() { + given() + .contentType("application/json") + .body("{\"name\":\"round-trip\",\"shape\":2}") + .when() + .post("/unsupported/format") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", Matchers.is("round-trip")) + .body("shape", Matchers.is(2)); + } + + // --- @JsonGetter + @JsonSetter --- + + @Test + public void testGetterSetterSerialization() { + // @JsonGetter("label") renames the output property + RestAssured.get("/unsupported/getter-setter") + .then() + .statusCode(200) + .contentType("application/json") + .body("label", Matchers.is("test")) + .body("count", Matchers.is(5)) + .body(not(containsString("\"name\""))); + } + + @Test + public void testGetterSetterDeserialization() { + // @JsonSetter("label") accepts the renamed input + given() + .contentType("application/json") + .body("{\"label\":\"hello\",\"count\":10}") + .when() + .post("/unsupported/getter-setter") + .then() + .statusCode(200) + .contentType("application/json") + .body("label", Matchers.is("hello")) + .body("count", Matchers.is(10)); + } + + // --- @JsonIgnoreType --- + + @Test + public void testIgnoreTypeSerialization() { + // Field of @JsonIgnoreType type is excluded from serialization + RestAssured.get("/unsupported/ignore-type") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", Matchers.is("visible")) + .body(not(containsString("metadata"))) + .body(not(containsString("secret-data"))); + } + + // --- @JsonInclude --- + + @Test + public void testIncludeAllFieldsPresent() { + // When all fields are set, all appear in output + RestAssured.get("/unsupported/include-all-set") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", Matchers.is("test")) + .body("nullableField", Matchers.is("present")) + .body("emptyField", Matchers.is("not-empty")); + } + + @Test + public void testIncludeNullAndEmptyExcluded() { + // @JsonInclude(NON_NULL) excludes null fields, + // @JsonInclude(NON_EMPTY) excludes empty strings + RestAssured.get("/unsupported/include-nulls") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", Matchers.is("test")) + .body(not(containsString("nullableField"))) + .body(not(containsString("emptyField"))); + } + + // --- @JsonPropertyOrder --- + + @Test + public void testPropertyOrderSerialization() { + // @JsonPropertyOrder({"zebra","alpha","middle"}) controls output key ordering + String body = RestAssured.get("/unsupported/property-order") + .then() + .statusCode(200) + .contentType("application/json") + .body("alpha", Matchers.is("a")) + .body("middle", Matchers.is("m")) + .body("zebra", Matchers.is("z")) + .extract() + .asString(); + + int zebraPos = body.indexOf("\"zebra\""); + int alphaPos = body.indexOf("\"alpha\""); + int middlePos = body.indexOf("\"middle\""); + assertTrue(zebraPos < alphaPos, "zebra should appear before alpha"); + assertTrue(alphaPos < middlePos, "alpha should appear before middle"); + } + + // --- @JsonRawValue --- + + @Test + public void testRawValueSerialization() { + // @JsonRawValue outputs the string as raw JSON (not escaped) + String body = RestAssured.get("/unsupported/raw-value") + .then() + .statusCode(200) + .contentType("application/json") + .body("name", Matchers.is("test")) + .body("rawJson.nested", Matchers.is("value")) + .body("rawJson.count", Matchers.is(1)) + .extract() + .asString(); + + // Verify the raw JSON is embedded directly, not escaped as a string + assertTrue(body.contains("\"nested\":\"value\""), + "Raw JSON should be embedded directly, not escaped"); + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/AnyGetterBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/AnyGetterBean.java new file mode 100644 index 0000000000000..b648929604d14 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/AnyGetterBean.java @@ -0,0 +1,32 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; + +public class AnyGetterBean { + + private String name; + + private Map properties = new HashMap<>(); + + @JsonAnyGetter + public Map getProperties() { + return properties; + } + + @JsonAnySetter + public void addProperty(String key, String value) { + properties.put(key, value); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/AnySetterIgnorePropertiesBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/AnySetterIgnorePropertiesBean.java new file mode 100644 index 0000000000000..d3b7f11639b96 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/AnySetterIgnorePropertiesBean.java @@ -0,0 +1,44 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties({ "removed", "deleted" }) +public class AnySetterIgnorePropertiesBean { + + @JsonProperty("id") + private String identifier; + + private String name; + + private Map extras = new HashMap<>(); + + @JsonAnySetter + public void addExtra(String key, Object value) { + extras.put(key, value); + } + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getExtras() { + return extras; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/AutoDetectBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/AutoDetectBean.java new file mode 100644 index 0000000000000..3a75a3ac3f2e9 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/AutoDetectBean.java @@ -0,0 +1,18 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; + +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE) +public class AutoDetectBean { + + private String visibleField; + private int count; + + public AutoDetectBean() { + } + + public AutoDetectBean(String visibleField, int count) { + this.visibleField = visibleField; + this.count = count; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/CreatorAliasBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/CreatorAliasBean.java new file mode 100644 index 0000000000000..f4d5603f23205 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/CreatorAliasBean.java @@ -0,0 +1,27 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class CreatorAliasBean { + + private final String name; + private final String code; + + @JsonCreator + public CreatorAliasBean( + @JsonProperty("name") @JsonAlias({ "fullName", "display_name" }) String name, + @JsonProperty("code") @JsonAlias("identifier") String code) { + this.name = name; + this.code = code; + } + + public String getName() { + return name; + } + + public String getCode() { + return code; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/CreatorIgnoreBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/CreatorIgnoreBean.java new file mode 100644 index 0000000000000..73fb18be8f045 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/CreatorIgnoreBean.java @@ -0,0 +1,35 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class CreatorIgnoreBean { + + private final String name; + private final int value; + + @JsonIgnore + private final String computed; + + @JsonCreator + public CreatorIgnoreBean( + @JsonProperty("name") String name, + @JsonProperty("value") int value) { + this.name = name; + this.value = value; + this.computed = name + ":" + value; + } + + public String getName() { + return name; + } + + public int getValue() { + return value; + } + + public String getComputed() { + return computed; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/FormatBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/FormatBean.java new file mode 100644 index 0000000000000..65594c56ea2c4 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/FormatBean.java @@ -0,0 +1,27 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonFormat; + +public class FormatBean { + + private String name; + + @JsonFormat(shape = JsonFormat.Shape.NUMBER) + private FormatShape shape; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public FormatShape getShape() { + return shape; + } + + public void setShape(FormatShape shape) { + this.shape = shape; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/FormatShape.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/FormatShape.java new file mode 100644 index 0000000000000..5223f5579f7bc --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/FormatShape.java @@ -0,0 +1,7 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +public enum FormatShape { + CIRCLE, + SQUARE, + TRIANGLE +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/GeneratedAnnotationResource.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/GeneratedAnnotationResource.java new file mode 100644 index 0000000000000..e416abacd8d2d --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/GeneratedAnnotationResource.java @@ -0,0 +1,279 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import java.util.List; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; + +import com.fasterxml.jackson.annotation.JsonView; + +import io.smallrye.common.annotation.NonBlocking; + +@Path("/generated") +@NonBlocking +public class GeneratedAnnotationResource { + + @ServerExceptionMapper + public Response handleParseException(WebApplicationException e) { + var cause = e.getCause() == null ? e : e.getCause(); + return Response.status(Response.Status.BAD_REQUEST).entity(cause.getMessage()).build(); + } + + // --- PropertyIgnoreBean: @JsonProperty + @JsonIgnore --- + + @GET + @Path("/property-ignore") + public PropertyIgnoreBean getPropertyIgnore() { + PropertyIgnoreBean bean = new PropertyIgnoreBean(); + bean.setName("Alice"); + bean.setSecret("hidden-value"); + bean.setAge(25); + return bean; + } + + @POST + @Path("/property-ignore") + @Consumes(MediaType.APPLICATION_JSON) + public PropertyIgnoreBean echoPropertyIgnore(PropertyIgnoreBean bean) { + return bean; + } + + // --- NamingWithOverrideBean: @JsonNaming + @JsonProperty + @JsonIgnore --- + + @GET + @Path("/naming-override") + public NamingWithOverrideBean getNamingOverride() { + NamingWithOverrideBean bean = new NamingWithOverrideBean(); + bean.setFirstName("John"); + bean.setLastName("Doe"); + bean.setEmailAddress("john@example.com"); + bean.setInternalId("INT-001"); + return bean; + } + + @POST + @Path("/naming-override") + @Consumes(MediaType.APPLICATION_JSON) + public NamingWithOverrideBean echoNamingOverride(NamingWithOverrideBean bean) { + return bean; + } + + // --- CreatorAliasBean: @JsonCreator + @JsonAlias + @JsonProperty --- + + @POST + @Path("/creator-alias") + @Consumes(MediaType.APPLICATION_JSON) + public CreatorAliasBean echoCreatorAlias(CreatorAliasBean bean) { + return bean; + } + + // --- ViewIgnoreBean: @JsonView + @JsonIgnore --- + + @GET + @Path("/view-ignore") + public ViewIgnoreBean getViewIgnore() { + return createViewIgnoreBean(); + } + + @JsonView(GeneratedViews.Public.class) + @GET + @Path("/view-ignore-public") + public ViewIgnoreBean getViewIgnorePublic() { + return createViewIgnoreBean(); + } + + @JsonView(GeneratedViews.Private.class) + @GET + @Path("/view-ignore-private") + public ViewIgnoreBean getViewIgnorePrivate() { + return createViewIgnoreBean(); + } + + private static ViewIgnoreBean createViewIgnoreBean() { + ViewIgnoreBean bean = new ViewIgnoreBean(); + bean.setPublicField("visible"); + bean.setPrivateField(42); + bean.setIgnoredField("ignored-value"); + return bean; + } + + // --- UnwrappedWithRenameBean: @JsonUnwrapped + @JsonProperty + @JsonIgnore --- + + @GET + @Path("/unwrapped-rename") + public UnwrappedWithRenameBean getUnwrappedRename() { + UnwrappedWithRenameBean bean = new UnwrappedWithRenameBean(); + bean.setName("test"); + bean.setHidden("secret"); + UnwrappedWithRenameBean.InnerAddress address = new UnwrappedWithRenameBean.InnerAddress(); + address.setCity("NYC"); + address.setZipCode("10001"); + bean.setAddress(address); + return bean; + } + + // --- AnySetterIgnorePropertiesBean: @JsonAnySetter + @JsonIgnoreProperties + @JsonProperty --- + + @POST + @Path("/any-setter-ignore-props") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public String echoAnySetterIgnoreProperties(AnySetterIgnorePropertiesBean bean) { + return "{\"id\":\"" + bean.getIdentifier() + + "\",\"name\":\"" + bean.getName() + + "\",\"extras_size\":" + bean.getExtras().size() + "}"; + } + + // --- PolymorphicWithPropertyBase: @JsonTypeInfo + @JsonSubTypes + @JsonTypeName + @JsonProperty --- + + @GET + @Path("/polymorphic-text") + public PolymorphicWithPropertyBase getPolymorphicText() { + return new PolymorphicWithPropertyBase.TextItem("hello", "plain"); + } + + @GET + @Path("/polymorphic-number") + public PolymorphicWithPropertyBase getPolymorphicNumber() { + return new PolymorphicWithPropertyBase.NumberItem(42); + } + + // --- MultiAnnotationRecord: @JsonProperty + @JsonAlias + @JsonIgnoreProperties --- + + @POST + @Path("/multi-annotation-record") + @Consumes(MediaType.APPLICATION_JSON) + public MultiAnnotationRecord echoMultiAnnotationRecord(MultiAnnotationRecord record) { + return record; + } + + // --- CreatorIgnoreBean: @JsonCreator + @JsonIgnore + @JsonProperty --- + + @POST + @Path("/creator-ignore") + @Consumes(MediaType.APPLICATION_JSON) + public CreatorIgnoreBean echoCreatorIgnore(CreatorIgnoreBean bean) { + return bean; + } + + // --- ValueCreatorWrapper: @JsonValue + @JsonCreator --- + + @GET + @Path("/value-creator") + @Produces(MediaType.APPLICATION_JSON) + public ValueCreatorWrapper getValueCreator() { + return new ValueCreatorWrapper("hello world"); + } + + // --- NamingAliasRecord: @JsonNaming + @JsonAlias --- + + @GET + @Path("/naming-alias") + public NamingAliasRecord getNamingAlias() { + return new NamingAliasRecord("John", "Doe"); + } + + @POST + @Path("/naming-alias") + @Consumes(MediaType.APPLICATION_JSON) + public NamingAliasRecord echoNamingAlias(NamingAliasRecord record) { + return record; + } + + // --- PropertyViewRecord: @JsonProperty + @JsonView --- + + @GET + @Path("/property-view") + public PropertyViewRecord getPropertyView() { + return new PropertyViewRecord("Alice", "SECRET", "A"); + } + + @JsonView(GeneratedViews.Public.class) + @GET + @Path("/property-view-public") + public PropertyViewRecord getPropertyViewPublic() { + return new PropertyViewRecord("Alice", "SECRET", "A"); + } + + @JsonView(GeneratedViews.Private.class) + @GET + @Path("/property-view-private") + public PropertyViewRecord getPropertyViewPrivate() { + return new PropertyViewRecord("Alice", "SECRET", "A"); + } + + // --- NamingViewBean: @JsonNaming + @JsonView + @JsonProperty --- + + @GET + @Path("/naming-view") + public NamingViewBean getNamingView() { + return createNamingViewBean(); + } + + @JsonView(GeneratedViews.Public.class) + @GET + @Path("/naming-view-public") + public NamingViewBean getNamingViewPublic() { + return createNamingViewBean(); + } + + @JsonView(GeneratedViews.Private.class) + @GET + @Path("/naming-view-private") + public NamingViewBean getNamingViewPrivate() { + return createNamingViewBean(); + } + + private static NamingViewBean createNamingViewBean() { + NamingViewBean bean = new NamingViewBean(); + bean.setFirstName("Jane"); + bean.setLastName("Smith"); + bean.setEmail("jane@example.com"); + return bean; + } + + // --- IgnorePropertiesCreatorRecord: @JsonIgnoreProperties + @JsonProperty --- + + @POST + @Path("/ignore-props-creator") + @Consumes(MediaType.APPLICATION_JSON) + public IgnorePropertiesCreatorRecord echoIgnorePropsCreator(IgnorePropertiesCreatorRecord record) { + return record; + } + + // --- IgnoreAnySetterBean: @JsonIgnore + @JsonAnySetter + @JsonProperty --- + + @POST + @Path("/ignore-any-setter") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public String echoIgnoreAnySetter(IgnoreAnySetterBean bean) { + return "{\"name\":\"" + bean.getName() + + "\",\"others_size\":" + bean.getOthers().size() + "}"; + } + + // --- Lists/collections of annotated types --- + + @POST + @Path("/property-ignore-list") + @Consumes(MediaType.APPLICATION_JSON) + public List echoPropertyIgnoreList(List list) { + return list; + } + + @GET + @Path("/polymorphic-list") + public List getPolymorphicList() { + return List.of( + new PolymorphicWithPropertyBase.TextItem("first", "html"), + new PolymorphicWithPropertyBase.NumberItem(99)); + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/GeneratedAnnotationTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/GeneratedAnnotationTest.java new file mode 100644 index 0000000000000..4aa5347a2cdee --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/GeneratedAnnotationTest.java @@ -0,0 +1,46 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +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.extension.RegisterExtension; + +import io.quarkus.test.QuarkusExtensionTest; + +public class GeneratedAnnotationTest extends AbstractGeneratedAnnotationTest { + + @RegisterExtension + static QuarkusExtensionTest test = new QuarkusExtensionTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(GeneratedAnnotationResource.class, + GeneratedViews.class, + PropertyIgnoreBean.class, + NamingWithOverrideBean.class, + CreatorAliasBean.class, + ViewIgnoreBean.class, + UnwrappedWithRenameBean.class, + UnwrappedWithRenameBean.InnerAddress.class, + AnySetterIgnorePropertiesBean.class, + PolymorphicWithPropertyBase.class, + PolymorphicWithPropertyBase.TextItem.class, + PolymorphicWithPropertyBase.NumberItem.class, + MultiAnnotationRecord.class, + CreatorIgnoreBean.class, + ValueCreatorWrapper.class, + NamingAliasRecord.class, + PropertyViewRecord.class, + NamingViewBean.class, + IgnorePropertiesCreatorRecord.class, + IgnoreAnySetterBean.class) + .addAsResource(new StringAsset( + "quarkus.jackson.fail-on-unknown-properties=true\n" + + "quarkus.rest.jackson.optimization.enable-reflection-free-serializers=false\n"), + "application.properties"); + } + }); +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/GeneratedAnnotationWithReflectionFreeSerializersTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/GeneratedAnnotationWithReflectionFreeSerializersTest.java new file mode 100644 index 0000000000000..8cf4b3bf2f830 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/GeneratedAnnotationWithReflectionFreeSerializersTest.java @@ -0,0 +1,52 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.Supplier; +import java.util.logging.Level; + +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.extension.RegisterExtension; + +import io.quarkus.test.QuarkusExtensionTest; + +public class GeneratedAnnotationWithReflectionFreeSerializersTest extends AbstractGeneratedAnnotationTest { + + @RegisterExtension + static QuarkusExtensionTest test = new QuarkusExtensionTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(GeneratedAnnotationResource.class, + GeneratedViews.class, + PropertyIgnoreBean.class, + NamingWithOverrideBean.class, + CreatorAliasBean.class, + ViewIgnoreBean.class, + UnwrappedWithRenameBean.class, + UnwrappedWithRenameBean.InnerAddress.class, + AnySetterIgnorePropertiesBean.class, + PolymorphicWithPropertyBase.class, + PolymorphicWithPropertyBase.TextItem.class, + PolymorphicWithPropertyBase.NumberItem.class, + MultiAnnotationRecord.class, + CreatorIgnoreBean.class, + ValueCreatorWrapper.class, + NamingAliasRecord.class, + PropertyViewRecord.class, + NamingViewBean.class, + IgnorePropertiesCreatorRecord.class, + IgnoreAnySetterBean.class) + .addAsResource(new StringAsset( + "quarkus.jackson.fail-on-unknown-properties=true\n" + + "quarkus.rest.jackson.optimization.enable-reflection-free-serializers=true\n"), + "application.properties"); + } + }).setLogRecordPredicate(record -> record.getLevel().equals(Level.INFO) + && record.getLoggerName().equals( + "io.quarkus.resteasy.reactive.jackson.deployment.processor.JacksonCodeGenerator")) + .assertLogRecords(records -> assertThat(records).isEmpty()); +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/GeneratedViews.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/GeneratedViews.java new file mode 100644 index 0000000000000..47fd23a86c95b --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/GeneratedViews.java @@ -0,0 +1,10 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +public class GeneratedViews { + + public static class Public { + } + + public static class Private extends Public { + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/GetterSetterBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/GetterSetterBean.java new file mode 100644 index 0000000000000..479d9f614cd66 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/GetterSetterBean.java @@ -0,0 +1,28 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonSetter; + +public class GetterSetterBean { + + private String name; + private int count; + + @JsonGetter("label") + public String getName() { + return name; + } + + @JsonSetter("label") + public void setName(String name) { + this.name = name; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/IgnoreAnySetterBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/IgnoreAnySetterBean.java new file mode 100644 index 0000000000000..27277e2b51f36 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/IgnoreAnySetterBean.java @@ -0,0 +1,44 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class IgnoreAnySetterBean { + + @JsonProperty("name") + private String name; + + @JsonIgnore + private String hidden; + + private Map others = new HashMap<>(); + + @JsonAnySetter + public void addOther(String key, String value) { + others.put(key, value); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getHidden() { + return hidden; + } + + public void setHidden(String hidden) { + this.hidden = hidden; + } + + public Map getOthers() { + return others; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/IgnorePropertiesCreatorRecord.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/IgnorePropertiesCreatorRecord.java new file mode 100644 index 0000000000000..831e7a9a30eb3 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/IgnorePropertiesCreatorRecord.java @@ -0,0 +1,10 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties({ "temp", "debug" }) +public record IgnorePropertiesCreatorRecord( + @JsonProperty("name") String name, + @JsonProperty("value") int value) { +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/IgnoreTypeBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/IgnoreTypeBean.java new file mode 100644 index 0000000000000..641535b72e28a --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/IgnoreTypeBean.java @@ -0,0 +1,26 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonPropertyOrder({ "name", "metadata" }) +public class IgnoreTypeBean { + + private String name; + private IgnoredType metadata; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public IgnoredType getMetadata() { + return metadata; + } + + public void setMetadata(IgnoredType metadata) { + this.metadata = metadata; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/IgnoredType.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/IgnoredType.java new file mode 100644 index 0000000000000..f9bbebdd1c829 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/IgnoredType.java @@ -0,0 +1,24 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreType; + +@JsonIgnoreType +public class IgnoredType { + + private String secret; + + public IgnoredType() { + } + + public IgnoredType(String secret) { + this.secret = secret; + } + + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/IncludeBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/IncludeBean.java new file mode 100644 index 0000000000000..56ef75098762c --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/IncludeBean.java @@ -0,0 +1,37 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class IncludeBean { + + private String name; + private String nullableField; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private String emptyField; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getNullableField() { + return nullableField; + } + + public void setNullableField(String nullableField) { + this.nullableField = nullableField; + } + + public String getEmptyField() { + return emptyField; + } + + public void setEmptyField(String emptyField) { + this.emptyField = emptyField; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/ManagedReferenceChild.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/ManagedReferenceChild.java new file mode 100644 index 0000000000000..50f2dc230b465 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/ManagedReferenceChild.java @@ -0,0 +1,27 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonBackReference; + +public class ManagedReferenceChild { + + private String childName; + + @JsonBackReference + private ManagedReferenceParent parent; + + public String getChildName() { + return childName; + } + + public void setChildName(String childName) { + this.childName = childName; + } + + public ManagedReferenceParent getParent() { + return parent; + } + + public void setParent(ManagedReferenceParent parent) { + this.parent = parent; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/ManagedReferenceParent.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/ManagedReferenceParent.java new file mode 100644 index 0000000000000..289f2516bad3b --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/ManagedReferenceParent.java @@ -0,0 +1,27 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonManagedReference; + +public class ManagedReferenceParent { + + private String parentName; + + @JsonManagedReference + private ManagedReferenceChild child; + + public String getParentName() { + return parentName; + } + + public void setParentName(String parentName) { + this.parentName = parentName; + } + + public ManagedReferenceChild getChild() { + return child; + } + + public void setChild(ManagedReferenceChild child) { + this.child = child; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/MultiAnnotationRecord.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/MultiAnnotationRecord.java new file mode 100644 index 0000000000000..39b1891675206 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/MultiAnnotationRecord.java @@ -0,0 +1,13 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record MultiAnnotationRecord( + @JsonProperty("title") String name, + @JsonAlias( { + "desc", "description" }) String summary, + @JsonProperty("is_active") boolean active){ +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/NamingAliasRecord.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/NamingAliasRecord.java new file mode 100644 index 0000000000000..23668705e3c8b --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/NamingAliasRecord.java @@ -0,0 +1,12 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record NamingAliasRecord( + String firstName, + @JsonAlias( { + "surname", "familyName" }) String lastName){ +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/NamingViewBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/NamingViewBean.java new file mode 100644 index 0000000000000..31a8015e5d0fa --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/NamingViewBean.java @@ -0,0 +1,43 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class NamingViewBean { + + @JsonView(GeneratedViews.Public.class) + private String firstName; + + @JsonView(GeneratedViews.Private.class) + private String lastName; + + @JsonProperty("e_mail") + private String email; + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/NamingWithOverrideBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/NamingWithOverrideBean.java new file mode 100644 index 0000000000000..2f208b325d8da --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/NamingWithOverrideBean.java @@ -0,0 +1,52 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class NamingWithOverrideBean { + + private String firstName; + + private String lastName; + + @JsonProperty("email") + private String emailAddress; + + @JsonIgnore + private String internalId; + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmailAddress() { + return emailAddress; + } + + public void setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + } + + public String getInternalId() { + return internalId; + } + + public void setInternalId(String internalId) { + this.internalId = internalId; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/PolymorphicWithPropertyBase.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/PolymorphicWithPropertyBase.java new file mode 100644 index 0000000000000..2fdad421659ec --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/PolymorphicWithPropertyBase.java @@ -0,0 +1,25 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "kind") +@JsonSubTypes({ + @JsonSubTypes.Type(value = PolymorphicWithPropertyBase.TextItem.class), + @JsonSubTypes.Type(value = PolymorphicWithPropertyBase.NumberItem.class) +}) +public sealed interface PolymorphicWithPropertyBase { + + @JsonTypeName("text") + record TextItem( + @JsonProperty("text_value") String value, + @JsonProperty("format") String format) implements PolymorphicWithPropertyBase { + } + + @JsonTypeName("number") + record NumberItem( + @JsonProperty("num_value") int value) implements PolymorphicWithPropertyBase { + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/PropertyIgnoreBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/PropertyIgnoreBean.java new file mode 100644 index 0000000000000..5d6f99e91dcd2 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/PropertyIgnoreBean.java @@ -0,0 +1,40 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class PropertyIgnoreBean { + + @JsonProperty("display_name") + private String name; + + @JsonIgnore + private String secret; + + @JsonProperty("years_old") + private int age; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/PropertyOrderBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/PropertyOrderBean.java new file mode 100644 index 0000000000000..1eb5984013daf --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/PropertyOrderBean.java @@ -0,0 +1,35 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonPropertyOrder({ "zebra", "alpha", "middle" }) +public class PropertyOrderBean { + + private String alpha; + private String middle; + private String zebra; + + public String getAlpha() { + return alpha; + } + + public void setAlpha(String alpha) { + this.alpha = alpha; + } + + public String getMiddle() { + return middle; + } + + public void setMiddle(String middle) { + this.middle = middle; + } + + public String getZebra() { + return zebra; + } + + public void setZebra(String zebra) { + this.zebra = zebra; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/PropertyViewRecord.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/PropertyViewRecord.java new file mode 100644 index 0000000000000..6f83c70e4d5cb --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/PropertyViewRecord.java @@ -0,0 +1,10 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonView; + +public record PropertyViewRecord( + @JsonView(GeneratedViews.Public.class) @JsonProperty("display_name") String name, + @JsonView(GeneratedViews.Private.class) @JsonProperty("secret_code") String code, + @JsonProperty("category") String category) { +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/RawValueBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/RawValueBean.java new file mode 100644 index 0000000000000..82951c65019d5 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/RawValueBean.java @@ -0,0 +1,27 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonRawValue; + +public class RawValueBean { + + private String name; + + @JsonRawValue + private String rawJson; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getRawJson() { + return rawJson; + } + + public void setRawJson(String rawJson) { + this.rawJson = rawJson; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/UnsupportedAnnotationResource.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/UnsupportedAnnotationResource.java new file mode 100644 index 0000000000000..5465d8ee4962b --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/UnsupportedAnnotationResource.java @@ -0,0 +1,160 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; + +import io.smallrye.common.annotation.NonBlocking; + +@Path("/unsupported") +@NonBlocking +public class UnsupportedAnnotationResource { + + @ServerExceptionMapper + public Response handleParseException(WebApplicationException e) { + var cause = e.getCause() == null ? e : e.getCause(); + return Response.status(Response.Status.BAD_REQUEST).entity(cause.getMessage()).build(); + } + + // --- @JsonAnyGetter + @JsonAnySetter --- + + @GET + @Path("/any-getter") + public AnyGetterBean getAnyGetter() { + AnyGetterBean bean = new AnyGetterBean(); + bean.setName("test"); + bean.addProperty("color", "red"); + bean.addProperty("size", "large"); + return bean; + } + + @POST + @Path("/any-getter") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public String echoAnyGetter(AnyGetterBean bean) { + return "{\"name\":\"" + bean.getName() + + "\",\"props_size\":" + bean.getProperties().size() + "}"; + } + + // --- @JsonAutoDetect --- + + @GET + @Path("/auto-detect") + public AutoDetectBean getAutoDetect() { + return new AutoDetectBean("hello", 42); + } + + // --- @JsonManagedReference + @JsonBackReference --- + + @GET + @Path("/managed-reference") + public ManagedReferenceParent getManagedReference() { + ManagedReferenceParent parent = new ManagedReferenceParent(); + parent.setParentName("parent"); + ManagedReferenceChild child = new ManagedReferenceChild(); + child.setChildName("child"); + child.setParent(parent); + parent.setChild(child); + return parent; + } + + // --- @JsonFormat --- + + @GET + @Path("/format") + public FormatBean getFormat() { + FormatBean bean = new FormatBean(); + bean.setName("shape-test"); + bean.setShape(FormatShape.SQUARE); + return bean; + } + + @POST + @Path("/format") + @Consumes(MediaType.APPLICATION_JSON) + public FormatBean echoFormat(FormatBean bean) { + return bean; + } + + // --- @JsonGetter + @JsonSetter --- + + @GET + @Path("/getter-setter") + public GetterSetterBean getGetterSetter() { + GetterSetterBean bean = new GetterSetterBean(); + bean.setName("test"); + bean.setCount(5); + return bean; + } + + @POST + @Path("/getter-setter") + @Consumes(MediaType.APPLICATION_JSON) + public GetterSetterBean echoGetterSetter(GetterSetterBean bean) { + return bean; + } + + // --- @JsonIgnoreType --- + + @GET + @Path("/ignore-type") + public IgnoreTypeBean getIgnoreType() { + IgnoreTypeBean bean = new IgnoreTypeBean(); + bean.setName("visible"); + bean.setMetadata(new IgnoredType("secret-data")); + return bean; + } + + // --- @JsonInclude --- + + @GET + @Path("/include-all-set") + public IncludeBean getIncludeAllSet() { + IncludeBean bean = new IncludeBean(); + bean.setName("test"); + bean.setNullableField("present"); + bean.setEmptyField("not-empty"); + return bean; + } + + @GET + @Path("/include-nulls") + public IncludeBean getIncludeNulls() { + IncludeBean bean = new IncludeBean(); + bean.setName("test"); + bean.setNullableField(null); + bean.setEmptyField(""); + return bean; + } + + // --- @JsonPropertyOrder --- + + @GET + @Path("/property-order") + public PropertyOrderBean getPropertyOrder() { + PropertyOrderBean bean = new PropertyOrderBean(); + bean.setAlpha("a"); + bean.setMiddle("m"); + bean.setZebra("z"); + return bean; + } + + // --- @JsonRawValue --- + + @GET + @Path("/raw-value") + public RawValueBean getRawValue() { + RawValueBean bean = new RawValueBean(); + bean.setName("test"); + bean.setRawJson("{\"nested\":\"value\",\"count\":1}"); + return bean; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/UnsupportedAnnotationTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/UnsupportedAnnotationTest.java new file mode 100644 index 0000000000000..dbfc1d08ad9ef --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/UnsupportedAnnotationTest.java @@ -0,0 +1,38 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +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.extension.RegisterExtension; + +import io.quarkus.test.QuarkusExtensionTest; + +public class UnsupportedAnnotationTest extends AbstractUnsupportedAnnotationTest { + + @RegisterExtension + static QuarkusExtensionTest test = new QuarkusExtensionTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(UnsupportedAnnotationResource.class, + AnyGetterBean.class, + AutoDetectBean.class, + ManagedReferenceParent.class, + ManagedReferenceChild.class, + FormatShape.class, + FormatBean.class, + GetterSetterBean.class, + IgnoredType.class, + IgnoreTypeBean.class, + IncludeBean.class, + PropertyOrderBean.class, + RawValueBean.class) + .addAsResource(new StringAsset( + "quarkus.rest.jackson.optimization.enable-reflection-free-serializers=false\n"), + "application.properties"); + } + }); +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/UnsupportedAnnotationWithReflectionFreeSerializersTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/UnsupportedAnnotationWithReflectionFreeSerializersTest.java new file mode 100644 index 0000000000000..40aba67015c71 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/UnsupportedAnnotationWithReflectionFreeSerializersTest.java @@ -0,0 +1,44 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.Supplier; +import java.util.logging.Level; + +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.extension.RegisterExtension; + +import io.quarkus.test.QuarkusExtensionTest; + +public class UnsupportedAnnotationWithReflectionFreeSerializersTest extends AbstractUnsupportedAnnotationTest { + + @RegisterExtension + static QuarkusExtensionTest test = new QuarkusExtensionTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(UnsupportedAnnotationResource.class, + AnyGetterBean.class, + AutoDetectBean.class, + ManagedReferenceParent.class, + ManagedReferenceChild.class, + FormatShape.class, + FormatBean.class, + GetterSetterBean.class, + IgnoredType.class, + IgnoreTypeBean.class, + IncludeBean.class, + PropertyOrderBean.class, + RawValueBean.class) + .addAsResource(new StringAsset( + "quarkus.rest.jackson.optimization.enable-reflection-free-serializers=true\n"), + "application.properties"); + } + }).setLogRecordPredicate(record -> record.getLevel().equals(Level.INFO) + && record.getLoggerName().equals( + "io.quarkus.resteasy.reactive.jackson.deployment.processor.JacksonCodeGenerator")) + .assertLogRecords(records -> assertThat(records).isNotEmpty()); +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/UnwrappedWithRenameBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/UnwrappedWithRenameBean.java new file mode 100644 index 0000000000000..c1e704833bf6c --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/UnwrappedWithRenameBean.java @@ -0,0 +1,65 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; + +public class UnwrappedWithRenameBean { + + @JsonProperty("label") + private String name; + + @JsonUnwrapped + private InnerAddress address; + + @JsonIgnore + private String hidden; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public InnerAddress getAddress() { + return address; + } + + public void setAddress(InnerAddress address) { + this.address = address; + } + + public String getHidden() { + return hidden; + } + + public void setHidden(String hidden) { + this.hidden = hidden; + } + + public static class InnerAddress { + + private String city; + + @JsonProperty("zip_code") + private String zipCode; + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getZipCode() { + return zipCode; + } + + public void setZipCode(String zipCode) { + this.zipCode = zipCode; + } + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/ValueCreatorWrapper.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/ValueCreatorWrapper.java new file mode 100644 index 0000000000000..69542e9af3c1c --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/ValueCreatorWrapper.java @@ -0,0 +1,19 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public class ValueCreatorWrapper { + + private final String data; + + @JsonCreator + public ValueCreatorWrapper(String data) { + this.data = data; + } + + @JsonValue + public String getData() { + return data; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/ViewIgnoreBean.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/ViewIgnoreBean.java new file mode 100644 index 0000000000000..6a3f76e585c9f --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/generated/ViewIgnoreBean.java @@ -0,0 +1,40 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test.generated; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonView; + +public class ViewIgnoreBean { + + @JsonView(GeneratedViews.Public.class) + private String publicField; + + @JsonView(GeneratedViews.Private.class) + private int privateField; + + @JsonIgnore + private String ignoredField; + + public String getPublicField() { + return publicField; + } + + public void setPublicField(String publicField) { + this.publicField = publicField; + } + + public int getPrivateField() { + return privateField; + } + + public void setPrivateField(int privateField) { + this.privateField = privateField; + } + + public String getIgnoredField() { + return ignoredField; + } + + public void setIgnoredField(String ignoredField) { + this.ignoredField = ignoredField; + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithSecurityContextTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithSecurityContextTest.java index b75fbd1942e77..1905cb3ada36f 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithSecurityContextTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithSecurityContextTest.java @@ -60,6 +60,15 @@ public void assertGeneratedResources() throws IOException { assertThat(securityContext.getSupplementalGroups()).containsExactly(125l, 126l); assertThat(securityContext.getFsGroup()).isEqualTo(127); assertThat(securityContext.getFsGroupChangePolicy()).isEqualTo("OnRootMismatch"); + + final var containers = podSpec.getContainers(); + assertThat(containers).hasSize(1); + final var container = containers.get(0); + assertThat(container.getName()).isEqualTo("kubernetes-with-security-context"); + final var containerSecContext = container.getSecurityContext(); + assertThat(containerSecContext).isNotNull(); + assertThat(containerSecContext.getAllowPrivilegeEscalation()).isTrue(); + assertThat(containerSecContext.getReadOnlyRootFilesystem()).isFalse(); }); }); }); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-security-context.properties b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-security-context.properties index 05a70bee0fec5..11aea5c004871 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-security-context.properties +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/resources/kubernetes-with-security-context.properties @@ -11,4 +11,6 @@ quarkus.kubernetes.security-context.run-as-group=124 quarkus.kubernetes.security-context.run-as-non-root=true quarkus.kubernetes.security-context.supplemental-groups=125,126 quarkus.kubernetes.security-context.fs-group=127 -quarkus.kubernetes.security-context.fs-group-change-policy=OnRootMismatch \ No newline at end of file +quarkus.kubernetes.security-context.fs-group-change-policy=OnRootMismatch +quarkus.kubernetes.security-context.allow-privilege-escalation=true +quarkus.kubernetes.security-context.read-only-root-filesystem=false diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/BuildIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/BuildIT.java index 923cc7e2f69c3..17ca63a5e22e0 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/BuildIT.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/BuildIT.java @@ -45,6 +45,21 @@ void testQuarkusBootstrapWorkspaceDiscovery() throws Exception { "[org.acme:acme-common-transitive:1.0-SNAPSHOT, org.acme:acme-common:1.0-SNAPSHOT, org.acme:acme-library:1.0-SNAPSHOT, org.acme:acme-quarkus-ext-deployment:1.0-SNAPSHOT, org.acme:acme-quarkus-ext:1.0-SNAPSHOT]"); } + @Test + void testMockitoNoDynamicAgentWarning() + throws MavenInvocationException, InterruptedException, IOException { + testDir = initProject("projects/mockito-non-public-inner-class", + "projects/mockito-non-public-inner-class-no-warning"); + running = new RunningInvoker(testDir, false); + MavenProcessInvocationResult result = running.execute( + List.of("clean", "verify", "-Dquarkus.analytics.disabled=true"), + Map.of()); + assertThat(result.getProcess().waitFor()).isZero(); + + String log = running.log(); + assertThat(log).doesNotContain("A Java agent has been loaded dynamically"); + } + @Test void testCustomTestSourceSets() throws MavenInvocationException, InterruptedException { diff --git a/test-framework/junit-mockito/src/main/resources/META-INF/quarkus-test-jvm-config.properties b/test-framework/junit-mockito/src/main/resources/META-INF/quarkus-test-jvm-config.properties new file mode 100644 index 0000000000000..1b63e9bbb5c9b --- /dev/null +++ b/test-framework/junit-mockito/src/main/resources/META-INF/quarkus-test-jvm-config.properties @@ -0,0 +1 @@ +xx.EnableDynamicAgentLoading=true