diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 186b103e3e18c..f23973fc842db 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -51,14 +51,14 @@ 3.10.1 4.1.0 4.0.0 - 4.0.2 + 4.0.3 2.11.0 - 6.6.1 - 4.6.0 + 6.6.2 + 4.6.1 2.1.2 1.0.13 3.0.1 - 3.15.0 + 3.16.0 4.25.0 2.7.0 2.1.3 @@ -110,12 +110,12 @@ 2.6.0.Final 2.2.1.Final 3.8.0.Final - 4.5.10 + 4.5.11 4.5.14 4.4.16 4.1.5 9.2.1 - 2.3.2 + 2.5.0 2.3.230 42.7.4 @@ -129,10 +129,10 @@ 1.2.6 2.2 5.10.5 - 15.0.10.Final + 15.0.11.Final 5.0.12.Final 3.1.8 - 4.1.111.Final + 4.1.115.Final 1.16.0 1.0.4 3.6.1.Final diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeMonitoringBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeMonitoringBuildItem.java new file mode 100644 index 0000000000000..a137322ab5ac5 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/NativeMonitoringBuildItem.java @@ -0,0 +1,20 @@ +package io.quarkus.deployment.builditem; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.deployment.pkg.NativeConfig; + +/** + * A build item that indicates whether native monitoring is enabled and which option from {@link NativeConfig.MonitoringOption}. + * To be used in the native image generation. + */ +public final class NativeMonitoringBuildItem extends MultiBuildItem { + private final NativeConfig.MonitoringOption option; + + public NativeMonitoringBuildItem(NativeConfig.MonitoringOption option) { + this.option = option; + } + + public NativeConfig.MonitoringOption getOption() { + return this.option; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveHierarchyBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveHierarchyBuildItem.java index 54b7c35d01ffe..7e3f44d2eaa82 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveHierarchyBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/nativeimage/ReflectiveHierarchyBuildItem.java @@ -24,6 +24,7 @@ * register the following: *

* - Superclasses + * - Subclasses * - Component types of collections * - Types used in bean properties (if method reflection is enabled) * - Field types (if field reflection is enabled) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java index 0903a5b88b568..b160e70246a94 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/BuildTimeConfigurationReader.java @@ -80,9 +80,11 @@ import io.smallrye.config.EnvConfigSource; import io.smallrye.config.ProfileConfigSourceInterceptor; import io.smallrye.config.PropertiesConfigSource; +import io.smallrye.config.PropertyName; import io.smallrye.config.SecretKeys; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.SmallRyeConfigBuilderCustomizer; import io.smallrye.config.SysPropConfigSource; import io.smallrye.config.common.AbstractConfigSource; @@ -626,22 +628,19 @@ ReadResult run() { objectsByClass.put(mapping.getKlass(), config.getConfigMapping(mapping.getKlass(), mapping.getPrefix())); } - // Build Time Values Recording - for (ConfigClass mapping : buildTimeMappings) { - Set mappedProperties = ConfigMappings.mappedProperties(mapping, allProperties); - for (String property : mappedProperties) { + Set buildTimeNames = getMappingsNames(buildTimeMappings); + Set buildTimeRunTimeNames = getMappingsNames(buildTimeRunTimeMappings); + Set runTimeNames = getMappingsNames(runTimeMappings); + for (String property : allProperties) { + PropertyName name = new PropertyName(property); + if (buildTimeNames.contains(name)) { unknownBuildProperties.remove(property); ConfigValue value = config.getConfigValue(property); if (value.getRawValue() != null) { allBuildTimeValues.put(value.getNameProfiled(), value.getRawValue()); } } - } - - // Build Time and Run Time Values Recording - for (ConfigClass mapping : buildTimeRunTimeMappings) { - Set mappedProperties = ConfigMappings.mappedProperties(mapping, allProperties); - for (String property : mappedProperties) { + if (buildTimeRunTimeNames.contains(name)) { unknownBuildProperties.remove(property); ConfigValue value = config.getConfigValue(property); if (value.getRawValue() != null) { @@ -649,12 +648,7 @@ ReadResult run() { buildTimeRunTimeValues.put(value.getNameProfiled(), value.getRawValue()); } } - } - - // Run Time Values Recording - for (ConfigClass mapping : runTimeMappings) { - Set mappedProperties = ConfigMappings.mappedProperties(mapping, allProperties); - for (String property : mappedProperties) { + if (runTimeNames.contains(name)) { unknownBuildProperties.remove(property); ConfigValue value = runtimeConfig.getConfigValue(property); if (value.getRawValue() != null) { @@ -1119,6 +1113,17 @@ private SmallRyeConfig getConfigForRuntimeRecording() { builder.getProfiles().add(""); builder.getSources().clear(); builder.getSourceProviders().clear(); + builder.withCustomizers(new SmallRyeConfigBuilderCustomizer() { + @Override + public void configBuilder(final SmallRyeConfigBuilder builder) { + builder.getMappingsBuilder().getMappings().clear(); + } + + @Override + public int priority() { + return Integer.MAX_VALUE; + } + }); builder.setAddDefaultSources(false) // Customizers may duplicate sources, but not much we can do about it, we need to run them .addDiscoveredCustomizers() @@ -1216,6 +1221,14 @@ private static void getDefaults( patternMap.getChild(childName)); } } + + private static Set getMappingsNames(final List configMappings) { + Set names = new HashSet<>(); + for (ConfigClass configMapping : configMappings) { + names.addAll(ConfigMappings.getProperties(configMapping).keySet()); + } + return PropertiesUtil.toPropertyNames(names); + } } public static final class ReadResult { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java index e82e137f8dd01..0d8e72863855b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/RunTimeConfigurationGenerator.java @@ -66,7 +66,7 @@ import io.smallrye.config.ConfigMappings; import io.smallrye.config.ConfigMappings.ConfigClass; import io.smallrye.config.Converters; -import io.smallrye.config.KeyMap; +import io.smallrye.config.PropertyName; import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; @@ -86,10 +86,13 @@ public final class RunTimeConfigurationGenerator { public static final MethodDescriptor REINIT = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, "reinit", void.class); public static final MethodDescriptor C_READ_CONFIG = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, "readConfig", void.class); + + static final FieldDescriptor C_MAPPED_PROPERTIES = FieldDescriptor.of(CONFIG_CLASS_NAME, "mappedProperties", Set.class); + static final MethodDescriptor C_GENERATE_MAPPED_PROPERTIES = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, + "generateMappedProperties", Set.class); + static final MethodDescriptor PN_NEW = MethodDescriptor.ofConstructor(PropertyName.class, String.class); static final FieldDescriptor C_UNKNOWN = FieldDescriptor.of(CONFIG_CLASS_NAME, "unknown", Set.class); static final FieldDescriptor C_UNKNOWN_RUNTIME = FieldDescriptor.of(CONFIG_CLASS_NAME, "unknownRuntime", Set.class); - static final MethodDescriptor C_MAPPED_PROPERTIES = MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, "mappedProperties", - KeyMap.class); static final MethodDescriptor CD_INVALID_VALUE = MethodDescriptor.ofMethod(ConfigDiagnostic.class, "invalidValue", void.class, String.class, IllegalArgumentException.class); @@ -177,7 +180,6 @@ public final class RunTimeConfigurationGenerator { Object.class, String.class, Converter.class); static final MethodDescriptor SRCB_NEW = MethodDescriptor.ofConstructor(SmallRyeConfigBuilder.class); - static final MethodDescriptor SRCB_WITH_CONVERTER = MethodDescriptor.ofMethod(SmallRyeConfigBuilder.class, "withConverter", ConfigBuilder.class, Class.class, int.class, Converter.class); static final MethodDescriptor SRCB_WITH_CUSTOMIZER = MethodDescriptor.ofMethod(AbstractConfigBuilder.class, @@ -185,15 +187,14 @@ public final class RunTimeConfigurationGenerator { static final MethodDescriptor SRCB_BUILD = MethodDescriptor.ofMethod(SmallRyeConfigBuilder.class, "build", SmallRyeConfig.class); - static final MethodDescriptor PU_FILTER_PROPERTIES_IN_ROOTS = MethodDescriptor.ofMethod(PropertiesUtil.class, - "filterPropertiesInRoots", Iterable.class, Iterable.class, Set.class); - static final MethodDescriptor PU_IS_PROPERTY_QUARKUS_COMPOUND_NAME = MethodDescriptor.ofMethod(PropertiesUtil.class, "isPropertyQuarkusCompoundName", boolean.class, NameIterator.class); - static final MethodDescriptor PU_FILTER_UNKNOWN = MethodDescriptor.ofMethod(PropertiesUtil.class, "filterUnknown", - void.class, Set.class, KeyMap.class); + static final MethodDescriptor PU_IS_PROPERTY_IN_ROOTS = MethodDescriptor.ofMethod(PropertiesUtil.class, "isPropertyInRoots", + boolean.class, String.class, Set.class); static final MethodDescriptor HS_NEW = MethodDescriptor.ofConstructor(HashSet.class); static final MethodDescriptor HS_ADD = MethodDescriptor.ofMethod(HashSet.class, "add", boolean.class, Object.class); + static final MethodDescriptor HS_CONTAINS = MethodDescriptor.ofMethod(HashSet.class, "contains", boolean.class, + Object.class); // todo: more space-efficient sorted map impl static final MethodDescriptor TM_NEW = MethodDescriptor.ofConstructor(TreeMap.class); @@ -261,8 +262,8 @@ public static final class GenerateOperation implements AutoCloseable { roots = Assert.checkNotNullParam("builder.roots", builder.getBuildTimeReadResult().getAllRoots()); additionalTypes = Assert.checkNotNullParam("additionalTypes", builder.getAdditionalTypes()); cc = ClassCreator.builder().classOutput(classOutput).className(CONFIG_CLASS_NAME).setFinal(true).build(); + generateMappedProperties(); generateEmptyParsers(); - generateUnknownFilter(); // not instantiable try (MethodCreator mc = cc.getMethodCreator(MethodDescriptor.ofConstructor(CONFIG_CLASS_NAME))) { mc.setModifiers(Opcodes.ACC_PRIVATE); @@ -280,10 +281,13 @@ public static final class GenerateOperation implements AutoCloseable { clinit = cc.getMethodCreator(MethodDescriptor.ofMethod(CONFIG_CLASS_NAME, "", void.class)); clinit.setModifiers(Opcodes.ACC_STATIC); - cc.getFieldCreator(C_UNKNOWN).setModifiers(Opcodes.ACC_STATIC | Opcodes.ACC_FINAL); + cc.getFieldCreator(C_MAPPED_PROPERTIES).setModifiers(Opcodes.ACC_STATIC); + clinit.writeStaticField(C_MAPPED_PROPERTIES, clinit.invokeStaticMethod(C_GENERATE_MAPPED_PROPERTIES)); + + cc.getFieldCreator(C_UNKNOWN).setModifiers(Opcodes.ACC_STATIC); clinit.writeStaticField(C_UNKNOWN, clinit.newInstance(HS_NEW)); - cc.getFieldCreator(C_UNKNOWN_RUNTIME).setModifiers(Opcodes.ACC_STATIC | Opcodes.ACC_FINAL); + cc.getFieldCreator(C_UNKNOWN_RUNTIME).setModifiers(Opcodes.ACC_STATIC); clinit.writeStaticField(C_UNKNOWN_RUNTIME, clinit.newInstance(HS_NEW)); clinitNameBuilder = clinit.newInstance(SB_NEW); @@ -457,10 +461,6 @@ public void run() { // generate sweep for clinit configSweepLoop(siParserBody, clinit, clinitConfig, getRegisteredRoots(BUILD_AND_RUN_TIME_FIXED), Type.BUILD_TIME); - - clinit.invokeStaticMethod(PU_FILTER_UNKNOWN, - clinit.readStaticField(C_UNKNOWN), - clinit.invokeStaticMethod(C_MAPPED_PROPERTIES)); clinit.invokeStaticMethod(CD_UNKNOWN_PROPERTIES, clinit.readStaticField(C_UNKNOWN)); if (liveReloadPossible) { @@ -468,10 +468,6 @@ public void run() { } // generate sweep for run time configSweepLoop(rtParserBody, readConfig, runTimeConfig, getRegisteredRoots(RUN_TIME), Type.RUNTIME); - - readConfig.invokeStaticMethod(PU_FILTER_UNKNOWN, - readConfig.readStaticField(C_UNKNOWN_RUNTIME), - readConfig.invokeStaticMethod(C_MAPPED_PROPERTIES)); readConfig.invokeStaticMethod(CD_UNKNOWN_PROPERTIES_RT, readConfig.readStaticField(C_UNKNOWN_RUNTIME)); // generate ensure-initialized method @@ -525,33 +521,42 @@ public void run() { private void configSweepLoop(MethodDescriptor parserBody, MethodCreator method, ResultHandle config, Set registeredRoots, Type type) { - ResultHandle nameSet; - ResultHandle iterator; + ResultHandle propertyNames = method.invokeVirtualMethod(SRC_GET_PROPERTY_NAMES, config); + ResultHandle iterator = method.invokeInterfaceMethod(ITRA_ITERATOR, propertyNames); - nameSet = filterProperties(method, config, registeredRoots); - iterator = method.invokeInterfaceMethod(ITRA_ITERATOR, nameSet); + ResultHandle rootSet = method.newInstance(HS_NEW); + for (String registeredRoot : registeredRoots) { + method.invokeVirtualMethod(HS_ADD, rootSet, method.load(registeredRoot)); + } try (BytecodeCreator sweepLoop = method.createScope()) { try (BytecodeCreator hasNext = sweepLoop.ifNonZero(sweepLoop.invokeInterfaceMethod(ITR_HAS_NEXT, iterator)) .trueBranch()) { - ResultHandle key = hasNext.checkCast(hasNext.invokeInterfaceMethod(ITR_NEXT, iterator), String.class); + + // !mappedProperties.contains(new PropertyName(key)) continue sweepLoop; + hasNext.ifNonZero( + hasNext.invokeVirtualMethod(HS_CONTAINS, hasNext.readStaticField(C_MAPPED_PROPERTIES), + hasNext.newInstance(PN_NEW, key))) + .trueBranch().continueScope(sweepLoop); + // NameIterator keyIter = new NameIterator(key); ResultHandle keyIter = hasNext.newInstance(NI_NEW_STRING, key); - BranchResult unknownProperty = hasNext + + // if (PropertiesUtil.isPropertyQuarkusCompoundName(keyIter)) + BranchResult quarkusCompoundName = hasNext .ifNonZero(hasNext.invokeStaticMethod(PU_IS_PROPERTY_QUARKUS_COMPOUND_NAME, keyIter)); - try (BytecodeCreator trueBranch = unknownProperty.trueBranch()) { - ResultHandle unknown; - if (type == Type.BUILD_TIME) { - unknown = trueBranch.readStaticField(C_UNKNOWN); - } else { - unknown = trueBranch.readStaticField(C_UNKNOWN_RUNTIME); - } + try (BytecodeCreator trueBranch = quarkusCompoundName.trueBranch()) { + ResultHandle unknown = type == Type.BUILD_TIME ? trueBranch.readStaticField(C_UNKNOWN) + : trueBranch.readStaticField(C_UNKNOWN_RUNTIME); trueBranch.invokeVirtualMethod(HS_ADD, unknown, key); } + + hasNext.ifNonZero(hasNext.invokeStaticMethod(PU_IS_PROPERTY_IN_ROOTS, key, rootSet)).falseBranch() + .continueScope(sweepLoop); + // if (! keyIter.hasNext()) continue sweepLoop; hasNext.ifNonZero(hasNext.invokeVirtualMethod(NI_HAS_NEXT, keyIter)).falseBranch().continueScope(sweepLoop); - // if (! keyIter.nextSegmentEquals("quarkus")) continue sweepLoop; // parse(config, keyIter); hasNext.invokeStaticMethod(parserBody, config, keyIter); // continue sweepLoop; @@ -560,21 +565,6 @@ private void configSweepLoop(MethodDescriptor parserBody, MethodCreator method, } } - private ResultHandle filterProperties(MethodCreator method, ResultHandle config, Set registeredRoots) { - // Roots - ResultHandle rootSet; - rootSet = method.newInstance(HS_NEW); - for (String registeredRoot : registeredRoots) { - method.invokeVirtualMethod(HS_ADD, rootSet, method.load(registeredRoot)); - } - - // PropertyNames - ResultHandle properties = method.invokeVirtualMethod(SRC_GET_PROPERTY_NAMES, config); - - // Filtered Properties - return method.invokeStaticMethod(PU_FILTER_PROPERTIES_IN_ROOTS, properties, rootSet); - } - private Set getRegisteredRoots(ConfigPhase configPhase) { Set registeredRoots = new HashSet<>(); for (RootDefinition root : roots) { @@ -1190,13 +1180,7 @@ private FieldDescriptor getOrCreateConverterInstance(Field field, ConverterType return fd; } - static final MethodDescriptor KM_NEW = MethodDescriptor.ofConstructor(KeyMap.class); - static final MethodDescriptor KM_FIND_OR_ADD = MethodDescriptor.ofMethod(KeyMap.class, "findOrAdd", KeyMap.class, - String.class); - static final MethodDescriptor KM_PUT_ROOT_VALUE = MethodDescriptor.ofMethod(KeyMap.class, "putRootValue", Object.class, - Object.class); - - private void generateUnknownFilter() { + private void generateMappedProperties() { Set names = new HashSet<>(); for (ConfigClass buildTimeMapping : buildTimeConfigResult.getBuildTimeMappings()) { names.addAll(ConfigMappings.getProperties(buildTimeMapping).keySet()); @@ -1207,17 +1191,15 @@ private void generateUnknownFilter() { for (ConfigClass runtimeConfigMapping : buildTimeConfigResult.getRunTimeMappings()) { names.addAll(ConfigMappings.getProperties(runtimeConfigMapping).keySet()); } + Set propertyNames = PropertiesUtil.toPropertyNames(names); - // Add a method that generates a KeyMap that can check if a property is mapped by a @ConfigMapping - MethodCreator mc = cc.getMethodCreator(C_MAPPED_PROPERTIES); + MethodCreator mc = cc.getMethodCreator(C_GENERATE_MAPPED_PROPERTIES); mc.setModifiers(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC); - ResultHandle keyMap = mc.newInstance(KM_NEW); - for (String name : names) { - mc.invokeVirtualMethod(KM_PUT_ROOT_VALUE, mc.invokeVirtualMethod(KM_FIND_OR_ADD, keyMap, mc.load(name)), - mc.load(true)); + ResultHandle set = mc.newInstance(HS_NEW); + for (PropertyName propertyName : propertyNames) { + mc.invokeVirtualMethod(HS_ADD, set, mc.newInstance(PN_NEW, mc.load(propertyName.getName()))); } - - mc.returnValue(keyMap); + mc.returnValue(set); mc.close(); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java index fb8daf5b727cf..45cd6424d478a 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java @@ -11,10 +11,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -24,6 +26,7 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.NativeImageFeatureBuildItem; +import io.quarkus.deployment.builditem.NativeMonitoringBuildItem; import io.quarkus.deployment.builditem.SuppressNonRuntimeConfigChangedWarningBuildItem; import io.quarkus.deployment.builditem.nativeimage.ExcludeConfigBuildItem; import io.quarkus.deployment.builditem.nativeimage.JPMSExportBuildItem; @@ -115,6 +118,7 @@ ArtifactResultBuildItem nativeSourcesResult(NativeConfig nativeConfig, List nativeImageSecurityProviders, List nativeImageFeatures, NativeImageRunnerBuildItem nativeImageRunner, + List nativeMonitoringBuildItems, CurateOutcomeBuildItem curateOutcomeBuildItem) { Path outputDir; @@ -147,6 +151,7 @@ ArtifactResultBuildItem nativeSourcesResult(NativeConfig nativeConfig, .setGraalVMVersion(GraalVM.Version.CURRENT) .setNativeImageFeatures(nativeImageFeatures) .setContainerBuild(nativeImageRunner.isContainerBuild()) + .setNativeMonitoringOptions(nativeMonitoringBuildItems) .build(); List command = nativeImageArgs.getArgs(); @@ -196,6 +201,7 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, LocalesBuildTimeCon Optional processInheritIODisabledBuildItem, List nativeImageFeatures, Optional nativeImageAgentConfigDirectoryBuildItem, + List nativeMonitoringItems, NativeImageRunnerBuildItem nativeImageRunner) { if (nativeConfig.debug().enabled()) { copyJarSourcesToLib(outputTargetBuildItem, curateOutcomeBuildItem); @@ -254,6 +260,7 @@ public NativeImageBuildItem build(NativeConfig nativeConfig, LocalesBuildTimeCon .setBrokenClasspath(incompleteClassPathAllowed.isAllow()) .setNativeImageSecurityProviders(nativeImageSecurityProviders) .setJPMSExportBuildItems(jpmsExportBuildItems) + .setNativeMonitoringOptions(nativeMonitoringItems) .setEnableModules(enableModules) .setNativeMinimalJavaVersions(nativeMinimalJavaVersions) .setUnsupportedOSes(unsupportedOses) @@ -605,6 +612,7 @@ static class Builder { private List nativeMinimalJavaVersions; private List unsupportedOSes; private List nativeImageFeatures; + private List nativeMonitoringItems; private Path outputDir; private String runnerJarName; private String pie = ""; @@ -713,6 +721,11 @@ public Builder setNativeImageName(String nativeImageName) { return this; } + public Builder setNativeMonitoringOptions(List options) { + this.nativeMonitoringItems = options; + return this; + } + @SuppressWarnings("deprecation") public NativeImageInvokerInfo build() { List nativeImageArgs = new ArrayList<>(); @@ -932,17 +945,23 @@ public NativeImageInvokerInfo build() { nativeImageArgs.add("-march=" + nativeConfig.march().get()); } - List monitoringOptions = new ArrayList<>(); + Set monitoringOptions = new LinkedHashSet<>(); if (!OS.WINDOWS.isCurrent() || containerBuild) { // --enable-monitoring=heapdump is not supported on Windows monitoringOptions.add(NativeConfig.MonitoringOption.HEAPDUMP); } + + if (nativeMonitoringItems != null && !nativeMonitoringItems.isEmpty()) { + monitoringOptions.addAll(nativeMonitoringItems.stream() + .map(NativeMonitoringBuildItem::getOption) + .collect(Collectors.toSet())); + } + if (nativeConfig.monitoring().isPresent()) { monitoringOptions.addAll(nativeConfig.monitoring().get()); } if (!monitoringOptions.isEmpty()) { nativeImageArgs.add("--enable-monitoring=" + monitoringOptions.stream() - .distinct() .map(o -> o.name().toLowerCase(Locale.ROOT)).collect(Collectors.joining(","))); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigDescriptionBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigDescriptionBuildStep.java index 4068bf65b1f16..df0e1ee7947d3 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigDescriptionBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigDescriptionBuildStep.java @@ -13,9 +13,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.OptionalDouble; -import java.util.OptionalInt; -import java.util.OptionalLong; import java.util.Properties; import java.util.Set; import java.util.function.Consumer; @@ -87,7 +84,7 @@ public void accept(Container node) { String defaultDefault; final Class valueClass = field.getType(); - EffectiveConfigTypeAndValues effectiveConfigTypeAndValues = getTypeName(field); + EffectiveConfigTypeAndValues effectiveConfigTypeAndValues = getTypeName(valueClass, field.getGenericType()); if (valueClass == boolean.class) { defaultDefault = "false"; @@ -112,8 +109,8 @@ public void accept(Container node) { ret.add(new ConfigDescriptionBuildItem(name, defVal, javadoc.getProperty(javadocKey), - effectiveConfigTypeAndValues.getTypeName(), - effectiveConfigTypeAndValues.getAllowedValues(), + effectiveConfigTypeAndValues.typeName(), + effectiveConfigTypeAndValues.allowedValues(), configPhase)); } }); @@ -147,136 +144,75 @@ private void processMappings(List mappings, List valueClass = field.getType(); - return getTypeName(field, valueClass); - } - - private EffectiveConfigTypeAndValues getTypeName(Field field, Class valueClass) { - EffectiveConfigTypeAndValues typeAndValues = new EffectiveConfigTypeAndValues(); - String name = valueClass.getName(); + private EffectiveConfigTypeAndValues getTypeName(Class valueClass, Type genericType) { + final String name; + final List allowedValues = new ArrayList<>(); // Extract Optionals, Lists and Sets if ((valueClass.equals(Optional.class) || valueClass.equals(List.class) || valueClass.equals(Set.class))) { - - if (field != null) { - Type genericType = field.getGenericType(); - name = genericType.getTypeName(); + String thisName = valueClass.getName(); + if (genericType != null) { + thisName = genericType.getTypeName(); } - if (name.contains("<") && name.contains(">")) { - name = name.substring(name.lastIndexOf("<") + 1, name.indexOf(">")); + if (thisName.contains("<") && thisName.contains(">")) { + thisName = thisName.substring(thisName.lastIndexOf("<") + 1, thisName.indexOf(">")); } try { - Class c = Class.forName(name); - return getTypeName(null, c); + Class c = Class.forName(thisName); + return getTypeName(c, null); } catch (ClassNotFoundException ex) { // Then we use the name as is. } - } - - // Check other optionals - if (valueClass.equals(OptionalInt.class)) { - name = Integer.class.getName(); - } else if (valueClass.equals(OptionalDouble.class)) { - name = Double.class.getName(); - } else if (valueClass.equals(OptionalLong.class)) { - name = Long.class.getName(); - } - - // Check if this is an enum - if (Enum.class.isAssignableFrom(valueClass)) { + name = thisName; + } else if (Enum.class.isAssignableFrom(valueClass)) { + // Check if this is an enum name = Enum.class.getName(); Object[] values = valueClass.getEnumConstants(); for (Object v : values) { - Enum casted = (Enum) valueClass.cast(v); - typeAndValues.addAllowedValue(casted.name()); + Enum casted = (Enum) valueClass.cast(v); + allowedValues.add(casted.name()); } + } else { + // Map all primitives + name = switch (valueClass.getName()) { + case "java.util.OptionalInt", "int" -> Integer.class.getName(); + case "boolean" -> Boolean.class.getName(); + case "float" -> Float.class.getName(); + case "java.util.OptionalDouble", "double" -> Double.class.getName(); + case "java.util.OptionalLong", "long" -> Long.class.getName(); + case "byte" -> Byte.class.getName(); + case "short" -> Short.class.getName(); + case "char" -> Character.class.getName(); + default -> valueClass.getName(); + }; } // Special case for Log level if (valueClass.isAssignableFrom(Level.class)) { - typeAndValues.addAllowedValue(Level.ALL.getName()); - typeAndValues.addAllowedValue(Level.CONFIG.getName()); - typeAndValues.addAllowedValue(Level.FINE.getName()); - typeAndValues.addAllowedValue(Level.FINER.getName()); - typeAndValues.addAllowedValue(Level.FINEST.getName()); - typeAndValues.addAllowedValue(Level.INFO.getName()); - typeAndValues.addAllowedValue(Level.OFF.getName()); - typeAndValues.addAllowedValue(Level.SEVERE.getName()); - typeAndValues.addAllowedValue(Level.WARNING.getName()); - } - - // Map all primitives - if (name.equals("int")) { - name = Integer.class.getName(); - } else if (name.equals("boolean")) { - name = Boolean.class.getName(); - } else if (name.equals("float")) { - name = Float.class.getName(); - } else if (name.equals("double")) { - name = Double.class.getName(); - } else if (name.equals("long")) { - name = Long.class.getName(); - } else if (name.equals("byte")) { - name = Byte.class.getName(); - } else if (name.equals("short")) { - name = Short.class.getName(); - } else if (name.equals("char")) { - name = Character.class.getName(); - } - - typeAndValues.setTypeName(name); - return typeAndValues; + allowedValues.add(Level.ALL.getName()); + allowedValues.add(Level.CONFIG.getName()); + allowedValues.add(Level.FINE.getName()); + allowedValues.add(Level.FINER.getName()); + allowedValues.add(Level.FINEST.getName()); + allowedValues.add(Level.INFO.getName()); + allowedValues.add(Level.OFF.getName()); + allowedValues.add(Level.SEVERE.getName()); + allowedValues.add(Level.WARNING.getName()); + } + + return new EffectiveConfigTypeAndValues(name, allowedValues); } - static class EffectiveConfigTypeAndValues { - private String typeName; - private List allowedValues; - - public EffectiveConfigTypeAndValues() { - - } - - public EffectiveConfigTypeAndValues(String typeName) { - this.typeName = typeName; - } - - public EffectiveConfigTypeAndValues(String typeName, List allowedValues) { - this.typeName = typeName; - this.allowedValues = allowedValues; - } - - public String getTypeName() { - return typeName; - } - - public void setTypeName(String typeName) { - this.typeName = typeName; - } - - public List getAllowedValues() { - return allowedValues; - } - - public void setAllowedValues(List allowedValues) { - this.allowedValues = allowedValues; - } - - public void addAllowedValue(String v) { - if (allowedValues == null) { - allowedValues = new ArrayList<>(); - } - allowedValues.add(v); - } + private record EffectiveConfigTypeAndValues(String typeName, List allowedValues) { } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/PropertiesUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/PropertiesUtil.java index 0a7b27e0f52cf..18978d1714028 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/configuration/PropertiesUtil.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/PropertiesUtil.java @@ -1,86 +1,14 @@ package io.quarkus.runtime.configuration; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; +import java.util.HashMap; +import java.util.Map; import java.util.Set; -import io.smallrye.config.KeyMap; +import io.smallrye.config.PropertyName; public class PropertiesUtil { private PropertiesUtil() { - } - - /** - * @deprecated Use {@link PropertiesUtil#filterPropertiesInRoots(Iterable, Set)} instead. - */ - @Deprecated(forRemoval = true) - public static boolean isPropertyInRoot(Set roots, NameIterator propertyName) { - for (String root : roots) { - // match everything - if (root.length() == 0) { - return true; - } - - // A sub property from a namespace is always bigger - if (propertyName.getName().length() <= root.length()) { - continue; - } - - final NameIterator rootNi = new NameIterator(root); - // compare segments - while (rootNi.hasNext()) { - String segment = rootNi.getNextSegment(); - if (!propertyName.hasNext()) { - propertyName.goToStart(); - break; - } - - final String nextSegment = propertyName.getNextSegment(); - if (!segment.equals(nextSegment)) { - propertyName.goToStart(); - break; - } - - rootNi.next(); - propertyName.next(); - - // root has no more segments, and we reached this far so everything matched. - // on top, property still has more segments to do the mapping. - if (!rootNi.hasNext() && propertyName.hasNext()) { - propertyName.goToStart(); - return true; - } - } - } - - return false; - } - - public static Iterable filterPropertiesInRoots(final Iterable properties, final Set roots) { - if (roots.isEmpty()) { - return properties; - } - - // Will match everything, so no point in filtering - if (roots.contains("")) { - return properties; - } - - List matchedProperties = new ArrayList<>(); - for (String property : properties) { - // This is a Quarkus compound name, usually by setting something like `quarkus.foo.bar` in the YAML source - // TODO - We let it through to match it later again to place it in the right unknown reporting (static or runtime). We can improve this too. - if (property.startsWith("\"quarkus.")) { - matchedProperties.add(property); - continue; - } - - if (isPropertyInRoots(property, roots)) { - matchedProperties.add(property); - } - } - return matchedProperties; + throw new IllegalStateException("Utility class"); } public static boolean isPropertyInRoots(final String property, final Set roots) { @@ -121,23 +49,27 @@ public static boolean isPropertyQuarkusCompoundName(NameIterator propertyName) { return propertyName.getName().startsWith("\"quarkus."); } - /** - * Removes false positives of configuration properties marked as unknown. To populate the old @ConfigRoot, all - * properties are iterated and matched against known roots. With @ConfigMapping the process is different, so - * properties that are known to @ConfigMapping are not known to the @ConfigRoot, so they will be marked as being - * unknown. It is a bit easier to just double-check on the unknown properties and remove these false positives by - * matching them against the known properties of @ConfigMapping. - * - * @param unknownProperties the collected unknown properties from the old @ConfigRoot mapping - * @param filterPatterns the mapped patterns from the discovered @ConfigMapping - */ - public static void filterUnknown(Set unknownProperties, KeyMap filterPatterns) { - Set toRemove = new HashSet<>(); - for (String unknownProperty : unknownProperties) { - if (filterPatterns.hasRootValue(unknownProperty)) { - toRemove.add(unknownProperty); + public static Set toPropertyNames(final Set names) { + Map propertyNames = new HashMap<>(); + for (String name : names) { + PropertyName propertyName = new PropertyName(name); + if (propertyNames.containsKey(propertyName)) { + String existing = propertyNames.remove(propertyName); + if (existing.length() < name.length()) { + propertyNames.put(new PropertyName(existing), existing); + } else if (existing.length() > name.length()) { + propertyNames.put(propertyName, name); + } else { + if (existing.indexOf('*') <= name.indexOf('*')) { + propertyNames.put(new PropertyName(existing), existing); + } else { + propertyNames.put(propertyName, name); + } + } + } else { + propertyNames.put(propertyName, name); } } - unknownProperties.removeAll(toRemove); + return propertyNames.keySet(); } } diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java index 15d58e6c94f71..0a849539e132b 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/QuarkusPlugin.java @@ -139,10 +139,6 @@ public void apply(Project project) { // Apply the `java` plugin project.getPluginManager().apply(JavaPlugin.class); - project.getGradle().getSharedServices().registerIfAbsent("forcedPropertiesService", ForcedPropertieBuildService.class, - spec -> { - }); - registerModel(); // register extension @@ -156,7 +152,11 @@ public void apply(Project project) { private void registerTasks(Project project, QuarkusPluginExtension quarkusExt) { TaskContainer tasks = project.getTasks(); - + String forcedPropertiesService = String.format("forcedPropertiesService-%s", project.getName()); + Provider serviceProvider = project.getGradle().getSharedServices().registerIfAbsent( + forcedPropertiesService, ForcedPropertieBuildService.class, + spec -> { + }); final String devRuntimeConfigName = ApplicationDeploymentClasspathBuilder .getBaseRuntimeConfigName(LaunchMode.DEVELOPMENT); final Configuration devRuntimeDependencies = project.getConfigurations().maybeCreate(devRuntimeConfigName); @@ -236,19 +236,16 @@ private void registerTasks(Project project, QuarkusPluginExtension quarkusExt) { }); tasks.register(QUARKUS_SHOW_EFFECTIVE_CONFIG_TASK_NAME, QuarkusShowEffectiveConfig.class, task -> { - configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); + configureQuarkusBuildTask(project, task, quarkusBuildAppModelTask, serviceProvider); task.setDescription("Show effective Quarkus build configuration."); }); TaskProvider quarkusBuildDependencies = tasks.register(QUARKUS_BUILD_DEP_TASK_NAME, QuarkusBuildDependencies.class, task -> { - configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); + configureQuarkusBuildTask(project, task, quarkusBuildAppModelTask, serviceProvider); task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); - task.getApplicationModel() - .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); - }); Property cacheLargeArtifacts = quarkusExt.getCacheLargeArtifacts(); @@ -256,7 +253,7 @@ private void registerTasks(Project project, QuarkusPluginExtension quarkusExt) { TaskProvider quarkusBuildCacheableAppParts = tasks.register( QUARKUS_BUILD_APP_PARTS_TASK_NAME, QuarkusBuildCacheableAppParts.class, task -> { - configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); + configureQuarkusBuildTask(project, task, quarkusBuildAppModelTask, serviceProvider); task.dependsOn(quarkusGenerateCode); task.getOutputs().doNotCacheIf( "Not adding uber-jars, native binaries and mutable-jar package type to Gradle " + @@ -272,7 +269,7 @@ public boolean isSatisfiedBy(Task t) { }); TaskProvider quarkusBuild = tasks.register(QUARKUS_BUILD_TASK_NAME, QuarkusBuild.class, build -> { - configureQuarkusBuildTask(project, quarkusExt, build, quarkusBuildAppModelTask); + configureQuarkusBuildTask(project, build, quarkusBuildAppModelTask, serviceProvider); build.dependsOn(quarkusBuildDependencies, quarkusBuildCacheableAppParts); build.getOutputs().doNotCacheIf( "Only collects and combines the outputs of " + QUARKUS_BUILD_APP_PARTS_TASK_NAME + " and " @@ -296,29 +293,20 @@ public boolean isSatisfiedBy(Task t) { tasks.register(IMAGE_BUILD_TASK_NAME, ImageBuild.class, task -> { task.dependsOn(quarkusRequiredExtension); - configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); + configureQuarkusBuildTask(project, task, quarkusBuildAppModelTask, serviceProvider); task.getBuilderName().set(quarkusRequiredExtension.flatMap(ImageCheckRequirementsTask::getOutputFile)); - task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); - task.getApplicationModel() - .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); task.finalizedBy(quarkusBuild); }); tasks.register(IMAGE_PUSH_TASK_NAME, ImagePush.class, task -> { task.dependsOn(quarkusRequiredExtension); - configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); + configureQuarkusBuildTask(project, task, quarkusBuildAppModelTask, serviceProvider); task.getBuilderName().set(quarkusRequiredExtension.flatMap(ImageCheckRequirementsTask::getOutputFile)); - task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); - task.getApplicationModel() - .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); task.finalizedBy(quarkusBuild); }); tasks.register(DEPLOY_TASK_NAME, Deploy.class, task -> { - configureQuarkusBuildTask(project, quarkusExt, task, quarkusBuildAppModelTask); - task.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); - task.getApplicationModel() - .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); + configureQuarkusBuildTask(project, task, quarkusBuildAppModelTask, serviceProvider); task.finalizedBy(quarkusBuild); }); @@ -326,10 +314,7 @@ public boolean isSatisfiedBy(Task t) { quarkusExt); TaskProvider quarkusRun = tasks.register(QUARKUS_RUN_TASK_NAME, QuarkusRun.class, build -> { - configureQuarkusBuildTask(project, quarkusExt, build, quarkusBuildAppModelTask); - build.getOutputs().doNotCacheIf("Dependencies are never cached", t -> true); - build.getApplicationModel() - .set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); + configureQuarkusBuildTask(project, build, quarkusBuildAppModelTask, serviceProvider); build.dependsOn(quarkusBuild); }); @@ -527,10 +512,13 @@ private static void configureApplicationModelTask(Project project, QuarkusApplic task.getApplicationModel().set(project.getLayout().getBuildDirectory().file(quarkusModelFile)); } - private static void configureQuarkusBuildTask(Project project, QuarkusPluginExtension quarkusExt, QuarkusBuildTask task, - TaskProvider quarkusGenerateAppModelTask) { + private static void configureQuarkusBuildTask(Project project, QuarkusBuildTask task, + TaskProvider quarkusGenerateAppModelTask, + Provider serviceProvider) { task.getApplicationModel().set(quarkusGenerateAppModelTask.flatMap(QuarkusApplicationModelTask::getApplicationModel)); SourceSet mainSourceSet = getSourceSet(project, SourceSet.MAIN_SOURCE_SET_NAME); + task.getAdditionalForcedProperties().set(serviceProvider); + task.usesService(serviceProvider); task.setCompileClasspath(mainSourceSet.getCompileClasspath().plus(mainSourceSet.getRuntimeClasspath()) .plus(mainSourceSet.getAnnotationProcessorPath()) .plus(mainSourceSet.getResources())); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java index f54010f9aba90..f5f75c86681ed 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java @@ -17,10 +17,10 @@ import org.gradle.api.file.RegularFileProperty; import org.gradle.api.logging.LogLevel; import org.gradle.api.provider.Property; -import org.gradle.api.services.ServiceReference; import org.gradle.api.tasks.Classpath; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.PathSensitive; import org.gradle.api.tasks.PathSensitivity; @@ -48,8 +48,8 @@ public abstract class QuarkusBuildTask extends QuarkusTask { static final String NATIVE_SOURCES = "native-sources"; private final QuarkusPluginExtensionView extensionView; - @ServiceReference("forcedPropertiesService") - abstract Property getAdditionalForcedProperties(); + @Internal + public abstract Property getAdditionalForcedProperties(); QuarkusBuildTask(String description, boolean compatible) { super(description, compatible); @@ -272,7 +272,7 @@ void generateBuild() { .collect(Collectors.joining("\n ", "\n ", ""))); } - WorkQueue workQueue = workQueue(quarkusProperties, getExtensionView().getCodeGenForkOptions().get()); + WorkQueue workQueue = workQueue(quarkusProperties, getExtensionView().getBuildForkOptions().get()); workQueue.submit(BuildWorker.class, params -> { params.getBuildSystemProperties() diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java index 5cc8b1319ac2f..50018c18c965e 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java @@ -551,18 +551,41 @@ protected void modifyDevModeContext(GradleDevModeLauncher.Builder builder) { private void addQuarkusDevModeDeps(GradleDevModeLauncher.Builder builder, ApplicationModel appModel) { - ResolvedDependency coreDeployment = null; - for (ResolvedDependency d : appModel.getDependencies()) { - if (d.isDeploymentCp() && d.getArtifactId().equals("quarkus-core-deployment") - && d.getGroupId().equals("io.quarkus")) { - coreDeployment = d; - break; - } + var devModeDependencyConfiguration = getProject().getConfigurations() + .findByName(ApplicationDeploymentClasspathBuilder.QUARKUS_BOOTSTRAP_RESOLVER_CONFIGURATION); + if (devModeDependencyConfiguration == null) { + final Configuration platformConfig = getProject().getConfigurations().findByName( + ToolingUtils.toPlatformConfigurationName( + ApplicationDeploymentClasspathBuilder.getFinalRuntimeConfigName(LaunchMode.DEVELOPMENT))); + getProject().getConfigurations().register( + ApplicationDeploymentClasspathBuilder.QUARKUS_BOOTSTRAP_RESOLVER_CONFIGURATION, + configuration -> { + configuration.setCanBeConsumed(false); + configuration.extendsFrom(platformConfig); + configuration.getDependencies().add(getQuarkusGradleBootstrapResolver()); + configuration.getDependencies().add(getQuarkusCoreDeployment(appModel)); + }); + devModeDependencyConfiguration = getProject().getConfigurations() + .getByName(ApplicationDeploymentClasspathBuilder.QUARKUS_BOOTSTRAP_RESOLVER_CONFIGURATION); } - if (coreDeployment == null) { - throw new GradleException("Failed to locate io.quarkus:quarkus-core-deployment on the application build classpath"); + + for (ResolvedArtifact appDep : devModeDependencyConfiguration.getResolvedConfiguration().getResolvedArtifacts()) { + ModuleVersionIdentifier artifactId = appDep.getModuleVersion().getId(); + //we only use the launcher for launching from the IDE, we need to exclude it + if (!(artifactId.getGroup().equals("io.quarkus") + && artifactId.getName().equals("quarkus-ide-launcher"))) { + if (artifactId.getGroup().equals("io.quarkus") + && artifactId.getName().equals("quarkus-class-change-agent")) { + builder.jvmArgs("-javaagent:" + appDep.getFile().getAbsolutePath()); + } else { + builder.classpathEntry(ArtifactKey.of(appDep.getModuleVersion().getId().getGroup(), appDep.getName(), + appDep.getClassifier(), appDep.getExtension()), appDep.getFile()); + } + } } + } + private Dependency getQuarkusGradleBootstrapResolver() { final String pomPropsPath = "META-INF/maven/io.quarkus/quarkus-bootstrap-gradle-resolver/pom.properties"; final InputStream devModePomPropsIs = DevModeMain.class.getClassLoader().getResourceAsStream(pomPropsPath); if (devModePomPropsIs == null) { @@ -586,38 +609,26 @@ private void addQuarkusDevModeDeps(GradleDevModeLauncher.Builder builder, Applic if (devModeVersion == null) { throw new GradleException("Classpath resource " + pomPropsPath + " is missing version"); } - Dependency gradleResolverDep = getProject().getDependencies() .create(String.format("%s:%s:%s", devModeGroupId, devModeArtifactId, devModeVersion)); - Dependency coreDeploymentDep = getProject().getDependencies() - .create(String.format("%s:%s:%s", coreDeployment.getGroupId(), coreDeployment.getArtifactId(), - coreDeployment.getVersion())); - - final Configuration devModeDependencyConfiguration = getProject().getConfigurations() - .detachedConfiguration(gradleResolverDep, coreDeploymentDep); - - final String platformConfigName = ToolingUtils.toPlatformConfigurationName( - ApplicationDeploymentClasspathBuilder.getFinalRuntimeConfigName(LaunchMode.DEVELOPMENT)); - final Configuration platformConfig = getProject().getConfigurations().findByName(platformConfigName); - if (platformConfig != null) { - // apply the platforms - devModeDependencyConfiguration.extendsFrom(platformConfig); - } + return gradleResolverDep; + } - for (ResolvedArtifact appDep : devModeDependencyConfiguration.getResolvedConfiguration().getResolvedArtifacts()) { - ModuleVersionIdentifier artifactId = appDep.getModuleVersion().getId(); - //we only use the launcher for launching from the IDE, we need to exclude it - if (!(artifactId.getGroup().equals("io.quarkus") - && artifactId.getName().equals("quarkus-ide-launcher"))) { - if (artifactId.getGroup().equals("io.quarkus") - && artifactId.getName().equals("quarkus-class-change-agent")) { - builder.jvmArgs("-javaagent:" + appDep.getFile().getAbsolutePath()); - } else { - builder.classpathEntry(ArtifactKey.of(appDep.getModuleVersion().getId().getGroup(), appDep.getName(), - appDep.getClassifier(), appDep.getExtension()), appDep.getFile()); - } + private Dependency getQuarkusCoreDeployment(ApplicationModel appModel) { + ResolvedDependency coreDeployment = null; + for (ResolvedDependency d : appModel.getDependencies()) { + if (d.isDeploymentCp() && d.getArtifactId().equals("quarkus-core-deployment") + && d.getGroupId().equals("io.quarkus")) { + coreDeployment = d; + break; } } + if (coreDeployment == null) { + throw new GradleException("Failed to locate io.quarkus:quarkus-core-deployment on the application build classpath"); + } + return getProject().getDependencies() + .create(String.format("%s:%s:%s", coreDeployment.getGroupId(), coreDeployment.getArtifactId(), + coreDeployment.getVersion())); } private void addLocalProject(ResolvedDependency project, GradleDevModeLauncher.Builder builder, Set addeDeps, diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusPluginExtensionView.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusPluginExtensionView.java index f4a8e87aba3d9..9dc97a6c53ea7 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusPluginExtensionView.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusPluginExtensionView.java @@ -64,6 +64,7 @@ public QuarkusPluginExtensionView(Project project, QuarkusPluginExtension extens getCleanupBuildOutput().set(extension.getCleanupBuildOutput()); getFinalName().set(extension.getFinalName()); getCodeGenForkOptions().set(getProviderFactory().provider(() -> extension.codeGenForkOptions)); + getBuildForkOptions().set(getProviderFactory().provider(() -> extension.buildForkOptions)); getIgnoredEntries().set(extension.ignoredEntriesProperty()); getMainResources().setFrom(project.getExtensions().getByType(SourceSetContainer.class).getByName(MAIN_SOURCE_SET_NAME) .getResources().getSourceDirectories()); @@ -127,6 +128,9 @@ private Provider> getQuarkusRelevantProjectProperties(Projec @Nested public abstract ListProperty> getCodeGenForkOptions(); + @Nested + public abstract ListProperty> getBuildForkOptions(); + @Input @Optional public abstract Property getJarEnabled(); diff --git a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java index 70c195738f6bd..7fc07fdf4f152 100644 --- a/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java +++ b/devtools/gradle/gradle-model/src/main/java/io/quarkus/gradle/dependency/ApplicationDeploymentClasspathBuilder.java @@ -37,6 +37,8 @@ public class ApplicationDeploymentClasspathBuilder { + public static final String QUARKUS_BOOTSTRAP_RESOLVER_CONFIGURATION = "quarkusBootstrapResolverConfiguration"; + private static String getLaunchModeAlias(LaunchMode mode) { if (mode == LaunchMode.DEVELOPMENT) { return "Dev"; diff --git a/devtools/gradle/settings.gradle.kts b/devtools/gradle/settings.gradle.kts index b7ac646dd3f37..4d008c259a339 100644 --- a/devtools/gradle/settings.gradle.kts +++ b/devtools/gradle/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.gradle.develocity") version "3.18.1" + id("com.gradle.develocity") version "3.18.2" } develocity { diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index f605744e82af5..b5a95a94b2d9e 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -1375,7 +1375,8 @@ private QuarkusDevModeLauncher newLauncher(String actualDebugPort, String bootst .setPreferPomsFromWorkspace(true) // it's important to set the base directory instead of the POM // which maybe manipulated by a plugin and stored outside the base directory - .setCurrentProject(project.getBasedir().toString()); + .setCurrentProject(project.getBasedir().toString()) + .setEffectiveModelBuilder(BootstrapMavenContextConfig.getEffectiveModelBuilderProperty(projectProperties)); // There are a couple of reasons we don't want to use the original Maven session: // 1) a reload could be triggered by a change in a pom.xml, in which case the Maven session might not be in sync any more with the effective POM; diff --git a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java index 8b3a2d7a4bb44..a2b09b2e7a06c 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/QuarkusBootstrapProvider.java @@ -40,6 +40,7 @@ import io.quarkus.bootstrap.resolver.AppModelResolverException; import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext; +import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContextConfig; import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException; import io.quarkus.bootstrap.resolver.maven.EffectiveModelResolver; import io.quarkus.bootstrap.resolver.maven.IncubatingApplicationModelResolver; @@ -195,7 +196,9 @@ private MavenArtifactResolver artifactResolver(QuarkusBootstrapMojo mojo, Launch .setPreferPomsFromWorkspace(true) .setProjectModelProvider(getProjectMap(mojo.mavenSession())::get) // pass the repositories since Maven extensions could manipulate repository configs - .setRemoteRepositories(mojo.remoteRepositories())); + .setRemoteRepositories(mojo.remoteRepositories()) + .setEffectiveModelBuilder(BootstrapMavenContextConfig + .getEffectiveModelBuilderProperty(mojo.mavenProject().getProperties()))); } // PROD packaging mode with workspace discovery disabled return MavenArtifactResolver.builder() diff --git a/docs/src/main/asciidoc/dev-ui.adoc b/docs/src/main/asciidoc/dev-ui.adoc index dedfcef592f0d..8a1ca192b8840 100644 --- a/docs/src/main/asciidoc/dev-ui.adoc +++ b/docs/src/main/asciidoc/dev-ui.adoc @@ -922,7 +922,7 @@ public JsonArray getAll() { // <2> <1> This example runs nonblocking. We could also return `Uni` <2> The method name `getAll` will be available in the Javascript -https://github.com/quarkusio/quarkus/blob/main/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/devconsole/CacheJsonRPCService.java[Example code] +https://github.com/quarkusio/quarkus/blob/main/extensions/cache/runtime/src/main/java/io/quarkus/cache/runtime/devui/CacheJsonRPCService.java[Example code] *Webcomponent (Javascript) part* diff --git a/docs/src/main/asciidoc/extension-faq.adoc b/docs/src/main/asciidoc/extension-faq.adoc index 957c350541a2e..af7e78193ae4b 100644 --- a/docs/src/main/asciidoc/extension-faq.adoc +++ b/docs/src/main/asciidoc/extension-faq.adoc @@ -14,36 +14,41 @@ TODO: uncomment the above for experimental or tech-preview content. The document header ends at the first blank line. Do not remove the blank line between the header and the abstract summary. //// -## Should you write an extension? +== Should you write an extension? -### Why would I want to write an extension? +=== Why would I want to write an extension? -See the xref:writing-extensions#extension-philosophy[extension philosophy]. - -One useful thing extensions can do is bundle other extensions. +See the xref:writing-extensions.adoc#extension-philosophy[extension philosophy]. +The xref:extension-maturity-matrix.adoc[extension maturity matrix] shows the kinds of capabilities extensions can offer. +Another useful thing extensions can do is bundle other extensions. Have a look at the link:https://quarkus.io/extensions/io.quarkiverse.microprofile/quarkus-microprofile/[Quarkus MicroProfile extension] for an example of aggregator extensions. -### Are there cases an extension isn't necessary? +=== Are there cases an extension isn't necessary? Not every problem needs an extension! If you're just bundling up external libraries (that aren't already extensions) and making minor adjustments, you might not need an extension. For example, plain libraries can create new configuration elements and register classes with Jandex (this link:https://www.loicmathieu.fr/wordpress/en/informatique/quarkus-tip-comment-ne-pas-creer-une-extension-quarkus/[blog shows how]). -## Bytecode transformation +== How do I know what kind of capabilities I might want to include in an extension? + +Have a look at the xref:extension-maturity-matrix.adoc[extension maturity matrix]. + + +== Bytecode transformation -### How can I change the code of things on the classpath? +=== How can I change the code of things on the classpath? A `BytecodeTransformerBuildItem` can be used to manipulate bytecode. For example, see this link:https://quarkus.io/blog/solving-problems-with-extensions/[blog about removed problematic bridge methods from a dependency]. -## CDI +== CDI -### I'm working with CDI, and I don't know how to ... +=== I'm working with CDI, and I don't know how to ... The xref:cdi-integration.adoc[CDI integration guide] presents solutions to a number of CDI-related use cases for extension authors. -### I have transformed a user class to add an injected field, but CDI isn't working +=== I have transformed a user class to add an injected field, but CDI isn't working What happens if an extension transforms a user class using `BytecodeTransformerBuildItem`, and replaces `@jakarta.annotation.Resource` with `@jakarta.inject.Inject`? The field will not be injected by Arc. Debugging will show the transformed class being loaded in the app, but it looks like Arc doesn't see the new code. @@ -54,29 +59,29 @@ The reason is that _all_ Quarkus's bytecode transformations are done after Jande Most extensions use Jandex as a source of truth to find out what to do. Those extensions won't see new/modified endpoints in the bytecode itself. The solution to this limitation is annotation transformers. You should also be aware that while Arc and Quarkus REST honour annotation transformers, not all extensions do. -### Something in my classpath has @Inject annotations, which are confusing CDI. How can I fix that? +=== Something in my classpath has @Inject annotations, which are confusing CDI. How can I fix that? You will need to implement an `AnnotationsTransformer` and strip out out the problematic injection sites. (Remember, if the use case involves CDI, it needs to be an `AnnotationsTransformer`, not a BytecodeTransformer`.) See link:https://quarkus.io/blog/solving-problems-with-extensions-2/[this blog] about on using an `AnnotationsTransformer` extension to clean non `@Inject` annotations from the Airline library so that it can be used in CDI-enabled runtimes. -## Cross-cutting concerns +== Cross-cutting concerns -### How can I redirect application logging to an external service? +=== How can I redirect application logging to an external service? A `LogHandlerBuildItem` is a convenient way to redirect application logs. See this link:https://quarkus.io/blog/quarkus-aws-cloudwatch_extension/[worked example of an extension which directs output to AWS CloudWatch]. -## Build and hosting infrastructure for extensions +== Build and hosting infrastructure for extensions -### Can I use Gradle to build my extension? +=== Can I use Gradle to build my extension? Yes, but it's not the most typical pattern. See the xref:building-my-first-extension.adoc#gradle-setup[Building Your First Extension Guide] for instructions on setting up a Gradle extension. Have a look at the link:https://quarkus.io/extensions/org.jobrunr/quarkus-jobrunr/[JobRunr extension] for an example implementation. -### If I want my extension to be in code.quarkus.io, does it have to be in the Quarkiverse GitHub org? +=== If I want my extension to be in code.quarkus.io, does it have to be in the Quarkiverse GitHub org? Registering an extension in the catalog is independent from where the source code is. The link:https://hub.quarkiverse.io[quarkiverse repository] has some shortcuts to make releasing and testing extensions easier, but any extension can link:https://hub.quarkiverse.io/checklistfornewprojects/#make-your-extension-available-in-the-tooling[register into the catalog]. -### My extension isn't showing up on extensions.quarkus.io +=== My extension isn't showing up on extensions.quarkus.io Every extension in the link:https://github.com/quarkusio/quarkus-extension-catalog/tree/main/extensions[extension catalog] should appear in http://code.quarkus.io, http://extensions.quarkus.io, and the command line tools. The web pages at http://extensions.quarkus.io are refreshed a few times a delay, so there may be a delay in new extensions showing up there. @@ -87,10 +92,10 @@ To debug a missing extension, first: - Check if the extension is listed in the http://https://registry.quarkus.io/q/swagger-ui/#/Client/get_client_extensions_all[Quarkus registry] list of all known extensions - Check if there has been a green link:https://github.com/quarkusio/extensions/actions/workflows/build_and_publish.yml[build of the extensions site] since updating the catalog -## Other topics +== Other topics -### What's the difference between a quickstart and a codestart? +=== What's the difference between a quickstart and a codestart? Both codestarts and quickstarts are designed to help users get coding quickly. A codestarts is a generated application and a quickstart is browsable source code. diff --git a/docs/src/main/asciidoc/extension-maturity-matrix.adoc b/docs/src/main/asciidoc/extension-maturity-matrix.adoc new file mode 100644 index 0000000000000..43dfed1e028e9 --- /dev/null +++ b/docs/src/main/asciidoc/extension-maturity-matrix.adoc @@ -0,0 +1,178 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// +[id="extension-maturity-matrix"] += A maturity matrix for Quarkus extensions +include::_attributes.adoc[] +:diataxis-type: concept +:categories: writing-extensions +:topics: extensions +:summary: Quarkus extensions can do a lot, or a little. This guide explains some of the capabilities extension authors might want to include. +//// +The document header ends at the first blank line. Do not remove the blank line between the header and the abstract summary. +//// + +What makes a good Quarkus extension? What capabilities is a Quarkus extension expected to provide? Of course, it depends on the extension you are building. But, we found a set of attributes common to many extensions. This document explains what they are. + +image::extension-maturity-matrix.svg[A maturity matrix] + +This isn't defining an exact order, even within a single row. Different extensions have different goals, and different developers will have different views on what capabilities are most important. You may wish to (for example) prioritise a fantastic programming model over enhancing your extension's Dev UI tile. That's fine! + +Also, not every step will apply to every extension. For example, you don't need a Dev Service if your extension doesn't depend on external services. + +It's completely OK to publish a first version of an extension that doesn't handle everything. In fact, it's OK if your extension _never_ gets to the more advanced features. This is a suggested pathway, not a minimum feature set. + +Also note that this list only includes the technical features of your extension. +You might also want to think about how you share your extension, and how it presents itself to the world. +The link:https://hub.quarkiverse.io/checklistfornewprojects/[new extension checklist] on the Quarkiverse Hub has a useful list of ways extensions can participate in the ecosystem. +It's also a good idea to spend some time on the metadata in the xref:extension-metadata#quarkus-extension-yaml[`quarkus-extension.yaml` file], which is used by Quarkus tooling. + +Here are some pointers on how to achieve those capabilities. + +== Run modes + +Quarkus applications can be run as a normal jar-based JVM application, +or live-coded in dev mode, or compiled to a native binary. +Each environment places different demands on framework extensions. + +=== Works in JVM mode + +For most extensions, this is the minimum expectation. +When wrapping an existing library, this is usually trivial to achieve; if an extension is providing net-new capability, it might be a bit more work. Quarkus provides tools for xref:writing-extensions.adoc#testing-extensions[unit testing and integration testing] extensions. + +=== Works in dev mode + +In some cases, extra work may be needed to ensure any wrapped libraries can tolerate +dev mode, since the classloading is different and hot reloading can break some assumptions. Extensions may also wish to add some +xref:writing-extensions.adoc#integrating-with-development-mode[special handling for dev mode]. +To add automated tests which validate dev mode, you can xref:writing-extensions.adoc#testing-hot-reload[add tests which extend the `QuarkusDevModeTest`]. + +=== Works as a native application + +For many libraries, native mode support is the primary motivation for creating an extension. See xref:writing-extensions.adoc#native-executable-support[the guide on native executable support] for more discussion about some of the adaptations that might be needed. + +== Developer Joy + +Developer Joy is an important Quarkus principle. +Here are some extension capabilities that contribute to joyful development. + +=== Configuration support + +Extensions should support Quarkus's unified configuration, by xref:writing-extensions.adoc#configuration[integrating with the Quarkus configuration model]. +The Writing Extensions guide has more guidance on xref:writing-extensions.adoc#how-to-expose-configuration[the Quarkus configuration philosophy]. + +=== CDI Beans + +Quarkus extensions should aim to xref:writing-extensions.adoc#expose-your-components-via-cdi[expose components via CDI], so that they can be consumed in a frictionless way by user applications. +Having everything injectable as CDI beans also helps testing, especially xref:getting-started-testing#mock-support[mocking]. + +=== Dev Service + +Dev Services are generally relevant for extensions that "connect" to something, such as databases for datasources, a keycloak instance for security, an Apache Kafka instance for messaging, etc. + +To provide a Dev Service, use the `DevServicesResultBuildItem` build item. See the xref:extension-writing-dev-service.adoc[Dev Services how-to] for more information. + +=== Basic Dev UI + +Every extension gets a tile in the Dev UI. The default tile pulls information from the xref:extension-metadata.adoc[extension metadata], which is another reason to spend a bit of time getting the metadata right. + +Extensions also use Dev UI hooks to present extra information to users. For example, the tile could include a link to an external console, or an internal page which presents simple text metrics. See the xref:dev-ui.adoc[Dev UI for extension developers] guide. + + +=== Rich Dev UI + +Some extensions provide extremely sophisticated Dev UIs. +For example, they might allow users to interact with the running application, xref:dev-ui.adoc#hot-reload[respond to reloads], visualise application metrics, or xref:dev-ui.adoc#add-a-log-to-the-footer[stream an application-specific log]. +The xref:dev-ui.adoc[Dev UI for extension developers] guide also explains these more advanced options. + +=== Joyful programming model + +Quarkus's build-time philosophy means extensions can tidy up API boilerplate and make programming models more concise and expressive. +A good starting point is usually to use + xref:writing-extensions.adoc#scanning-deployments-using-jandex[Jandex] to scan user code for annotations and other markers. +Although providing new, joyful, ways to do things is good, +it's important to not break the normal patterns that users may be familiar with. + +For some inspiration in this area, have a look at xref:logging#simplified-logging[simplified logging], xref:hibernate-orm-panache.adoc[simplified Hibernate ORM with Panache], the xref:rest-client.adoc#query-parameters[`@RestQuery` annotation], or the way Quarkus allows test containers to be used xref:getting-started-dev-services.adoc[without any configuration]. + +=== Codestart application template + +Codestarts are templates which can be used to generate applications for users. +Extensions can xref:extension-codestart.adoc[provide their own codestart templates]. + +== Supersonic subatomic performance + +Extensions should use build-time application knowledge to eliminate wasteful runtime code paths. We call this supersonic subatomic performance. +Because Quarkus moves work to the build stage, Quarkus applications should have fast startup, high throughput, and low memory requirements. Performance tuning is a large subject, but extensions should use build-time application knowledge to eliminate wasteful runtime code paths at runtime. + +=== Static initialization + +Do as much initialization as much as possible statically. +This avoid runtime overhead. + +=== Replace reflection with generated bytecode + +Many Java libraries make heavy use of reflection to delay decisions to run-time. Quarkus aims to improve performance by moving logic to build time, reducing unnecessary dynamism. +Extensions should aim to replace reflection with build-time code. +This is enabled by + xref:writing-extensions.adoc#scanning-deployments-using-jandex[Jandex], an "offline reflection" library. It may also be necessary to do some bytecode transformation of existing libraries. + +For a case study of how to eliminate reflection and what the performance benefits turned out to be, see https://quarkus.io/blog/quarkus-metaprogramming/[reflectionless Jackson serialization] + +=== Virtual thread support + +Not every library is suitable for using with virtual threads, out of the box. +xref:virtual-threads#why-not["Why not virtual threads everywhere?"] explains why. + +To get your library working properly with virtual threads, you should make sure the library is not pinning the carrier thread. + Quarkus has xref:virtual-threads.adoc#testing-virtual-thread-applications[test helpers to do these checks in an automated way]. + For dispatching work, you should use the xref:virtual-threads.adoc#inject-the-virtual-thread-executor[virtual executor managed by Quarkus]. The link:https://quarkus.io/extensions/io.quarkus/quarkus-websockets-next/[WebSockets-next extension] uses the virtual dispatcher and smart dispatch, and is a good example to follow. + +=== Hot path performance optimization + +Although Quarkus offers some unique opportunities for extension performance, extension developers shouldn't forget https://www.linkedin.com/pulse/how-optimize-software-performance-efficiency-subcodevs/[the basics of performance optimization]. + +=== Non-blocking internals + +Quarkus's reactive core is a key contributor to its excellent throughput and scalability. Extensions should consider adopting this model for their own internal operations. + +=== Add Mutiny-based APIs + +For maximum scalability, go beyond the reactive core and enable fully reactive programming, using Mutiny. Most projects that support a reactive programming model offer two distinct extensions, a `-reactive` and a plain one. +See, for example, https://quarkus.io/extensions/io.quarkiverse.quarkus-elasticsearch/quarkus-elasticsearch/[ElasticSearch] and https://quarkus.io/extensions/io.quarkiverse.quarkus-elasticsearch-reactive/quarkus-elasticsearch-reactive/[ElasticSearch Reactive] extensions. + +== Operations + +Developer joy is important, but so are observability, maintainability, and other operational considerations. +Many of these characteristics come by default with the Quarkus framework or https://quarkus.io/extensions/io.quarkus/quarkus-opentelemetry/[observability-focussed extensions]. But extensions can do more. + +=== Logging + +Quarkus uses JBoss Logging as its logging engine, and xref:logging[supports several logging APIs]. (This is normal Java logging, not OpenTelemetry logging.) + +Avoid using errors and warnings for conditions that will not affect normal operation. These outputs can cause false alarms in user monitoring systems. + + +=== Define health endpoints + +Extensions may wish to xref:writing-extensions#extension-defined-endpoints[define library-specific endpoints] for health criteria which are specific to that extension. To add a new endpoint, extensions should produce a `NonApplicationRootPathBuildItem`. + +=== Tracing context + +You should test that OpenTelemetry output for applications using your extension have properly-defined spans. You may need to do extra work to ensure spans are created with the right tracing ID. +For example, extensions which have reactive internals should support xref:duplicated-context.adoc[duplicated contexts] for correct context propagation. + +=== Advanced Kubernetes and containers integration + +Quarkus is designed to be a Kubernetes-native runtime. +Extensions can continue this philosophy by adding library-specific integration points with Kubernetes. +Being Kubernetes-native implies being container-native. At a minimum, extensions should always work well in containers, but extensions may also have opportunities to integrate with the lower levels of the container stack. + + +== References + +- xref:writing-extensions.adoc[Writing your own extension] guide +- xref:building-my-first-extension.adoc[Building your first extension] +- link:https://hub.quarkiverse.io.adoc[The Quarkiverse Hub documentation] \ No newline at end of file diff --git a/docs/src/main/asciidoc/extension-metadata.adoc b/docs/src/main/asciidoc/extension-metadata.adoc index 2fc7a7efe4d02..2bd3af6873a81 100644 --- a/docs/src/main/asciidoc/extension-metadata.adoc +++ b/docs/src/main/asciidoc/extension-metadata.adoc @@ -11,7 +11,7 @@ include::_attributes.adoc[] Quarkus extensions are distributed as Maven JAR artifacts that application and other libraries may depend on. When a Quarkus application project is built, tested or edited using the Quarkus dev tools, Quarkus extension JAR artifacts will be identified on the application classpath by the presence of the Quarkus extension metadata files in them. This document describes the purpose of each Quarkus extension metadata file and its content. -IMPORTANT: Two of the metadata files have the same name but different extensions, `quarkus-extension.yaml` and `quarkus-extension.properties`. It is easy to mix them up, be careful. You will usually edit the YAML file and track it in your SCM. While you _can_ manually manage the properties file, Quarkus will generated it at build if you don't. +IMPORTANT: Two of the metadata files have the same name but different extensions, `quarkus-extension.yaml` and `quarkus-extension.properties`. It is easy to mix them up, be careful. You will usually edit the YAML file and track it in your SCM. While you _can_ manually manage the properties file, Quarkus will generate it at build if you don't. [[quarkus-extension-yaml]] == META-INF/quarkus-extension.yaml diff --git a/docs/src/main/asciidoc/images/extension-maturity-matrix.svg b/docs/src/main/asciidoc/images/extension-maturity-matrix.svg new file mode 100644 index 0000000000000..964d9f7d22e99 --- /dev/null +++ b/docs/src/main/asciidoc/images/extension-maturity-matrix.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + Run modes + Works in dev mode + Works on JVM + Works in native + + + + + + + + + + + + + + + + + + + + + Developer joy + Unified config + CDI bean + Dev Service + Basic Dev UI + Rich Dev UI + Joyful programming model + Codestart template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Static init + Replace reflection + Virtual thread support + Non-blocking internals + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add Mutiny-based API + + + + + + + + + + + + + + + Operations + Logging + Define health endpoints + Tracing context + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Implemented| Not yet done/not in scope + \ No newline at end of file diff --git a/docs/src/main/asciidoc/qute.adoc b/docs/src/main/asciidoc/qute.adoc index 658832463d1d6..f8a91fe6af198 100644 --- a/docs/src/main/asciidoc/qute.adoc +++ b/docs/src/main/asciidoc/qute.adoc @@ -10,11 +10,11 @@ include::_attributes.adoc[] :topics: templating,qute :extensions: io.quarkus:quarkus-qute,io.quarkus:quarkus-resteasy-qute,io.quarkus:quarkus-rest-qute -Qute is a templating engine designed specifically to meet the Quarkus needs. -The usage of reflection is minimized to reduce the size of native images. +Qute is a templating engine developed specifically for Quarkus. +Reflection usage is minimized to reduce the size of native images. The API combines both the imperative and the non-blocking reactive style of coding. -In the development mode, all files located in `src/main/resources/templates` are watched for changes and modifications are immediately visible. -Furthermore, we try to detect most of the template problems at build time. +In development mode, all files located in `src/main/resources/templates` are monitored for changes, and modifications become visible immediately. +Furthermore, we aim to detect most template issues at build time. In this guide, you will learn how to easily render templates in your application. == Solution @@ -65,7 +65,7 @@ Let's start with a Hello World template: NOTE: Templates located in the `pub` directory are served via HTTP. This behavior is built-in, no controllers are needed. For example, the template `src/main/resources/templates/pub/foo.html` will be served from the paths `/foo` and `/foo.html` by default. -If your application is running, you can open your browser and hit: http://localhost:8080/hello?name=Martin +Once your application is running, you can open your browser and navigate to: http://localhost:8080/hello?name=Martin For more information about Qute Web options, see the https://docs.quarkiverse.io/quarkus-qute-web/dev/index.html[Qute Web guide]. @@ -144,7 +144,7 @@ Hello Martin! There's an alternate way to declare your templates in your Java code, which relies on the following convention: -- Organise your template files in the `/src/main/resources/templates` directory, by grouping them into one directory per resource class. So, if +- Organize your template files in the `/src/main/resources/templates` directory, by grouping them into one directory per resource class. So, if your `ItemResource` class references two templates `hello` and `goodbye`, place them at `/src/main/resources/templates/ItemResource/hello.txt` and `/src/main/resources/templates/ItemResource/goodbye.txt`. Grouping templates per resource class makes it easier to navigate to them. - In each of your resource class, declare a `@CheckedTemplate static class Template {}` class within your resource class. @@ -389,7 +389,7 @@ public class ItemResource { *Template extension methods* are used to extend the set of accessible properties of data objects. Sometimes, you're not in control of the classes that you want to use in your template, and you cannot add methods -to them. Template extension methods allows you to declare new method for those classes that will be available +to them. Template extension methods allow you to declare new methods for those classes that will be available from your templates just as if they belonged to the target class. Let's keep extending on our simple HTML page that contains the item name, price and add a discounted price. @@ -442,7 +442,7 @@ grouped by target type, or in a single `TemplateExtensions` class by convention. == Rendering Periodic Reports -Templating engine could be also very useful when rendering periodic reports. +The templating engine can also be very useful for rendering periodic reports. You'll need to add the `quarkus-scheduler` and `quarkus-qute` extensions first. In your `pom.xml` file, add: diff --git a/docs/src/main/asciidoc/security-jwt.adoc b/docs/src/main/asciidoc/security-jwt.adoc index b2687eca370ed..fcc6ab1b97ea2 100644 --- a/docs/src/main/asciidoc/security-jwt.adoc +++ b/docs/src/main/asciidoc/security-jwt.adoc @@ -619,7 +619,7 @@ We suggest that you check out the quickstart solutions and explore the `security == Reference Guide -[supported-injection-scopes] +[[supported-injection-scopes]] === Supported Injection Scopes `@ApplicationScoped`, `@Singleton` and `@RequestScoped` outer bean injection scopes are all supported when an `org.eclipse.microprofile.jwt.JsonWebToken` is injected, with the `@RequestScoped` scoping for `JsonWebToken` enforced to ensure the current token is represented. diff --git a/docs/src/main/asciidoc/security-testing.adoc b/docs/src/main/asciidoc/security-testing.adoc index bed66779c75e3..f89af3f3c4af6 100644 --- a/docs/src/main/asciidoc/security-testing.adoc +++ b/docs/src/main/asciidoc/security-testing.adoc @@ -156,6 +156,50 @@ public String getDetail() { } ---- +It is also possible to set custom permissions like in the example below: + +[source,java] +---- +@PermissionsAllowed("see", permission = CustomPermission.class) +public String getDetail() { + return "detail"; +} +---- + +The `CustomPermission` needs to be granted to the `SecurityIdentity` created +by the `@TestSecurity` annotation with a `SecurityIdentityAugmentor` CDI bean: + +[source,java] +---- +@ApplicationScoped +public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor { + @Override + public Uni augment(SecurityIdentity securityIdentity, + AuthenticationRequestContext authenticationRequestContext) { + final SecurityIdentity augmentedIdentity; + if (shouldGrantCustomPermission(securityIdentity) { + augmentedIdentity = QuarkusSecurityIdentity.builder(securityIdentity) + .addPermission(new CustomPermission("see")).build(); + } else { + augmentedIdentity = securityIdentity; + } + return Uni.createFrom().item(augmentedIdentity); + } +} +---- + +Quarkus will only augment the `SecurityIdentity` created with the `@TestSecurity` annotation if you set +the `@TestSecurity#augmentors` annotation attribute to the `CustomSecurityIdentityAugmentor.class` like this: + +[source,java] +---- +@Test +@TestSecurity(user = "testUser", permissions = "see:detail", augmentors = CustomSecurityIdentityAugmentor.class) +void someTestMethod() { + ... +} +---- + === Mixing security tests If it becomes necessary to test security features using both `@TestSecurity` and Basic Auth (which is the fallback auth diff --git a/docs/src/main/asciidoc/websockets-next-reference.adoc b/docs/src/main/asciidoc/websockets-next-reference.adoc index e90a96fe2284c..23d223caef1ca 100644 --- a/docs/src/main/asciidoc/websockets-next-reference.adoc +++ b/docs/src/main/asciidoc/websockets-next-reference.adoc @@ -4,7 +4,7 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// [id="websockets-next-reference-guide"] -= WebSockets Next extension reference guide += WebSockets Next reference guide :extension-status: preview include::_attributes.adoc[] :numbered: @@ -78,7 +78,7 @@ implementation("io.quarkus:quarkus-websockets-next") == Endpoints -Both the server and client APIs allow you to define _endpoints_ that are used to consume and send messages. +Both the <> and <> define _endpoints_ that are used to consume and send messages. The endpoints are implemented as CDI beans and support injection. Endpoints declare <> annotated with `@OnTextMessage`, `@OnBinaryMessage`, `@OnPong`, `@OnOpen`, `@OnClose` and `@OnError`. These methods are used to handle various WebSocket events. @@ -559,6 +559,7 @@ This means that if an endpoint receives events `A` and `B` (in this particular o However, in some situations it is preferable to process events concurrently, i.e. with no ordering guarantees but also with no concurrency limits. For this cases, the `InboundProcessingMode#CONCURRENT` should be used. +[[server-api]] == Server API === HTTP server configuration @@ -900,14 +901,15 @@ public class CustomTenantResolver implements TenantResolver { ---- For more information on Hibernate multitenancy, refer to the https://quarkus.io/guides/hibernate-orm#multitenancy[hibernate documentation]. +[[client-api]] == Client API [[client-connectors]] === Client connectors -The `io.quarkus.websockets.next.WebSocketConnector` is used to configure and create new connections for client endpoints. -A CDI bean that implements this interface is provided and can be injected in other beans. -The actual type argument is used to determine the client endpoint. +A connector can be used to configure and open a new client connection backed by a client endpoint that is used to consume and send messages. +Quarkus provides a CDI bean with bean type `io.quarkus.websockets.next.WebSocketConnector` and default qualifer that can be injected in other beans. +The actual type argument of an injection point is used to determine the client endpoint. The type is validated during build - if it does not represent a client endpoint the build fails. Let’s consider the following client endpoint: @@ -955,6 +957,31 @@ public class MyBean { NOTE: If an application attempts to inject a connector for a missing endpoint, an error is thrown. +Connectors are not thread-safe and should not be used concurrently. +Connectors should also not be reused. +If you need to create multiple connections in a row you'll need to obtain a new connetor instance programmatically using `Instance#get()`: + +[source, java] +---- +import jakarta.enterprise.inject.Instance; + +@Singleton +public class MyBean { + + @Inject + Instance> connector; + + void connect() { + var connection1 = connector.get().baseUri(uri) + .addHeader("Foo", "alpha") + .connectAndAwait(); + var connection2 = connector.get().baseUri(uri) + .addHeader("Foo", "bravo") + .connectAndAwait(); + } +} +---- + ==== Basic connector In the case where the application developer does not need the combination of the client endpoint and the connector, a _basic connector_ can be used. @@ -991,6 +1018,31 @@ The basic connector is closer to a low-level API and is reserved for advanced us However, unlike others low-level WebSocket clients, it is still a CDI bean and can be injected in other beans. It also provides a way to configure the execution model of the callbacks, ensuring optimal integration with the rest of Quarkus. +Connectors are not thread-safe and should not be used concurrently. +Connectors should also not be reused. +If you need to create multiple connections in a row you'll need to obtain a new connetor instance programmatically using `Instance#get()`: + +[source, java] +---- +import jakarta.enterprise.inject.Instance; + +@Singleton +public class MyBean { + + @Inject + Instance connector; + + void connect() { + var connection1 = connector.get().baseUri(uri) + .addHeader("Foo", "alpha") + .connectAndAwait(); + var connection2 = connector.get().baseUri(uri) + .addHeader("Foo", "bravo") + .connectAndAwait(); + } +} +---- + [[ws-client-connection]] === WebSocket client connection diff --git a/docs/src/main/asciidoc/writing-extensions.adoc b/docs/src/main/asciidoc/writing-extensions.adoc index f62c27c51d03a..5e6f84dcf8340 100644 --- a/docs/src/main/asciidoc/writing-extensions.adoc +++ b/docs/src/main/asciidoc/writing-extensions.adoc @@ -331,6 +331,10 @@ Too flexible to benefit from the build time boot promoted by Quarkus. Most extension we have seen do not make use of these extreme flexibility capabilities. The way to port a CDI extension to Quarkus is to rewrite it as a Quarkus extension which will define the various beans at build time (deployment time in extension parlance). +=== Levels of capability + +Quarkus extensions can do lots of things. The xref:extension-maturity-matrix.adoc[extension maturity matrix] lays out a path through the various capabilities, with a suggested implementation order. + == Technical aspect [[bootstrap-three-phases]] diff --git a/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/auth/OAuth2AuthMechanism.java b/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/auth/OAuth2AuthMechanism.java index ef644ce1007b8..2742c4e1b1307 100644 --- a/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/auth/OAuth2AuthMechanism.java +++ b/extensions/elytron-security-oauth2/runtime/src/main/java/io/quarkus/elytron/security/oauth2/runtime/auth/OAuth2AuthMechanism.java @@ -5,6 +5,8 @@ import jakarta.enterprise.context.ApplicationScoped; +import org.jboss.logging.Logger; + import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponseStatus; import io.quarkus.security.credential.TokenCredential; @@ -23,7 +25,7 @@ */ @ApplicationScoped public class OAuth2AuthMechanism implements HttpAuthenticationMechanism { - + private static final Logger LOG = Logger.getLogger(OAuth2AuthMechanism.class); private static final String BEARER_PREFIX = "Bearer "; protected static final ChallengeData CHALLENGE_DATA = new ChallengeData( @@ -46,7 +48,9 @@ public Uni authenticate(RoutingContext context, String authHeader = context.request().headers().get("Authorization"); if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) { - // No suitable bearer token has been found in this request, + // No suitable bearer token has been found in this request + LOG.debug("Bearer access token is not available"); + return Uni.createFrom().nullItem(); } diff --git a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayProcessor.java b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayProcessor.java index e55818785a9af..bf5a400c5610d 100644 --- a/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayProcessor.java +++ b/extensions/flyway/deployment/src/main/java/io/quarkus/flyway/deployment/FlywayProcessor.java @@ -298,8 +298,8 @@ public ServiceStartBuildItem startActions(FlywayRecorder recorder, public InitTaskBuildItem configureInitTask(ApplicationInfoBuildItem app) { return InitTaskBuildItem.create() .withName(app.getName() + "-flyway-init") - .withTaskEnvVars(Map.of("QUARKUS_INIT_AND_EXIT", "true", "QUARKUS_FLYWAY_ENABLED", "true")) - .withAppEnvVars(Map.of("QUARKUS_FLYWAY_ENABLED", "false")) + .withTaskEnvVars(Map.of("QUARKUS_INIT_AND_EXIT", "true", "QUARKUS_FLYWAY_ACTIVE", "true")) + .withAppEnvVars(Map.of("QUARKUS_FLYWAY_ACTIVE", "false")) .withSharedEnvironment(true) .withSharedFilesystem(true); } diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java index accf86b916e7e..dd9ed6a3bfe60 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java @@ -3,12 +3,24 @@ public class HalLink { private final String href; + private final String title; + private final String type; - public HalLink(String href) { + public HalLink(String href, String title, String type) { this.href = href; + this.title = title; + this.type = type; } public String getHref() { return href; } + + public String getTitle() { + return title; + } + + public String getType() { + return type; + } } diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java index 01873dba72aa5..7bcf0ab6cc227 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java @@ -12,6 +12,14 @@ public class HalLinkJacksonSerializer extends JsonSerializer { public void serialize(HalLink value, JsonGenerator generator, SerializerProvider serializers) throws IOException { generator.writeStartObject(); generator.writeObjectField("href", value.getHref()); + if (value.getTitle() != null) { + generator.writeObjectField("title", value.getTitle()); + } + + if (value.getType() != null) { + generator.writeObjectField("type", value.getType()); + } + generator.writeEndObject(); } } diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java index 60fd49d43e2a5..44fb1354b24d0 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java @@ -10,6 +10,14 @@ public class HalLinkJsonbSerializer implements JsonbSerializer { public void serialize(HalLink value, JsonGenerator generator, SerializationContext context) { generator.writeStartObject(); generator.write("href", value.getHref()); + if (value.getTitle() != null) { + generator.write("title", value.getTitle()); + } + + if (value.getType() != null) { + generator.write("type", value.getType()); + } + generator.writeEnd(); } } diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java index 16338be4d3aea..1e3fbc97deb20 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java @@ -24,7 +24,9 @@ public Map getLinks() { @SuppressWarnings("unused") public void addLinks(Link... links) { for (Link link : links) { - this.links.put(link.getRel(), new HalLink(link.getUri().toString())); + this.links.put(link.getRel(), new HalLink(link.getUri().toString(), + link.getTitle(), + link.getType())); } } } diff --git a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java index 92199763171cf..31d74e0528c51 100644 --- a/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java +++ b/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/devservices/InfinispanDevServiceProcessor.java @@ -259,7 +259,7 @@ private static class QuarkusInfinispanContainer extends InfinispanContainer { public QuarkusInfinispanContainer(String clientName, InfinispanDevServicesConfig config, LaunchMode launchMode, boolean useSharedNetwork) { - super(config.imageName.orElse(IMAGE_BASENAME + ":" + Version.getVersion())); + super(config.imageName.orElse(IMAGE_BASENAME + ":" + Version.getUnbrandedVersion())); this.fixedExposedPort = config.port; this.useSharedNetwork = useSharedNetwork; if (launchMode == DEVELOPMENT) { diff --git a/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinProcessor.java b/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinProcessor.java index 340f4de9888ac..aff88dfbd80b2 100644 --- a/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinProcessor.java +++ b/extensions/kotlin/deployment/src/main/java/io/quarkus/kotlin/deployment/KotlinProcessor.java @@ -61,6 +61,8 @@ void registerKotlinReflection(final BuildProducer refl .build()); reflectiveClass.produce(ReflectiveClassBuildItem.builder("kotlin.KotlinVersion$Companion[]").constructors(false) .build()); + reflectiveClass.produce( + ReflectiveClassBuildItem.builder("kotlin.collections.EmptyList", "kotlin.collections.EmptyMap").build()); nativeResourcePatterns.produce(builder().includePatterns( "META-INF/.*.kotlin_module$", diff --git a/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java b/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java index ce7cd265223b3..030d99653992e 100644 --- a/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java +++ b/extensions/netty/runtime/src/main/java/io/quarkus/netty/runtime/graal/NettySubstitutions.java @@ -173,7 +173,8 @@ final class Target_io_netty_handler_ssl_JdkSslServerContext { KeyManagerFactory keyManagerFactory, Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, - SecureRandom secureRandom, String keyStore) throws SSLException { + SecureRandom secureRandom, String keyStore, Target_io_netty_handler_ssl_ResumptionController resumptionController) + throws SSLException { } } @@ -181,12 +182,13 @@ final class Target_io_netty_handler_ssl_JdkSslServerContext { final class Target_io_netty_handler_ssl_JdkSslClientContext { @Alias - Target_io_netty_handler_ssl_JdkSslClientContext(Provider sslContextProvider, X509Certificate[] trustCertCollection, - TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, - String keyPassword, KeyManagerFactory keyManagerFactory, Iterable ciphers, - CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, String[] protocols, - long sessionCacheSize, long sessionTimeout, SecureRandom secureRandom, - String keyStoreType) throws SSLException { + Target_io_netty_handler_ssl_JdkSslClientContext(Provider sslContextProvider, + X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, + X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, + KeyManagerFactory keyManagerFactory, Iterable ciphers, CipherSuiteFilter cipherFilter, + ApplicationProtocolConfig apn, String[] protocols, long sessionCacheSize, long sessionTimeout, + SecureRandom secureRandom, String keyStoreType, String endpointIdentificationAlgorithm, + Target_io_netty_handler_ssl_ResumptionController resumptionController) throws SSLException { } } @@ -222,43 +224,55 @@ final class Target_io_netty_handler_ssl_JdkAlpnSslEngine { } } +@TargetClass(className = "io.netty.handler.ssl.ResumptionController") +final class Target_io_netty_handler_ssl_ResumptionController { + + @Alias + Target_io_netty_handler_ssl_ResumptionController() { + + } +} + @TargetClass(className = "io.netty.handler.ssl.SslContext") final class Target_io_netty_handler_ssl_SslContext { @Substitute - static SslContext newServerContextInternal(SslProvider provider, Provider sslContextProvider, + static SslContext newServerContextInternal(SslProvider provider, + Provider sslContextProvider, X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, - X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, - KeyManagerFactory keyManagerFactory, Iterable ciphers, - CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, - long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, - String[] protocols, boolean startTls, boolean enableOcsp, - SecureRandom secureRandom, String keyStoreType, + X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, + long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, + boolean enableOcsp, SecureRandom secureRandom, String keyStoreType, Map.Entry, Object>... ctxOptions) throws SSLException { if (enableOcsp) { throw new IllegalArgumentException("OCSP is not supported with this SslProvider: " + provider); } + Target_io_netty_handler_ssl_ResumptionController resumptionController = new Target_io_netty_handler_ssl_ResumptionController(); return (SslContext) (Object) new Target_io_netty_handler_ssl_JdkSslServerContext(sslContextProvider, trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, - clientAuth, protocols, startTls, secureRandom, keyStoreType); + clientAuth, protocols, startTls, secureRandom, keyStoreType, resumptionController); } @Substitute - static SslContext newClientContextInternal(SslProvider provider, Provider sslContextProvider, - X509Certificate[] trustCert, - TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, - KeyManagerFactory keyManagerFactory, Iterable ciphers, CipherSuiteFilter cipherFilter, - ApplicationProtocolConfig apn, String[] protocols, long sessionCacheSize, long sessionTimeout, - boolean enableOcsp, SecureRandom secureRandom, - String keyStoreType, Map.Entry, Object>... options) throws SSLException { + static SslContext newClientContextInternal(SslProvider provider, + Provider sslContextProvider, + X509Certificate[] trustCert, TrustManagerFactory trustManagerFactory, + X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, String[] protocols, + long sessionCacheSize, long sessionTimeout, boolean enableOcsp, + SecureRandom secureRandom, String keyStoreType, String endpointIdentificationAlgorithm, + Map.Entry, Object>... options) throws SSLException { if (enableOcsp) { throw new IllegalArgumentException("OCSP is not supported with this SslProvider: " + provider); } + Target_io_netty_handler_ssl_ResumptionController resumptionController = new Target_io_netty_handler_ssl_ResumptionController(); return (SslContext) (Object) new Target_io_netty_handler_ssl_JdkSslClientContext(sslContextProvider, trustCert, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize, - sessionTimeout, secureRandom, keyStoreType); + sessionTimeout, secureRandom, keyStoreType, endpointIdentificationAlgorithm, + resumptionController); } } diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/metric/MetricProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/metric/MetricProcessor.java index 1224d28e0c2c8..fcc57f9187165 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/metric/MetricProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/metric/MetricProcessor.java @@ -22,7 +22,9 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.NativeMonitoringBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem; +import io.quarkus.deployment.pkg.NativeConfig; import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig; import io.quarkus.opentelemetry.runtime.metrics.cdi.MetricsProducer; import io.quarkus.opentelemetry.runtime.metrics.instrumentation.JvmMetricsService; @@ -33,6 +35,11 @@ public class MetricProcessor { private static final DotName METRIC_READER = DotName.createSimple(MetricReader.class.getName()); private static final DotName METRIC_PROCESSOR = DotName.createSimple(MetricProcessor.class.getName()); + @BuildStep + void addNativeMonitoring(BuildProducer nativeMonitoring) { + nativeMonitoring.produce(new NativeMonitoringBuildItem(NativeConfig.MonitoringOption.JFR)); + } + @BuildStep UnremovableBeanBuildItem ensureProducersAreRetained( CombinedIndexBuildItem indexBuildItem, diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/exporter/InMemoryMetricExporter.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/exporter/InMemoryMetricExporter.java index 56e548f9167c5..104062b53127c 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/exporter/InMemoryMetricExporter.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/exporter/InMemoryMetricExporter.java @@ -106,11 +106,6 @@ private static boolean isPathFound(String path, Attributes attributes) { return value.toString().equals(path); } - public void assertCount(final int count) { - Awaitility.await().atMost(5, SECONDS) - .untilAsserted(() -> Assertions.assertEquals(count, getFinishedMetricItems().size())); - } - public void assertCount(final String name, final String target, final int count) { Awaitility.await().atMost(5, SECONDS) .untilAsserted(() -> Assertions.assertEquals(count, getFinishedMetricItems(name, target).size())); @@ -131,7 +126,12 @@ public void assertCountPointsAtLeast(final String name, final String target, fin .untilAsserted(() -> { List metricData = getFinishedMetricItems(name, target); Assertions.assertTrue(1 <= metricData.size()); - Assertions.assertTrue(countPoints <= metricData.get(0).getData().getPoints().size()); + Assertions.assertTrue(countPoints <= metricData.stream() + .reduce((first, second) -> second) // get the last received + .orElse(null) + .getData() + .getPoints() + .size()); }); } diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/HttpServerMetricsTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/HttpServerMetricsTest.java index 31da620a25891..dd2839e33024f 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/HttpServerMetricsTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/HttpServerMetricsTest.java @@ -70,7 +70,10 @@ void collectsHttpRouteFromEndAttributes() { .statusCode(INTERNAL_SERVER_ERROR.getStatusCode()); metricExporter.assertCountPointsAtLeast("http.server.request.duration", null, 2); - MetricData metric = metricExporter.getFinishedMetricItems("http.server.request.duration", null).get(0); + MetricData metric = metricExporter + .getFinishedMetricItems("http.server.request.duration", null).stream() + .reduce((first, second) -> second) // get the last received + .orElse(null); assertThat(metric) .hasName("http.server.request.duration") diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java index de673ab9c77db..9c4606ea68e99 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java @@ -10,6 +10,7 @@ import java.io.PrintWriter; import java.io.StringWriter; +import java.time.Instant; import java.util.Map; import java.util.Optional; import java.util.logging.Handler; @@ -43,6 +44,7 @@ public void publish(LogRecord record) { final LogRecordBuilder logRecordBuilder = openTelemetry.getLogsBridge() .loggerBuilder(INSTRUMENTATION_NAME) .build().logRecordBuilder() + .setTimestamp(Instant.now()) .setObservedTimestamp(record.getInstant()); if (record.getLevel() != null) { diff --git a/extensions/quartz/deployment/pom.xml b/extensions/quartz/deployment/pom.xml index b67c16e1a8706..c206dc6570a62 100644 --- a/extensions/quartz/deployment/pom.xml +++ b/extensions/quartz/deployment/pom.xml @@ -34,11 +34,6 @@ io.quarkus quarkus-quartz - - org.eclipse.transformer - org.eclipse.transformer - 0.5.0 - io.quarkus diff --git a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/JakartaEnablement.java b/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/JakartaEnablement.java deleted file mode 100644 index 7bb45dfb9afbc..0000000000000 --- a/extensions/quartz/deployment/src/main/java/io/quarkus/quartz/deployment/JakartaEnablement.java +++ /dev/null @@ -1,89 +0,0 @@ -package io.quarkus.quartz.deployment; - -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import org.eclipse.transformer.action.ActionContext; -import org.eclipse.transformer.action.ByteData; -import org.eclipse.transformer.action.impl.ActionContextImpl; -import org.eclipse.transformer.action.impl.ByteDataImpl; -import org.eclipse.transformer.action.impl.ClassActionImpl; -import org.eclipse.transformer.action.impl.SelectionRuleImpl; -import org.eclipse.transformer.action.impl.SignatureRuleImpl; -import org.eclipse.transformer.util.FileUtils; -import org.objectweb.asm.ClassReader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.quarkus.bootstrap.classloading.QuarkusClassLoader; -import io.quarkus.deployment.annotations.BuildProducer; -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; - -/** - * Quartz is compiled using references to classes in the javax packages; - * we need to transform these to fix compatibility with jakarta packages. - * We do this by leveraging the Eclipse Transformer project during Augmentation, so - * that end users don't need to bother. - */ -public class JakartaEnablement { - - private static final List CLASSES_NEEDING_TRANSFORMATION = List.of( - "org.quartz.ExecuteInJTATransaction", - "org.quartz.ee.servlet.QuartzInitializerServlet", - "org.quartz.ee.servlet.QuartzInitializerListener", - "org.quartz.ee.jta.JTAJobRunShell", - "org.quartz.ee.jta.UserTransactionHelper", - "org.quartz.ee.jta.UserTransactionHelper$UserTransactionWithContext", - "org.quartz.xml.XMLSchedulingDataProcessor", - "org.quartz.impl.jdbcjobstore.JTANonClusteredSemaphore"); - - @BuildStep - void transformToJakarta(BuildProducer transformers) { - if (QuarkusClassLoader.isClassPresentAtRuntime("jakarta.transaction.Transaction")) { - JakartaTransformer tr = new JakartaTransformer(); - for (String classname : CLASSES_NEEDING_TRANSFORMATION) { - final BytecodeTransformerBuildItem item = new BytecodeTransformerBuildItem.Builder() - .setCacheable(true) - .setContinueOnFailure(false) - .setClassToTransform(classname) - .setClassReaderOptions(ClassReader.SKIP_DEBUG) - .setInputTransformer(tr::transform) - .build(); - transformers.produce(item); - } - } - } - - private static class JakartaTransformer { - - private final Logger logger; - private final ActionContext ctx; - // We need to prevent the Eclipse Transformer to adjust the "javax" packages. - // Thus why we split the strings. - private static final Map renames = Map.of("javax" + ".transaction", "jakarta.transaction", - "javax" + ".servlet", "jakarta.servlet", - "javax" + ".xml.bind", "jakarta.xml.bind"); - - JakartaTransformer() { - logger = LoggerFactory.getLogger("JakartaTransformer"); - //N.B. we enable only this single transformation of package renames, not the full set of capabilities of Eclipse Transformer; - //this might need tailoring if the same idea gets applied to a different context. - ctx = new ActionContextImpl(logger, - new SelectionRuleImpl(logger, Collections.emptyMap(), Collections.emptyMap()), - new SignatureRuleImpl(logger, renames, null, null, null, null, null, Collections.emptyMap())); - } - - byte[] transform(final String name, final byte[] bytes) { - logger.debug("Jakarta EE compatibility enhancer for Quarkus: transforming " + name); - final ClassActionImpl classTransformer = new ClassActionImpl(ctx); - final ByteBuffer input = ByteBuffer.wrap(bytes); - final ByteData inputData = new ByteDataImpl(name, input, FileUtils.DEFAULT_CHARSET); - final ByteData outputData = classTransformer.apply(inputData); - return outputData.buffer().array(); - } - } - -} diff --git a/extensions/quartz/runtime/pom.xml b/extensions/quartz/runtime/pom.xml index afb86369ef3b5..76b0fd2e9aaaf 100644 --- a/extensions/quartz/runtime/pom.xml +++ b/extensions/quartz/runtime/pom.xml @@ -30,20 +30,6 @@ org.quartz-scheduler quartz - - - com.zaxxer - HikariCP - - - com.zaxxer - HikariCP-java7 - - - com.mchange - c3p0 - - jakarta.transaction diff --git a/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java b/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java index 6da3709d42b2f..73e5ae4beabc0 100644 --- a/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java +++ b/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java @@ -29,7 +29,7 @@ protected Map getInstanceLinks(Object entity) { private Map linksToMap(RESTServiceDiscovery serviceDiscovery) { Map links = new HashMap<>(serviceDiscovery.size()); for (RESTServiceDiscovery.AtomLink atomLink : serviceDiscovery) { - links.put(atomLink.getRel(), new HalLink(atomLink.getHref())); + links.put(atomLink.getRel(), new HalLink(atomLink.getHref(), atomLink.getTitle(), atomLink.getType())); } return links; } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonCodeGenerator.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonCodeGenerator.java index d407c7e6ec443..b06abe3deaf15 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonCodeGenerator.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonCodeGenerator.java @@ -161,7 +161,7 @@ protected enum FieldKind { MAP(true), TYPE_VARIABLE(true); - private boolean generic; + private final boolean generic; FieldKind(boolean generic) { this.generic = generic; @@ -193,7 +193,7 @@ protected FieldKind registerTypeToBeGenerated(Type fieldType, String typeName) { } } if (pType.arguments().size() == 2 && typeName.equals("java.util.Map")) { - registerTypeToBeGenerated(pType.arguments().get(1)); + registerTypeToBeGenerated(pType.arguments().get(0)); registerTypeToBeGenerated(pType.arguments().get(1)); return FieldKind.MAP; } @@ -281,7 +281,7 @@ private Type fieldType() { if (isPublicField()) { return fieldInfo.type(); } - if (methodInfo.name().startsWith("set")) { + if (methodInfo.parametersCount() == 1 && methodInfo.name().startsWith("set")) { return methodInfo.parameterType(0); } return methodInfo.returnType(); @@ -304,6 +304,9 @@ private String fieldName() { private String fieldNameFromMethod(MethodInfo methodInfo) { String methodName = methodInfo.name(); + if (methodName.equals("get") || methodName.equals("set") || methodName.equals("is")) { + return methodName; + } if (methodName.startsWith("is")) { return methodName.substring(2, 3).toLowerCase() + methodName.substring(3); } diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/FieldNameSetGetPrefixResourceTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/FieldNameSetGetPrefixResourceTest.java new file mode 100644 index 0000000000000..6a483942bc139 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/FieldNameSetGetPrefixResourceTest.java @@ -0,0 +1,52 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.smallrye.common.annotation.NonBlocking; + +// Ensures uncommon field names like "set", "get", and "is" are generated correctly. +class FieldNameSetGetPrefixResourceTest { + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(Resource.class, Resource.UncommonBody.class).addAsResource( + new StringAsset( + "quarkus.rest.jackson.optimization.enable-reflection-free-serializers=true\n"), + "application.properties")); + + @Test + void testFieldNameSetGetIsPrefix() { + RestAssured.get("/field-name-prefixes") + .then() + .statusCode(200) + .contentType("application/json") + .body("id", Matchers.equalTo("id")) + .body("set", Matchers.is(true)) + .body("get", Matchers.is(true)) + .body("is", Matchers.is(false)) + .body("setText", Matchers.equalTo("setText")); + } + + @NonBlocking + @Path("/field-name-prefixes") + private static class Resource { + @GET + public UncommonBody get() { + return new UncommonBody("id", true, true, false, "setText"); + } + + private record UncommonBody(String id, boolean set, boolean get, boolean is, String setText) { + } + } + +} diff --git a/extensions/resteasy-reactive/rest-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java b/extensions/resteasy-reactive/rest-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java index 2a991448e49df..7c19b66d00eae 100644 --- a/extensions/resteasy-reactive/rest-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java +++ b/extensions/resteasy-reactive/rest-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java @@ -62,6 +62,8 @@ private LinkInfo getLinkInfo(ResourceMethod resourceMethod, MethodInfo resourceM AnnotationInstance restLinkAnnotation, String resourceClassPath, IndexView index) { Type returnType = getNonAsyncReturnType(resourceMethodInfo.returnType()); String rel = getAnnotationValue(restLinkAnnotation, "rel", deductRel(resourceMethod, returnType, index)); + String title = getAnnotationValue(restLinkAnnotation, "title", null); + String type = getAnnotationValue(restLinkAnnotation, "type", null); String entityType = getAnnotationValue(restLinkAnnotation, "entityType", deductEntityType(returnType)); String path = UriBuilder.fromPath(resourceClassPath).path(resourceMethod.getPath()).toTemplate(); while (path.endsWith("/")) { @@ -69,7 +71,7 @@ private LinkInfo getLinkInfo(ResourceMethod resourceMethod, MethodInfo resourceM } Set pathParameters = getPathParameters(path); - return new LinkInfo(rel, entityType, path, pathParameters); + return new LinkInfo(rel, title, type, entityType, path, pathParameters); } /** diff --git a/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java b/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java index 99f71ebaac2b0..4bb1d51d03d86 100644 --- a/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java +++ b/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java @@ -3,6 +3,9 @@ import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; + import org.jboss.resteasy.reactive.common.util.RestMediaType; import org.junit.jupiter.api.Test; @@ -89,5 +92,19 @@ void shouldGetHalLinksForRestLinkId() { .thenReturn(); assertThat(response.body().jsonPath().getString("_links.self.href")).endsWith("/records/with-rest-link-id/100"); + assertThat(response.body().jsonPath().getString("_links.self.title")).isEqualTo("The with rest link title"); + assertThat(response.body().jsonPath().getString("_links.self.type")).isEqualTo(MediaType.APPLICATION_JSON); + } + + @Test + void shouldIncludeAllFieldsFromLink() { + Response response = given() + .header(HttpHeaders.ACCEPT, RestMediaType.APPLICATION_HAL_JSON) + .get("/records/with-rest-link-with-all-fields") + .thenReturn(); + + assertThat(response.body().jsonPath().getString("_links.all.href")).endsWith("/records/with-rest-link-id/100"); + assertThat(response.body().jsonPath().getString("_links.all.title")).isEqualTo("The title link"); + assertThat(response.body().jsonPath().getString("_links.all.type")).isEqualTo(MediaType.APPLICATION_JSON); } } diff --git a/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java b/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java index 50255cd5f39cb..42750f5090a57 100644 --- a/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java +++ b/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java @@ -1,5 +1,6 @@ package io.quarkus.resteasy.reactive.links.deployment; +import java.net.URI; import java.time.Duration; import java.util.Arrays; import java.util.LinkedList; @@ -11,10 +12,12 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Link; import jakarta.ws.rs.core.MediaType; import org.jboss.resteasy.reactive.common.util.RestMediaType; +import io.quarkus.hal.HalEntityWrapper; import io.quarkus.resteasy.reactive.links.InjectRestLinks; import io.quarkus.resteasy.reactive.links.RestLink; import io.quarkus.resteasy.reactive.links.RestLinkType; @@ -172,7 +175,7 @@ public TestRecordWithPersistenceId getWithPersistenceId(@PathParam("id") int id) @GET @Path("/with-rest-link-id/{id}") @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) - @RestLink(entityType = TestRecordWithRestLinkId.class) + @RestLink(entityType = TestRecordWithRestLinkId.class, title = "The with rest link title", type = MediaType.APPLICATION_JSON) @InjectRestLinks public TestRecordWithRestLinkId getWithRestLinkId(@PathParam("id") int id) { return REST_LINK_ID_RECORDS.stream() @@ -181,4 +184,18 @@ public TestRecordWithRestLinkId getWithRestLinkId(@PathParam("id") int id) { .orElseThrow(NotFoundException::new); } + @GET + @Path("/with-rest-link-with-all-fields") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + public HalEntityWrapper getAllFieldsFromLink() { + + var entity = new TestRecordWithIdAndPersistenceIdAndRestLinkId(1, 10, 100, "one"); + return new HalEntityWrapper<>(entity, + Link.fromUri(URI.create("/records/with-rest-link-id/100")) + .rel("all") + .title("The title link") + .type(MediaType.APPLICATION_JSON) + .build()); + } + } diff --git a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java index cfa5316efb180..d083abcf59e61 100644 --- a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java +++ b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java @@ -25,6 +25,20 @@ */ String rel() default ""; + /** + * Intended for labelling the link with a human-readable identifier. + * + * @return the link title. + */ + String title() default ""; + + /** + * Hint to indicate the media type expected when dereferencing the target resource. + * + * @return the link expected media type. + */ + String type() default ""; + /** * Declares a link for the given type of resources. * If not set, it will default to the returning type of the annotated method. diff --git a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinkInfo.java b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinkInfo.java index 45851358c6e3c..fda5c0f2e7dea 100644 --- a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinkInfo.java +++ b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinkInfo.java @@ -6,14 +6,20 @@ public final class LinkInfo { private final String rel; + private final String title; + + private final String type; + private final String entityType; private final String path; private final Set pathParameters; - public LinkInfo(String rel, String entityType, String path, Set pathParameters) { + public LinkInfo(String rel, String title, String type, String entityType, String path, Set pathParameters) { this.rel = rel; + this.title = title; + this.type = type; this.entityType = entityType; this.path = path; this.pathParameters = pathParameters; @@ -23,6 +29,14 @@ public String getRel() { return rel; } + public String getTitle() { + return title; + } + + public String getType() { + return type; + } + public String getEntityType() { return entityType; } diff --git a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java index f29ccc56c1710..787b4bb90254f 100644 --- a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java +++ b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java @@ -37,9 +37,7 @@ public Collection getTypeLinks(Class elementType) { List links = new ArrayList<>(linkInfoList.size()); for (LinkInfo linkInfo : linkInfoList) { if (linkInfo.getPathParameters().size() == 0) { - links.add(Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(linkInfo.getPath())) - .rel(linkInfo.getRel()) - .build()); + links.add(linkBuilderFor(linkInfo).build()); } } return links; @@ -52,13 +50,25 @@ public Collection getInstanceLinks(T instance) { List linkInfoList = linksContainer.getForClass(instance.getClass()); List links = new ArrayList<>(linkInfoList.size()); for (LinkInfo linkInfo : linkInfoList) { - links.add(Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(linkInfo.getPath())) - .rel(linkInfo.getRel()) - .build(getPathParameterValues(linkInfo, instance))); + links.add(linkBuilderFor(linkInfo).build(getPathParameterValues(linkInfo, instance))); } return links; } + private Link.Builder linkBuilderFor(LinkInfo linkInfo) { + Link.Builder builder = Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(linkInfo.getPath())) + .rel(linkInfo.getRel()); + if (linkInfo.getTitle() != null) { + builder.title(linkInfo.getTitle()); + } + + if (linkInfo.getType() != null) { + builder.type(linkInfo.getType()); + } + + return builder; + } + private Object[] getPathParameterValues(LinkInfo linkInfo, Object instance) { List values = new ArrayList<>(linkInfo.getPathParameters().size()); for (String name : linkInfo.getPathParameters()) { diff --git a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java index c7a58fa36e1d0..a3d66cc7f172f 100644 --- a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java +++ b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java @@ -34,7 +34,7 @@ protected Map getInstanceLinks(Object entity) { private Map linksToMap(Collection refLinks) { Map links = new HashMap<>(); for (Link link : refLinks) { - links.put(link.getRel(), new HalLink(link.getUri().toString())); + links.put(link.getRel(), new HalLink(link.getUri().toString(), link.getTitle(), link.getType())); } return links; diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java index 82fc4151c7ede..bd5b9bcf607e4 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java @@ -107,6 +107,7 @@ import io.quarkus.security.deployment.PermissionSecurityChecks.PermissionSecurityChecksBuilder; import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.security.runtime.IdentityProviderManagerCreator; +import io.quarkus.security.runtime.QuarkusPermissionSecurityIdentityAugmentor; import io.quarkus.security.runtime.QuarkusSecurityRolesAllowedConfigBuilder; import io.quarkus.security.runtime.SecurityBuildTimeConfig; import io.quarkus.security.runtime.SecurityCheckRecorder; @@ -691,7 +692,8 @@ void configurePermissionCheckers(PermissionSecurityChecksBuilderBuildItem checke // - this processor relies on the bean archive index (cycle: idx -> additional bean -> idx) // - we have injection points (=> better validation from Arc) as checker beans are only requested from this augmentor var syntheticBeanConfigurator = SyntheticBeanBuildItem - .configure(SecurityIdentityAugmentor.class) + .configure(QuarkusPermissionSecurityIdentityAugmentor.class) + .addType(SecurityIdentityAugmentor.class) // ATM we do get augmentors from CDI once, no need to keep the instance in the CDI container .scope(Dependent.class) .unremovable() diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusPermissionSecurityIdentityAugmentor.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusPermissionSecurityIdentityAugmentor.java index 300cd5e830204..7ca713dc4c4bd 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusPermissionSecurityIdentityAugmentor.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusPermissionSecurityIdentityAugmentor.java @@ -15,7 +15,7 @@ * Adds a permission checker that grants access to the {@link QuarkusPermission} * when {@link QuarkusPermission#isGranted(SecurityIdentity)} is true. */ -final class QuarkusPermissionSecurityIdentityAugmentor implements SecurityIdentityAugmentor { +public final class QuarkusPermissionSecurityIdentityAugmentor implements SecurityIdentityAugmentor { /** * Permission checker only authorizes authenticated users and checkers shouldn't throw a security exception. diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java index 6667ba40b89ca..ad1e9ba9ca8a9 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/SecurityCheckRecorder.java @@ -29,7 +29,6 @@ import io.quarkus.runtime.annotations.Recorder; import io.quarkus.security.ForbiddenException; import io.quarkus.security.StringPermission; -import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.security.runtime.interceptor.SecurityCheckStorageBuilder; import io.quarkus.security.runtime.interceptor.SecurityConstrainer; import io.quarkus.security.runtime.interceptor.check.AuthenticatedCheck; @@ -436,10 +435,11 @@ private static Object convertMethodParamToPermParam(int i, Object methodArg, } } - public Function, SecurityIdentityAugmentor> createPermissionAugmentor() { - return new Function, SecurityIdentityAugmentor>() { + public Function, QuarkusPermissionSecurityIdentityAugmentor> createPermissionAugmentor() { + return new Function, QuarkusPermissionSecurityIdentityAugmentor>() { @Override - public SecurityIdentityAugmentor apply(SyntheticCreationalContext ctx) { + public QuarkusPermissionSecurityIdentityAugmentor apply( + SyntheticCreationalContext ctx) { return new QuarkusPermissionSecurityIdentityAugmentor(ctx.getInjectedReference(BlockingSecurityExecutor.class)); } }; diff --git a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java index 0ac20289bbaa7..79b6dcabfda21 100644 --- a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java +++ b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java @@ -9,6 +9,8 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import org.jboss.logging.Logger; + import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.cookie.ServerCookieDecoder; @@ -34,6 +36,7 @@ */ @ApplicationScoped public class JWTAuthMechanism implements HttpAuthenticationMechanism { + private static final Logger LOG = Logger.getLogger(JWTAuthMechanism.class); private static final String ERROR_MSG = "SmallRye JWT requires a safe (isolated) Vert.x sub-context for propagation " + "of the '" + TokenCredential.class.getName() + "', but the current context hasn't been flagged as such."; protected static final String COOKIE_HEADER = "Cookie"; @@ -86,6 +89,8 @@ public void run() { return identityProviderManager .authenticate(HttpSecurityUtils.setRoutingContextAttribute( new TokenAuthenticationRequest(new JsonWebTokenCredential(jwtToken)), context)); + } else { + LOG.debug("Bearer access token is not available"); } return Uni.createFrom().optional(Optional.empty()); } diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DotNames.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DotNames.java index 5dd492dbbcb1f..8d3b71041bdca 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DotNames.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DotNames.java @@ -4,6 +4,9 @@ final class DotNames { // @formatter:off + static final DotName INSTANCE = DotName.createSimple(jakarta.enterprise.inject.Instance.class.getName()); + static final DotName INJECTABLE_INSTANCE = DotName.createSimple(io.quarkus.arc.InjectableInstance.class.getName()); + static final DotName PROVIDER = DotName.createSimple(jakarta.inject.Provider.class.getName()); static final DotName INCOMING = DotName.createSimple(org.eclipse.microprofile.reactive.messaging.Incoming.class.getName()); static final DotName INCOMINGS = DotName.createSimple(io.smallrye.reactive.messaging.annotations.Incomings.class.getName()); static final DotName OUTGOING = DotName.createSimple(org.eclipse.microprofile.reactive.messaging.Outgoing.class.getName()); diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java index 96234fc7cc580..03c4133fe4edb 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/SmallRyeReactiveMessagingKafkaProcessor.java @@ -20,7 +20,6 @@ import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; -import org.jboss.jandex.MethodParameterInfo; import org.jboss.jandex.Type; import org.jboss.logging.Logger; @@ -354,15 +353,19 @@ private void processIncomingType(DefaultSerdeDiscoveryState discovery, } private Type getInjectionPointType(AnnotationInstance annotation) { - switch (annotation.target().kind()) { - case FIELD: - return annotation.target().asField().type(); - case METHOD_PARAMETER: - MethodParameterInfo parameter = annotation.target().asMethodParameter(); - return parameter.method().parameterType(parameter.position()); - default: - return null; - } + return switch (annotation.target().kind()) { + case FIELD -> handleInstanceChannelInjection(annotation.target().asField().type()); + case METHOD_PARAMETER -> handleInstanceChannelInjection(annotation.target().asMethodParameter().type()); + default -> null; + }; + } + + private Type handleInstanceChannelInjection(Type type) { + return (DotNames.INSTANCE.equals(type.name()) + || DotNames.PROVIDER.equals(type.name()) + || DotNames.INJECTABLE_INSTANCE.equals(type.name())) + ? type.asParameterizedType().arguments().get(0) + : type; } private void handleAdditionalProperties(String channelName, boolean incoming, DefaultSerdeDiscoveryState discovery, diff --git a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java index 05289111c374d..cc971ba643bcb 100644 --- a/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java +++ b/extensions/smallrye-reactive-messaging-kafka/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/kafka/deployment/DefaultSerdeConfigTest.java @@ -13,6 +13,7 @@ import java.util.concurrent.CompletionStage; import java.util.function.Function; +import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import org.apache.avro.generic.GenericRecord; @@ -41,6 +42,7 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; +import io.quarkus.arc.InjectableInstance; import io.quarkus.commons.classloading.ClassLoaderHelper; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; @@ -111,6 +113,7 @@ boolean isKafkaConnector(List list, boolean in assertThat(configs) .extracting(RunTimeConfigurationDefaultBuildItem::getKey, RunTimeConfigurationDefaultBuildItem::getValue) + .hasSize(expectations.length) .allSatisfy(tuple -> { Object[] e = tuple.toArray(); String key = (String) e[0]; @@ -3048,5 +3051,26 @@ private static class ChannelChildSerializer { Multi channel2; } + @Test + void instanceInjectionPoint() { + Tuple[] expectations = { + tuple("mp.messaging.outgoing.channel1.value.serializer", "org.apache.kafka.common.serialization.StringSerializer"), + tuple("mp.messaging.incoming.channel2.value.deserializer", "org.apache.kafka.common.serialization.IntegerDeserializer"), + tuple("mp.messaging.outgoing.channel3.value.serializer", "org.apache.kafka.common.serialization.DoubleSerializer"), + }; + doTest(expectations, InstanceInjectionPoint.class); + } + + private static class InstanceInjectionPoint { + @Channel("channel1") + Instance> emitter1; + + @Channel("channel2") + Instance> channel2; + + @Channel("channel3") + InjectableInstance> channel3; + } + } diff --git a/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/DotNames.java b/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/DotNames.java index dc321cb954ec5..efff33c3c0b6c 100644 --- a/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/DotNames.java +++ b/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/DotNames.java @@ -4,6 +4,9 @@ final class DotNames { // @formatter:off + static final DotName INSTANCE = DotName.createSimple(jakarta.enterprise.inject.Instance.class.getName()); + static final DotName INJECTABLE_INSTANCE = DotName.createSimple(io.quarkus.arc.InjectableInstance.class.getName()); + static final DotName PROVIDER = DotName.createSimple(jakarta.inject.Provider.class.getName()); static final DotName INCOMING = DotName.createSimple(org.eclipse.microprofile.reactive.messaging.Incoming.class.getName()); static final DotName INCOMINGS = DotName.createSimple(io.smallrye.reactive.messaging.annotations.Incomings.class.getName()); static final DotName OUTGOING = DotName.createSimple(org.eclipse.microprofile.reactive.messaging.Outgoing.class.getName()); diff --git a/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/PulsarSchemaDiscoveryProcessor.java b/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/PulsarSchemaDiscoveryProcessor.java index 3f88ab082a283..f5539127a0149 100644 --- a/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/PulsarSchemaDiscoveryProcessor.java +++ b/extensions/smallrye-reactive-messaging-pulsar/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/PulsarSchemaDiscoveryProcessor.java @@ -10,7 +10,6 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.DotName; import org.jboss.jandex.MethodInfo; -import org.jboss.jandex.MethodParameterInfo; import org.jboss.jandex.Type; import org.jboss.logging.Logger; @@ -144,15 +143,19 @@ private static String incomingSchemaKey(String channelName) { } private Type getInjectionPointType(AnnotationInstance annotation) { - switch (annotation.target().kind()) { - case FIELD: - return annotation.target().asField().type(); - case METHOD_PARAMETER: - MethodParameterInfo parameter = annotation.target().asMethodParameter(); - return parameter.method().parameterType(parameter.position()); - default: - return null; - } + return switch (annotation.target().kind()) { + case FIELD -> handleInstanceChannelInjection(annotation.target().asField().type()); + case METHOD_PARAMETER -> handleInstanceChannelInjection(annotation.target().asMethodParameter().type()); + default -> null; + }; + } + + private Type handleInstanceChannelInjection(Type type) { + return (DotNames.INSTANCE.equals(type.name()) + || DotNames.PROVIDER.equals(type.name()) + || DotNames.INJECTABLE_INSTANCE.equals(type.name())) + ? type.asParameterizedType().arguments().get(0) + : type; } private void produceRuntimeConfigurationDefaultBuildItem(DefaultSchemaDiscoveryState discovery, diff --git a/extensions/smallrye-reactive-messaging-pulsar/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/DefaultSchemaConfigTest.java b/extensions/smallrye-reactive-messaging-pulsar/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/DefaultSchemaConfigTest.java index 898ade58c059d..b7e9e7e686948 100644 --- a/extensions/smallrye-reactive-messaging-pulsar/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/DefaultSchemaConfigTest.java +++ b/extensions/smallrye-reactive-messaging-pulsar/deployment/src/test/java/io/quarkus/smallrye/reactivemessaging/pulsar/deployment/DefaultSchemaConfigTest.java @@ -15,8 +15,10 @@ import java.util.concurrent.CompletionStage; import java.util.function.Supplier; +import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.Produces; import jakarta.inject.Inject; +import jakarta.inject.Provider; import org.apache.avro.specific.AvroGenerated; import org.apache.pulsar.client.api.Messages; @@ -40,6 +42,7 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; +import io.quarkus.arc.InjectableInstance; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.commons.classloading.ClassLoaderHelper; import io.quarkus.deployment.annotations.BuildProducer; @@ -2108,5 +2111,26 @@ Multi> method4() { } } + @Test + void instanceInjectionPoint() { + Tuple[] expectations = { + tuple("mp.messaging.outgoing.channel1.schema", "STRING"), + tuple("mp.messaging.incoming.channel2.schema", "INT32"), + tuple("mp.messaging.outgoing.channel3.schema", "DOUBLE"), + }; + doTest(expectations, InstanceInjectionPoint.class); + } + + private static class InstanceInjectionPoint { + @Channel("channel1") + Instance> emitter1; + + @Channel("channel2") + Provider> channel2; + + @Channel("channel3") + InjectableInstance> channel3; + } + } diff --git a/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java b/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java index f32cb7327b77a..0b6eddb255515 100644 --- a/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java +++ b/extensions/websockets-next/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketProcessor.java @@ -1594,10 +1594,10 @@ static boolean hasBlockingSignature(MethodInfo method) { if (KotlinUtils.isKotlinSuspendMethod(method)) { return false; } - switch (method.returnType().kind()) { case VOID: case CLASS: + case ARRAY: return true; case PARAMETERIZED_TYPE: // Uni, Multi -> non-blocking diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/ClientEndpointProgrammaticTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/ClientEndpointProgrammaticTest.java new file mode 100644 index 0000000000000..b885c5c82f0ff --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/ClientEndpointProgrammaticTest.java @@ -0,0 +1,114 @@ +package io.quarkus.websockets.next.test.client.programmatic; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.HandshakeRequest; +import io.quarkus.websockets.next.OnClose; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.WebSocketClient; +import io.quarkus.websockets.next.WebSocketClientConnection; +import io.quarkus.websockets.next.WebSocketConnector; + +public class ClientEndpointProgrammaticTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(ServerEndpoint.class, ClientEndpoint.class); + }); + + @Inject + Instance> connector; + + @TestHTTPResource("/") + URI uri; + + @Test + void testClient() throws InterruptedException { + WebSocketClientConnection connection1 = connector + .get() + .baseUri(uri) + .addHeader("Foo", "Lu") + .connectAndAwait(); + connection1.sendTextAndAwait("Hi!"); + + WebSocketClientConnection connection2 = connector + .get() + .baseUri(uri) + .addHeader("Foo", "Ma") + .connectAndAwait(); + connection2.sendTextAndAwait("Hi!"); + + assertTrue(ClientEndpoint.MESSAGE_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(ClientEndpoint.MESSAGES.contains("Lu:Hello Lu!")); + assertTrue(ClientEndpoint.MESSAGES.contains("Lu:Hi!")); + assertTrue(ClientEndpoint.MESSAGES.contains("Ma:Hello Ma!")); + assertTrue(ClientEndpoint.MESSAGES.contains("Ma:Hi!"), ClientEndpoint.MESSAGES.toString()); + + connection1.closeAndAwait(); + connection2.closeAndAwait(); + assertTrue(ClientEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(ServerEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS)); + } + + @WebSocket(path = "/endpoint") + public static class ServerEndpoint { + + static final CountDownLatch CLOSED_LATCH = new CountDownLatch(2); + + @OnOpen + String open(HandshakeRequest handshakeRequest) { + return "Hello " + handshakeRequest.header("Foo") + "!"; + } + + @OnTextMessage + String echo(String message) { + return message; + } + + @OnClose + void close() { + CLOSED_LATCH.countDown(); + } + + } + + @WebSocketClient(path = "/endpoint") + public static class ClientEndpoint { + + static final CountDownLatch MESSAGE_LATCH = new CountDownLatch(4); + + static final List MESSAGES = new CopyOnWriteArrayList<>(); + + static final CountDownLatch CLOSED_LATCH = new CountDownLatch(2); + + @OnTextMessage + void onMessage(String message, HandshakeRequest handshakeRequest) { + MESSAGES.add(handshakeRequest.header("Foo") + ":" + message); + MESSAGE_LATCH.countDown(); + } + + @OnClose + void close() { + CLOSED_LATCH.countDown(); + } + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/InvalidConnectorProgrammaticInjectionPointTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/InvalidConnectorProgrammaticInjectionPointTest.java new file mode 100644 index 0000000000000..80a4a39226b82 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/InvalidConnectorProgrammaticInjectionPointTest.java @@ -0,0 +1,40 @@ +package io.quarkus.websockets.next.test.client.programmatic; + +import static org.junit.jupiter.api.Assertions.fail; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Unremovable; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.websockets.next.WebSocketClientException; +import io.quarkus.websockets.next.WebSocketConnector; + +public class InvalidConnectorProgrammaticInjectionPointTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Service.class); + }) + .setExpectedException(WebSocketClientException.class, true); + + @Test + void testInvalidInjectionPoint() { + fail(); + } + + @Unremovable + @Singleton + public static class Service { + + @Inject + Instance> invalid; + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ArrayBinaryCodecTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ArrayBinaryCodecTest.java new file mode 100644 index 0000000000000..82dd8aa42d889 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ArrayBinaryCodecTest.java @@ -0,0 +1,80 @@ +package io.quarkus.websockets.next.test.codec; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Type; +import java.net.URI; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.BinaryMessageCodec; +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; + +public class ArrayBinaryCodecTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Item.class, Endpoint.class, WSClient.class, ItemArrayBinaryCodec.class); + }); + + @Inject + Vertx vertx; + + @TestHTTPResource("end") + URI testUri; + + @Test + public void testCodec() { + try (WSClient client = new WSClient(vertx)) { + client.connect(testUri); + client.sendAndAwait(Buffer.buffer("Foo")); + client.waitForMessages(1); + assertEquals("Foo", client.getMessages().get(0).toString()); + } + } + + @Singleton + public static class ItemArrayBinaryCodec implements BinaryMessageCodec { + + @Override + public boolean supports(Type type) { + return (type instanceof GenericArrayType) || (type instanceof Class && ((Class) type).isArray()); + } + + @Override + public Buffer encode(Item[] value) { + return Buffer.buffer(value[0].getName()); + } + + @Override + public Item[] decode(Type type, Buffer value) { + throw new UnsupportedOperationException(); + } + + } + + @WebSocket(path = "/end") + public static class Endpoint { + + @OnBinaryMessage + Item[] process(String name) { + Item item = new Item(); + item.setName(name); + item.setCount(1); + return new Item[] { item }; + } + + } +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ArrayTextCodecTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ArrayTextCodecTest.java new file mode 100644 index 0000000000000..9bd6d0bc1bc1d --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ArrayTextCodecTest.java @@ -0,0 +1,58 @@ +package io.quarkus.websockets.next.test.codec; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +public class ArrayTextCodecTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Item.class, Endpont.class, WSClient.class); + }); + + @Inject + Vertx vertx; + + @TestHTTPResource("end") + URI testUri; + + @Test + public void testCodec() throws Exception { + try (WSClient client = new WSClient(vertx)) { + client.connect(testUri); + client.waitForMessages(1); + assertEquals(new JsonArray().add(new JsonObject().put("name", "Foo").put("count", 1)).toString(), + client.getMessages().get(0).toString()); + } + } + + @WebSocket(path = "/end") + public static class Endpont { + + // The default JsonTextMessageCodec is used + @OnOpen + Item[] open() { + Item item = new Item(); + item.setName("Foo"); + item.setCount(1); + return new Item[] { item }; + } + + } +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ByteArrayBinaryMessageTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ByteArrayBinaryMessageTest.java new file mode 100644 index 0000000000000..988db21af3eb3 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/codec/ByteArrayBinaryMessageTest.java @@ -0,0 +1,55 @@ +package io.quarkus.websockets.next.test.codec; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.OnBinaryMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; + +public class ByteArrayBinaryMessageTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Endpont.class, WSClient.class); + }); + + @Inject + Vertx vertx; + + @TestHTTPResource("end") + URI testUri; + + @Test + public void testCodec() throws Exception { + try (WSClient client = new WSClient(vertx)) { + client.connect(testUri); + client.send(Buffer.buffer("43")); + client.waitForMessages(1); + assertEquals("43", client.getMessages().get(0).toString()); + } + } + + @WebSocket(path = "/end") + public static class Endpont { + + // This is an equivalent to Sender#sendBinary(byte[]) + // byte[] is encoded with Buffer#buffer(byte[]), codec is not needed + @OnBinaryMessage + byte[] echo(Buffer message) { + return message.getBytes(); + } + + } +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java index 98ab5ac1596e2..f261b513dce2a 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java @@ -5,6 +5,9 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.inject.Instance; + import io.quarkus.arc.Arc; import io.smallrye.common.annotation.CheckReturnValue; import io.smallrye.common.annotation.Experimental; @@ -12,10 +15,30 @@ import io.vertx.core.buffer.Buffer; /** - * This basic connector can be used to configure and open new client connections. Unlike with {@link WebSocketConnector} a - * client endpoint class is not needed. + * A basic connector can be used to configure and open a new client connection. Unlike with {@link WebSocketConnector} a + * client endpoint is not used to consume and send messages. + *

+ * Quarkus provides a CDI bean with bean type {@code BasicWebSocketConnector} and qualifier {@link Default}. *

* This construct is not thread-safe and should not be used concurrently. + *

+ * Connectors should not be reused. If you need to create multiple connections in a row you'll need to obtain a new connetor + * instance programmatically using {@link Instance#get()}: + *

+ * import jakarta.enterprise.inject.Instance;
+ *
+ * @Inject
+ * Instance<BasicWebSocketConnector> connector;
+ *
+ * void connect() {
+ *      var connection1 = connector.get().baseUri(uri)
+ *                  .addHeader("Foo", "alpha")
+ *                  .connectAndAwait();
+ *      var connection2 = connector.get().baseUri(uri)
+ *                  .addHeader("Foo", "bravo")
+ *                  .connectAndAwait();
+ * }
+ * 
* * @see WebSocketClientConnection */ diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenClientConnections.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenClientConnections.java index e4270cc8b54ae..0f99844f49ecd 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenClientConnections.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenClientConnections.java @@ -4,12 +4,14 @@ import java.util.Optional; import java.util.stream.Stream; +import jakarta.enterprise.inject.Default; + import io.smallrye.common.annotation.Experimental; /** * Provides convenient access to all open client connections. *

- * Quarkus provides a built-in CDI bean with the {@link jakarta.inject.Singleton} scope that implements this interface. + * Quarkus provides a CDI bean with bean type {@link OpenClientConnections} and qualifier {@link Default}. */ @Experimental("This API is experimental and may change in the future") public interface OpenClientConnections extends Iterable { diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenConnections.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenConnections.java index c8a5c797289c7..6f5f59c2cdbf0 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenConnections.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenConnections.java @@ -4,12 +4,14 @@ import java.util.Optional; import java.util.stream.Stream; +import jakarta.enterprise.inject.Default; + import io.smallrye.common.annotation.Experimental; /** * Provides convenient access to all open connections. *

- * Quarkus provides a built-in CDI bean with the {@link jakarta.inject.Singleton} scope that implements this interface. + * Quarkus provides a CDI bean with bean type {@link OpenConnections} and qualifier {@link Default}. */ @Experimental("This API is experimental and may change in the future") public interface OpenConnections extends Iterable { diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java index e33f95bea1e54..d6c11f3f5bccd 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java @@ -5,7 +5,7 @@ /** * This interface represents a client connection to a WebSocket endpoint. *

- * Quarkus provides a built-in CDI bean that implements this interface and can be injected in a {@link WebSocketClient} + * Quarkus provides a CDI bean that implements this interface and can be injected in a {@link WebSocketClient} * endpoint and used to interact with the connected server. */ @Experimental("This API is experimental and may change in the future") diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java index c5deaa339b216..ea82ba5942b4d 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java @@ -8,7 +8,7 @@ /** * This interface represents a connection from a client to a specific {@link WebSocket} endpoint on the server. *

- * Quarkus provides a built-in CDI bean that implements this interface and can be injected in a {@link WebSocket} + * Quarkus provides a CDI bean that implements this interface and can be injected in a {@link WebSocket} * endpoint and used to interact with the connected client, or all clients connected to the endpoint respectively * (broadcasting). *

diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java index 06f91ddf3e919..9a170e4f1431d 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java @@ -3,14 +3,40 @@ import java.net.URI; import java.net.URLEncoder; +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.inject.Instance; + import io.smallrye.common.annotation.CheckReturnValue; import io.smallrye.common.annotation.Experimental; import io.smallrye.mutiny.Uni; /** - * This connector can be used to configure and open new client connections using a client endpoint class. + * A connector can be used to configure and open a new client connection backed by a client endpoint that is used to + * consume and send messages. + *

+ * Quarkus provides a CDI bean with bean type {@code WebSocketConnector} and qualifier {@link Default}. The actual type + * argument of an injection point is used to determine the client endpoint. The type is validated during build + * and if it does not represent a client endpoint then the build fails. *

* This construct is not thread-safe and should not be used concurrently. + *

+ * Connectors should not be reused. If you need to create multiple connections in a row you'll need to obtain a new connetor + * instance programmatically using {@link Instance#get()}: + *

+ * import jakarta.enterprise.inject.Instance;
+ *
+ * @Inject
+ * Instance<WebSocketConnector<MyEndpoint>> connector;
+ *
+ * void connect() {
+ *      var connection1 = connector.get().baseUri(uri)
+ *                  .addHeader("Foo", "alpha")
+ *                  .connectAndAwait();
+ *      var connection2 = connector.get().baseUri(uri)
+ *                  .addHeader("Foo", "bravo")
+ *                  .connectAndAwait();
+ * }
+ * 
* * @param The client endpoint class * @see WebSocketClient diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/BasicWebSocketConnectorImpl.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/BasicWebSocketConnectorImpl.java index bf6dd1044e0bf..45dffc6c30849 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/BasicWebSocketConnectorImpl.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/runtime/BasicWebSocketConnectorImpl.java @@ -239,7 +239,11 @@ public void handle(Void event) { trafficLogger.connectionClosed(connection); } if (closeHandler != null) { - doExecute(connection, new CloseReason(ws.closeStatusCode(), ws.closeReason()), closeHandler); + CloseReason reason = CloseReason.INTERNAL_SERVER_ERROR; + if (ws.closeStatusCode() != null) { + reason = new CloseReason(ws.closeStatusCode(), ws.closeReason()); + } + doExecute(connection, reason, closeHandler); } connectionManager.remove(BasicWebSocketConnectorImpl.class.getName(), connection); client.get().close(); diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContext.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContext.java index 3eb1af3732511..7dc987859495a 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContext.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContext.java @@ -97,7 +97,7 @@ public class BootstrapMavenContext { private static final String SETTINGS_XML = "settings.xml"; private static final String SETTINGS_SECURITY = "settings.security"; - private static final String EFFECTIVE_MODEL_BUILDER_PROP = "quarkus.bootstrap.effective-model-builder"; + static final String EFFECTIVE_MODEL_BUILDER_PROP = "quarkus.bootstrap.effective-model-builder"; private static final String WARN_ON_FAILING_WS_MODULES_PROP = "quarkus.bootstrap.warn-on-failing-workspace-modules"; private static final String MAVEN_RESOLVER_TRANSPORT_KEY = "maven.resolver.transport"; @@ -1080,8 +1080,7 @@ public boolean isPreferPomsFromWorkspace() { public boolean isEffectiveModelBuilder() { if (effectiveModelBuilder == null) { - final String s = PropertyUtils.getProperty(EFFECTIVE_MODEL_BUILDER_PROP); - effectiveModelBuilder = s == null ? false : Boolean.parseBoolean(s); + effectiveModelBuilder = Boolean.getBoolean(EFFECTIVE_MODEL_BUILDER_PROP); } return effectiveModelBuilder; } diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContextConfig.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContextConfig.java index 6dc0d5ebb242b..47a80712c78b3 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContextConfig.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/BootstrapMavenContextConfig.java @@ -6,6 +6,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.Properties; import java.util.function.Function; import org.apache.maven.model.Model; @@ -20,6 +21,23 @@ public class BootstrapMavenContextConfig> { + /** + * Resolves the effective value of the {@code effective-model-builder} option by looking for the + * {@code quarkus.bootstrap.effective-model-builder} property among the system properties and, + * if not set, in the properties argument. + *

+ * If the property is found, the method will return the result of {@link java.lang.Boolean#parseBoolean}. + * If the property is not set, the method will return false. + * + * @param props primary source of properties + * @return whether effective model builder should be enabled + */ + public static boolean getEffectiveModelBuilderProperty(Properties props) { + final String value = System.getProperty(BootstrapMavenContext.EFFECTIVE_MODEL_BUILDER_PROP); + return value == null ? Boolean.parseBoolean(props.getProperty(BootstrapMavenContext.EFFECTIVE_MODEL_BUILDER_PROP)) + : Boolean.parseBoolean(value); + } + protected String localRepo; protected String[] localRepoTail; protected Boolean localRepoTailIgnoreAvailability; diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalProject.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalProject.java index f97744c845d07..ca502db7afa0a 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalProject.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalProject.java @@ -366,71 +366,19 @@ public WorkspaceModule toWorkspaceModule(BootstrapMavenContext ctx) { .setBuildDir(getOutputDir()); final Model model = modelBuildingResult == null ? getRawModel() : modelBuildingResult.getEffectiveModel(); - if (!ArtifactCoords.TYPE_POM.equals(model.getPackaging())) { - final Build build = model.getBuild(); - boolean addDefaultSourceSet = true; - if (build != null && !build.getPlugins().isEmpty()) { - for (Plugin plugin : build.getPlugins()) { - if (plugin.getArtifactId().equals("maven-jar-plugin")) { - if (plugin.getExecutions().isEmpty()) { - final DefaultArtifactSources src = processJarPluginExecutionConfig(plugin.getConfiguration(), - false); - if (src != null) { - addDefaultSourceSet = false; - moduleBuilder.addArtifactSources(src); - } - } else { - for (PluginExecution e : plugin.getExecutions()) { - DefaultArtifactSources src = null; - if (e.getGoals().contains(ArtifactCoords.TYPE_JAR)) { - src = processJarPluginExecutionConfig(e.getConfiguration(), false); - addDefaultSourceSet &= !(src != null && e.getId().equals("default-jar")); - } else if (e.getGoals().contains("test-jar")) { - src = processJarPluginExecutionConfig(e.getConfiguration(), true); - } - if (src != null) { - moduleBuilder.addArtifactSources(src); - } - } - } - } else if (plugin.getArtifactId().equals("maven-surefire-plugin") && plugin.getConfiguration() != null) { - Object config = plugin.getConfiguration(); - if (!(config instanceof Xpp3Dom)) { - continue; - } - Xpp3Dom dom = (Xpp3Dom) config; - final Xpp3Dom depExcludes = dom.getChild("classpathDependencyExcludes"); - if (depExcludes != null) { - final Xpp3Dom[] excludes = depExcludes.getChildren("classpathDependencyExclude"); - if (excludes != null) { - final List list = new ArrayList<>(excludes.length); - for (Xpp3Dom exclude : excludes) { - list.add(exclude.getValue()); - } - moduleBuilder.setTestClasspathDependencyExclusions(list); - } - } - final Xpp3Dom additionalElements = dom.getChild("additionalClasspathElements"); - if (additionalElements != null) { - final Xpp3Dom[] elements = additionalElements.getChildren("additionalClasspathElement"); - if (elements != null) { - final List list = new ArrayList<>(elements.length); - for (Xpp3Dom element : elements) { - for (String s : element.getValue().split(",")) { - list.add(stripProjectBasedirPrefix(s, PROJECT_BASEDIR)); - } - } - moduleBuilder.setAdditionalTestClasspathElements(list); - } - } - } - } - } - + if (!ArtifactCoords.TYPE_POM.equals(getPackaging())) { + final List plugins = model.getBuild() == null ? List.of() : model.getBuild().getPlugins(); + boolean addDefaultSourceSet = addSourceSetsFromPlugins(plugins, moduleBuilder); if (addDefaultSourceSet) { - moduleBuilder.addArtifactSources(new DefaultArtifactSources(ArtifactSources.MAIN, - List.of(new DefaultSourceDir(getSourcesSourcesDir(), getClassesDir(), getGeneratedSourcesDir())), - collectMainResources(null))); + var pluginManagement = model.getBuild() == null ? null : model.getBuild().getPluginManagement(); + if (pluginManagement != null) { + addDefaultSourceSet = addSourceSetsFromPlugins(pluginManagement.getPlugins(), moduleBuilder); + } + if (addDefaultSourceSet) { + moduleBuilder.addArtifactSources(new DefaultArtifactSources(ArtifactSources.MAIN, + List.of(new DefaultSourceDir(getSourcesSourcesDir(), getClassesDir(), getGeneratedSourcesDir())), + collectMainResources(null))); + } } if (!moduleBuilder.hasTestSources()) { // FIXME: do tests have generated sources? @@ -454,6 +402,70 @@ public WorkspaceModule toWorkspaceModule(BootstrapMavenContext ctx) { return this.module = moduleBuilder.build(); } + private boolean addSourceSetsFromPlugins(List plugins, WorkspaceModule.Mutable moduleBuilder) { + boolean addDefaultSourceSet = true; + int processedPlugins = 0; + for (int i = 0; i < plugins.size() && processedPlugins < 2; ++i) { + var plugin = plugins.get(i); + if (plugin.getArtifactId().equals("maven-jar-plugin")) { + ++processedPlugins; + if (plugin.getExecutions().isEmpty()) { + final DefaultArtifactSources src = processJarPluginExecutionConfig(plugin.getConfiguration(), + false); + if (src != null) { + addDefaultSourceSet = false; + moduleBuilder.addArtifactSources(src); + } + } else { + for (PluginExecution e : plugin.getExecutions()) { + DefaultArtifactSources src = null; + if (e.getGoals().contains(ArtifactCoords.TYPE_JAR)) { + src = processJarPluginExecutionConfig(e.getConfiguration(), false); + addDefaultSourceSet &= !(src != null && e.getId().equals("default-jar")); + } else if (e.getGoals().contains("test-jar")) { + src = processJarPluginExecutionConfig(e.getConfiguration(), true); + } + if (src != null) { + moduleBuilder.addArtifactSources(src); + } + } + } + } else if (plugin.getArtifactId().equals("maven-surefire-plugin") && plugin.getConfiguration() != null) { + ++processedPlugins; + Object config = plugin.getConfiguration(); + if (!(config instanceof Xpp3Dom)) { + continue; + } + Xpp3Dom dom = (Xpp3Dom) config; + final Xpp3Dom depExcludes = dom.getChild("classpathDependencyExcludes"); + if (depExcludes != null) { + final Xpp3Dom[] excludes = depExcludes.getChildren("classpathDependencyExclude"); + if (excludes != null) { + final List list = new ArrayList<>(excludes.length); + for (Xpp3Dom exclude : excludes) { + list.add(exclude.getValue()); + } + moduleBuilder.setTestClasspathDependencyExclusions(list); + } + } + final Xpp3Dom additionalElements = dom.getChild("additionalClasspathElements"); + if (additionalElements != null) { + final Xpp3Dom[] elements = additionalElements.getChildren("additionalClasspathElement"); + if (elements != null) { + final List list = new ArrayList<>(elements.length); + for (Xpp3Dom element : elements) { + for (String s : element.getValue().split(",")) { + list.add(stripProjectBasedirPrefix(s, PROJECT_BASEDIR)); + } + } + moduleBuilder.setAdditionalTestClasspathElements(list); + } + } + } + } + return addDefaultSourceSet; + } + private List toArtifactDependencies(List rawModelDeps, BootstrapMavenContext ctx) { if (rawModelDeps.isEmpty()) { @@ -509,7 +521,7 @@ private DefaultArtifactSources processJarPluginExecutionConfig(Object config, bo new DefaultSourceDir(new DirectoryPathTree(test ? getTestSourcesSourcesDir() : getSourcesSourcesDir()), new DirectoryPathTree(test ? getTestClassesDir() : getClassesDir(), filter), // FIXME: wrong for tests - new DirectoryPathTree(test ? getGeneratedSourcesDir() : getGeneratedSourcesDir(), filter), + new DirectoryPathTree(getGeneratedSourcesDir(), filter), Map.of())); final Collection resources = test ? collectTestResources(filter) : collectMainResources(filter); return new DefaultArtifactSources(classifier, sources, resources); diff --git a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalWorkspace.java b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalWorkspace.java index e68e4affef2e8..a570d9f80455f 100644 --- a/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalWorkspace.java +++ b/independent-projects/bootstrap/maven-resolver/src/main/java/io/quarkus/bootstrap/resolver/maven/workspace/LocalWorkspace.java @@ -124,16 +124,12 @@ public File findArtifact(Artifact artifact) { return path.toFile(); } - if (!artifact.getClassifier().isEmpty()) { - if ("tests".equals(artifact.getClassifier())) { - //special classifier used for test jars - path = lp.getTestClassesDir(); - if (Files.exists(path)) { - return path.toFile(); - } + if ("tests".equals(artifact.getClassifier())) { + //special classifier used for test jars + path = lp.getTestClassesDir(); + if (Files.exists(path)) { + return path.toFile(); } - // otherwise, this artifact hasn't been built yet - return null; } if (ArtifactCoords.TYPE_JAR.equals(artifact.getExtension())) { diff --git a/independent-projects/resteasy-reactive/pom.xml b/independent-projects/resteasy-reactive/pom.xml index 8a13dcabcfb69..e485b49589cae 100644 --- a/independent-projects/resteasy-reactive/pom.xml +++ b/independent-projects/resteasy-reactive/pom.xml @@ -58,7 +58,7 @@ 2.6.2 2.8.0 - 4.5.9 + 4.5.11 5.5.0 1.0.0.Final 2.18.1 @@ -67,7 +67,7 @@ 3.0.4 3.0.1 4.2.2 - 3.13.2 + 3.16.0 1.0.4 5.14.2 1.1.0 diff --git a/independent-projects/vertx-utils/pom.xml b/independent-projects/vertx-utils/pom.xml index 9e1e39cca4375..f256694260ca4 100644 --- a/independent-projects/vertx-utils/pom.xml +++ b/independent-projects/vertx-utils/pom.xml @@ -17,7 +17,7 @@ 3.6.1.Final - 4.5.7 + 4.5.11 diff --git a/integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java b/integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java index 11e6a1d3675b7..a8ff4305d70c9 100644 --- a/integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java +++ b/integration-tests/elytron-resteasy/src/main/java/io/quarkus/it/resteasy/elytron/RootResource.java @@ -16,6 +16,8 @@ import jakarta.ws.rs.core.SecurityContext; import io.quarkus.security.Authenticated; +import io.quarkus.security.PermissionChecker; +import io.quarkus.security.PermissionsAllowed; import io.quarkus.security.identity.SecurityIdentity; @Path("/") @@ -73,4 +75,19 @@ public String getAttributes() { .map(e -> e.getKey() + "=" + e.getValue()) .collect(Collectors.joining(",")); } + + @GET + @Path("/test-security-permission-checker") + @PermissionsAllowed("see-principal") + public String getPrincipal(@Context SecurityContext sec) { + return sec.getUserPrincipal().getName() + ":" + identity.getPrincipal().getName() + ":" + principal.getName(); + } + + @PermissionChecker("see-principal") + boolean canSeePrincipal(SecurityContext sec) { + if (sec.getUserPrincipal() == null || sec.getUserPrincipal().getName() == null) { + return false; + } + return "meat loaf".equals(sec.getUserPrincipal().getName()); + } } diff --git a/integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java b/integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java index 25017f7014fc4..43158fc4d374c 100644 --- a/integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java +++ b/integration-tests/elytron-resteasy/src/test/java/io/quarkus/it/resteasy/elytron/TestSecurityTestCase.java @@ -11,6 +11,7 @@ import jakarta.inject.Inject; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -23,6 +24,7 @@ import io.quarkus.test.security.AttributeType; import io.quarkus.test.security.SecurityAttribute; import io.quarkus.test.security.TestSecurity; +import io.restassured.RestAssured; @QuarkusTest class TestSecurityTestCase { @@ -187,4 +189,24 @@ static Stream arrayParams() { arguments(new int[] { 1, 2 }, new String[] { "hello", "world" })); } + @Test + public void testPermissionChecker_anonymousUser() { + // user is not authenticated and access should not be granted by the permission checker + RestAssured.get("/test-security-permission-checker").then().statusCode(401); + } + + @Test + @TestSecurity(user = "authenticated-user") + public void testPermissionChecker_authenticatedUser() { + // user is authenticated, but access should not be granted by the permission checker + RestAssured.get("/test-security-permission-checker").then().statusCode(403); + } + + @Test + @TestSecurity(user = "meat loaf") + public void testPermissionChecker_authorizedUser() { + // user is authenticated and access should be granted by the permission checker + RestAssured.get("/test-security-permission-checker").then().statusCode(200) + .body(Matchers.is("meat loaf:meat loaf:meat loaf")); + } } diff --git a/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/build.gradle.kts b/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/build.gradle.kts new file mode 100644 index 0000000000000..8d6264bd678f9 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + java + id("io.quarkus") +} + +repositories { + mavenCentral() + mavenLocal() +} + +val quarkusPlatformGroupId: String by project +val quarkusPlatformArtifactId: String by project +val quarkusPlatformVersion: String by project + +dependencies { + implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")) +} + +quarkus { + buildForkOptions { + println("message!") + } +} diff --git a/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/gradle.properties b/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/gradle.properties new file mode 100644 index 0000000000000..ec2b6ef199c2c --- /dev/null +++ b/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/gradle.properties @@ -0,0 +1,2 @@ +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus diff --git a/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/settings.gradle b/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/settings.gradle new file mode 100644 index 0000000000000..73c92a8563f3a --- /dev/null +++ b/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + mavenLocal { + content { + includeGroupByRegex 'io.quarkus.*' + includeGroup 'org.hibernate.orm' + } + } + mavenCentral() + gradlePluginPortal() + } + plugins { + id 'io.quarkus' version "${quarkusPluginVersion}" + } +} +rootProject.name='code-with-quarkus' diff --git a/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/src/main/java/org/acme/EntryPoint.java b/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/src/main/java/org/acme/EntryPoint.java new file mode 100644 index 0000000000000..ed5d9a989519d --- /dev/null +++ b/integration-tests/gradle/src/main/resources/basic-java-application-with-fork-options/src/main/java/org/acme/EntryPoint.java @@ -0,0 +1,9 @@ +package org.acme; + + + +public class EntryPoint { + public static void main(String[] args) { + + } +} diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/build.gradle.kts b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/build.gradle.kts new file mode 100644 index 0000000000000..c6260a98129f5 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/build.gradle.kts @@ -0,0 +1,15 @@ +allprojects { + + group = "org.acme" + version = "1.0.0-SNAPSHOT" + + repositories { + mavenLocal { + content { + includeGroupByRegex("io.quarkus.*") + includeGroup("org.hibernate.orm") + } + } + mavenCentral() + } +} diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/gradle.properties b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/gradle.properties new file mode 100644 index 0000000000000..0b9b349582ac3 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/gradle.properties @@ -0,0 +1,4 @@ +# Gradle properties + +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformGroupId=io.quarkus diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modA/build.gradle.kts b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modA/build.gradle.kts new file mode 100644 index 0000000000000..f9b4541bc516f --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modA/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + `java-library` + id("io.quarkus") + id("com.github.ben-manes.versions") version "0.51.0" + +} +val quarkusPlatformGroupId: String by project +val quarkusPlatformArtifactId: String by project +val quarkusPlatformVersion: String by project + +dependencies { + implementation(enforcedPlatform("$quarkusPlatformGroupId:$quarkusPlatformArtifactId:$quarkusPlatformVersion")) + api("io.quarkus:quarkus-resteasy") + api("io.quarkus:quarkus-resteasy-jackson") + api("io.quarkus:quarkus-arc") +} diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/.dockerignore b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/.dockerignore new file mode 100644 index 0000000000000..4361d2fb38ddc --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/.dockerignore @@ -0,0 +1,5 @@ +* +!build/*-runner +!build/*-runner.jar +!build/lib/* +!build/quarkus-app/* \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/build.gradle.kts b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/build.gradle.kts new file mode 100644 index 0000000000000..7db659d8359d7 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + `java-library` + id("io.quarkus") + id("com.github.ben-manes.versions") version "0.51.0" +} +val quarkusPlatformGroupId: String by project +val quarkusPlatformArtifactId: String by project +val quarkusPlatformVersion: String by project + +val javaVersion = "17" + +dependencies { + implementation(enforcedPlatform("$quarkusPlatformGroupId:$quarkusPlatformArtifactId:$quarkusPlatformVersion")) + implementation(project(":modA")) +} diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/java/org/acme/GreetingResource.java b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/java/org/acme/GreetingResource.java new file mode 100644 index 0000000000000..e5f5b6ca5694b --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/java/org/acme/GreetingResource.java @@ -0,0 +1,16 @@ +package org.acme; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class GreetingResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "foo bar"; + } +} diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/resources/META-INF/resources/index.html b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000000000..abf53e21ca41b --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,182 @@ + + + + + code-with-quarkus - 1.0.0-SNAPSHOT + + + + +

+ +
+
+

Congratulations, you have created a new Quarkus cloud application.

+ +

What is this page?

+ +

This page is served by Quarkus. The source is in + src/main/resources/META-INF/resources/index.html.

+ +

What are your next steps?

+ +

If not already done, run the application in dev mode using: ./gradlew quarkusDev. +

+
    +
  • Your static assets are located in src/main/resources/META-INF/resources.
  • +
  • Configure your application in src/main/resources/application.properties.
  • +
  • Quarkus now ships with a Dev UI (available in dev mode only)
  • +
  • Play with the getting started example code located in src/main/java:
  • +
+
+

RESTEasy JAX-RS example

+

REST is easy peasy with this Hello World RESTEasy resource.

+

@Path: /hello-resteasy

+

Related guide section...

+
+
+

RESTEasy JSON serialisation using Jackson

+

This example demonstrate RESTEasy JSON serialisation by letting you list, add and remove quark types from a list. Quarked!

+

@Path: /resteasy-jackson/quarks/

+

Related guide section...

+
+ +
+
+
+

Application

+
    +
  • GroupId: org.acme
  • +
  • ArtifactId: code-with-quarkus
  • +
  • Version: 1.0.0-SNAPSHOT
  • +
  • Quarkus Version: 1.12.2.Final
  • +
+
+
+

Do you like Quarkus?

+
    +
  • Go give it a star on GitHub.
  • +
+
+
+

Selected extensions guides

+ +
+ +
+
+ + \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/resources/application.properties b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/main/resources/application.properties new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/native-test/java/org/acme/NativeGreetingResourceIT.java b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/native-test/java/org/acme/NativeGreetingResourceIT.java new file mode 100644 index 0000000000000..909dc2e828c69 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/native-test/java/org/acme/NativeGreetingResourceIT.java @@ -0,0 +1,9 @@ +package org.acme; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class NativeGreetingResourceIT extends GreetingResourceTest { + + // Execute the same tests but in native mode. +} diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/test/java/org/acme/GreetingResourceTest.java b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/test/java/org/acme/GreetingResourceTest.java new file mode 100644 index 0000000000000..c832dcf849e72 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/modB/src/test/java/org/acme/GreetingResourceTest.java @@ -0,0 +1,21 @@ +package org.acme; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +@QuarkusTest +public class GreetingResourceTest { + + @Test + public void testHelloEndpoint() { + given() + .when().get("/hello") + .then() + .statusCode(200) + .body(is("foo bar")); + } + +} \ No newline at end of file diff --git a/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/settings.gradle.kts b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/settings.gradle.kts new file mode 100644 index 0000000000000..b93cd3eb8bc81 --- /dev/null +++ b/integration-tests/gradle/src/main/resources/quarkus-plugin-in-multiple-modules/settings.gradle.kts @@ -0,0 +1,23 @@ +pluginManagement { + val quarkusPluginVersion: String by settings + repositories { + mavenLocal { + content { + includeGroupByRegex("io.quarkus.*") + includeGroup("org.hibernate.orm") + } + } + mavenCentral() + gradlePluginPortal() + } + plugins { + id("io.quarkus") version quarkusPluginVersion + } +} + +rootProject.name="code-with-quarkus" + +include( + "modA", + "modB" +) diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/BuildForkOptionsAreIncludedInQuarkusBuildTaskTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/BuildForkOptionsAreIncludedInQuarkusBuildTaskTest.java new file mode 100644 index 0000000000000..dcddf7cdabcdb --- /dev/null +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/BuildForkOptionsAreIncludedInQuarkusBuildTaskTest.java @@ -0,0 +1,16 @@ +package io.quarkus.gradle; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class BuildForkOptionsAreIncludedInQuarkusBuildTaskTest extends QuarkusGradleWrapperTestBase { + + @Test + public void testBuildForkOptionsAreProcessed() throws Exception { + var projectDir = getProjectDir("basic-java-application-with-fork-options"); + var buildResult = runGradleWrapper(projectDir, "clean", "quarkusBuild"); + assertThat(BuildResult.isSuccessful(buildResult.getTasks().get(":quarkusGenerateCode"))).isTrue(); + assertThat(buildResult.getOutput().contains("message!")).isTrue(); + } +} diff --git a/integration-tests/gradle/src/test/java/io/quarkus/gradle/QuarkusAppliedToMultipleModulesTest.java b/integration-tests/gradle/src/test/java/io/quarkus/gradle/QuarkusAppliedToMultipleModulesTest.java new file mode 100644 index 0000000000000..849ed8abd92b7 --- /dev/null +++ b/integration-tests/gradle/src/test/java/io/quarkus/gradle/QuarkusAppliedToMultipleModulesTest.java @@ -0,0 +1,18 @@ +package io.quarkus.gradle; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; + +import org.junit.jupiter.api.Test; + +public class QuarkusAppliedToMultipleModulesTest extends QuarkusGradleWrapperTestBase { + + @Test + public void testBasicMultiModuleBuild() throws Exception { + final File projectDir = getProjectDir("quarkus-plugin-in-multiple-modules"); + final BuildResult build = runGradleWrapper(projectDir, "clean", "quarkusBuild"); + assertThat(BuildResult.isSuccessful(build.getTasks().get(":modA:quarkusBuild"))).isTrue(); + assertThat(BuildResult.isSuccessful(build.getTasks().get(":modB:quarkusBuild"))).isTrue(); + } +} diff --git a/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingResource.kt b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingResource.kt index f03c9a4729413..a1d0804d563c0 100644 --- a/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingResource.kt +++ b/integration-tests/kotlin-serialization/src/main/kotlin/io/quarkus/it/kotser/GreetingResource.kt @@ -73,5 +73,17 @@ class GreetingResource { return Response.ok().entity(javaMethod.invoke(this)).build() } + @GET + @Path("emptyList") + fun emptyList(): List { + return emptyList() + } + + @GET + @Path("emptyMap") + fun emptyMap(): Map { + return emptyMap() + } + fun reflect() = "hello, world" } diff --git a/integration-tests/kotlin-serialization/src/test/kotlin/io/quarkus/it/kotser/ResourceTest.kt b/integration-tests/kotlin-serialization/src/test/kotlin/io/quarkus/it/kotser/ResourceTest.kt index df95d3b5ea7e3..35d43c86e164f 100644 --- a/integration-tests/kotlin-serialization/src/test/kotlin/io/quarkus/it/kotser/ResourceTest.kt +++ b/integration-tests/kotlin-serialization/src/test/kotlin/io/quarkus/it/kotser/ResourceTest.kt @@ -124,4 +124,14 @@ open class ResourceTest { body(CoreMatchers.equalTo("hello, world")) } } + + @Test + fun testEmptyList() { + When { get("/emptyList") } Then { statusCode(200) } + } + + @Test + fun testEmptyMap() { + When { get("/emptyList") } Then { statusCode(200) } + } } diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitBase.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitBase.java index 565d098e0fa6e..d9c682e6d5c30 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitBase.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithFlywayInitBase.java @@ -29,9 +29,7 @@ public void assertGeneratedResources(Path kubernetesDir, String name, String tas && name.equals(d.getMetadata().getName())) .map(d -> (Deployment) d).findAny(); - assertTrue(deployment.isPresent()); - assertThat(deployment).satisfies(j -> j.isPresent()); - assertThat(deployment.get()).satisfies(d -> { + assertThat(deployment).isPresent().get().satisfies(d -> { assertThat(d.getMetadata()).satisfies(m -> { assertThat(m.getName()).isEqualTo(name); }); @@ -56,9 +54,7 @@ public void assertGeneratedResources(Path kubernetesDir, String name, String tas .filter(j -> "Job".equals(j.getKind()) && jobName.equals(j.getMetadata().getName())) .map(j -> (Job) j) .findAny(); - assertTrue(job.isPresent()); - - assertThat(job.get()).satisfies(j -> { + assertThat(job).isPresent().get().satisfies(j -> { assertThat(j.getSpec()).satisfies(jobSpec -> { assertThat(jobSpec.getCompletionMode()).isEqualTo("NonIndexed"); assertThat(jobSpec.getTemplate()).satisfies(t -> { @@ -69,7 +65,7 @@ public void assertGeneratedResources(Path kubernetesDir, String name, String tas assertThat(podSpec.getRestartPolicy()).isEqualTo("OnFailure"); assertThat(podSpec.getContainers()).singleElement().satisfies(container -> { assertThat(container.getName()).isEqualTo(jobName); - assertThat(container.getEnv()).filteredOn(env -> "QUARKUS_FLYWAY_ENABLED".equals(env.getName())) + assertThat(container.getEnv()).filteredOn(env -> "QUARKUS_FLYWAY_ACTIVE".equals(env.getName())) .singleElement().satisfies(env -> { assertThat(env.getValue()).isEqualTo("true"); }); diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java index f9a1957dd172a..3f0873bc20084 100644 --- a/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/DevMojoIT.java @@ -1622,4 +1622,12 @@ public void testThatAptInAnnotationProcessorsWorks() throws MavenInvocationExcep assertThat(entityMetamodelClassFile).exists(); assertThat(entityQueryClassFile).doesNotExist(); } + + @Test + void testMultimoduleFilteredClassifier() + throws MavenInvocationException, IOException { + testDir = initProject("projects/multimodule-filtered-classifier"); + run(true); + assertThat(devModeClient.getHttpResponse("/")).isEqualTo("Big"); + } } diff --git a/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/app/pom.xml b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/app/pom.xml new file mode 100644 index 0000000000000..4b0d7b99b3c82 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/app/pom.xml @@ -0,0 +1,36 @@ + + + + acme-parent + org.acme + 1.0.0-SNAPSHOT + + 4.0.0 + + acme-app + + + org.acme + \${project.version} + acme-lib + shared + + + + + + \${quarkus.platform.group-id} + quarkus-maven-plugin + + + + build + + + + + + + \ No newline at end of file diff --git a/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/app/src/main/java/org/acme/App.java b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/app/src/main/java/org/acme/App.java new file mode 100644 index 0000000000000..6f9e51cb54958 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/app/src/main/java/org/acme/App.java @@ -0,0 +1,19 @@ +package org.acme; + +import org.acme.shared.BigBean; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/") +public class App { + + @Inject + BigBean bean; + + @GET + public String get() { + return bean.getName(); + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/pom.xml b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/pom.xml new file mode 100644 index 0000000000000..dddbb8780fc0d --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/pom.xml @@ -0,0 +1,14 @@ + + + + acme-parent + org.acme + 1.0.0-SNAPSHOT + + 4.0.0 + + acme-lib + + \ No newline at end of file diff --git a/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/BigBeanProducer.java b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/BigBeanProducer.java new file mode 100644 index 0000000000000..993e92be768f1 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/BigBeanProducer.java @@ -0,0 +1,15 @@ +package org.acme; + +import jakarta.enterprise.inject.Produces; +import org.acme.shared.BigBean; + +/** + * The purpose of this class is to create a conflict with the shared BigBeanProducer + */ +public class BigBeanProducer { + + @Produces + public BigBean getName() { + return new BigBean(); + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/shared/BigBean.java b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/shared/BigBean.java new file mode 100644 index 0000000000000..e74b855a968a6 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/shared/BigBean.java @@ -0,0 +1,7 @@ +package org.acme.shared; + +public class BigBean { + public String getName() { + return "Big"; + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/shared/BigBeanProducer.java b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/shared/BigBeanProducer.java new file mode 100644 index 0000000000000..eabf45b6f1524 --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/java/org/acme/shared/BigBeanProducer.java @@ -0,0 +1,11 @@ +package org.acme.shared; + +import jakarta.enterprise.inject.Produces; + +public class BigBeanProducer { + + @Produces + public BigBean getBigBean() { + return new BigBean(); + } +} diff --git a/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/resources/META-INF/beans.xml b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/library/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/pom.xml b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/pom.xml new file mode 100644 index 0000000000000..09ebd99118eda --- /dev/null +++ b/integration-tests/maven/src/test/resources-filtered/projects/multimodule-filtered-classifier/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + org.acme + acme-parent + pom + 1.0.0-SNAPSHOT + + library + app + + + + @project.version@ + ${compiler-plugin.version} + ${version.surefire.plugin} + ${maven.compiler.source} + ${maven.compiler.target} + UTF-8 + UTF-8 + quarkus-bom + io.quarkus + true + + + + + \${quarkus.platform.group-id} + \${quarkus.platform.artifact-id} + \${quarkus.platform.version} + pom + import + + + + + + io.quarkus + quarkus-rest + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + package + + jar + + + shared + + **/shared/* + META-INF/* + + + + + + + \${quarkus.platform.group-id} + quarkus-maven-plugin + \${quarkus.platform.version} + true + + + maven-compiler-plugin + \${compiler-plugin.version} + + + + + + \${quarkus.platform.group-id} + quarkus-maven-plugin + + + + diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/AbstractExporterTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/AbstractExporterTest.java index 1ba0b74775661..980bebaed537b 100644 --- a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/AbstractExporterTest.java +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/AbstractExporterTest.java @@ -132,17 +132,21 @@ private Metric getMetric(final String metricName) { .atMost(Duration.ofSeconds(30)) .untilAsserted(() -> { List reqs = metrics.getMetricRequests(); - assertThat(reqs).hasSizeGreaterThan(1); + Optional metric = getMetric(metricName, reqs); + assertThat(metric).isPresent(); }); final List metricRequests = metrics.getMetricRequests(); + return getMetric(metricName, metricRequests).get(); + } + private Optional getMetric(String metricName, List metricRequests) { return metricRequests.stream() .flatMap(reqs -> reqs.getResourceMetricsList().stream()) .flatMap(resourceMetrics -> resourceMetrics.getScopeMetricsList().stream()) .flatMap(libraryMetrics -> libraryMetrics.getMetricsList().stream()) .filter(metric -> metric.getName().equals(metricName)) - .findFirst().get(); + .findFirst(); } private void verifyLogs() { diff --git a/integration-tests/opentelemetry/src/main/resources/application.properties b/integration-tests/opentelemetry/src/main/resources/application.properties index 68c3b9664276b..1992d76c95677 100644 --- a/integration-tests/opentelemetry/src/main/resources/application.properties +++ b/integration-tests/opentelemetry/src/main/resources/application.properties @@ -21,4 +21,3 @@ quarkus.security.users.embedded.plain-text=true quarkus.security.users.embedded.enabled=true quarkus.http.auth.basic=true -quarkus.native.monitoring=jfr diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/CustomPermission.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/CustomPermission.java new file mode 100644 index 0000000000000..5325e49879a92 --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/CustomPermission.java @@ -0,0 +1,10 @@ +package io.quarkus.it.keycloak; + +import java.security.BasicPermission; + +public class CustomPermission extends BasicPermission { + + public CustomPermission(String name) { + super(name); + } +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java index d9187dc7e9d1f..fdfbe0128baee 100644 --- a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/ProtectedJwtResource.java @@ -14,6 +14,7 @@ import org.eclipse.microprofile.jwt.JsonWebToken; import io.quarkus.security.Authenticated; +import io.quarkus.security.PermissionsAllowed; import io.quarkus.security.identity.SecurityIdentity; @Path("/web-app") @@ -40,6 +41,14 @@ public String testSecurity() { + principal.getName(); } + @GET + @Path("test-security-with-augmentors") + @PermissionsAllowed(permission = CustomPermission.class, value = "augmented") + public String testSecurityWithAugmentors() { + return securityContext.getUserPrincipal().getName() + ":" + identity.getPrincipal().getName() + ":" + + principal.getName(); + } + @POST @Path("test-security") @Consumes("application/json") diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/TestSecurityIdentityAugmentor.java b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/TestSecurityIdentityAugmentor.java new file mode 100644 index 0000000000000..6e8662823ad16 --- /dev/null +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/java/io/quarkus/it/keycloak/TestSecurityIdentityAugmentor.java @@ -0,0 +1,37 @@ +package io.quarkus.it.keycloak; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; + +@ApplicationScoped +public class TestSecurityIdentityAugmentor implements SecurityIdentityAugmentor { + + private static volatile boolean invoked = false; + + @Override + public Uni augment(SecurityIdentity securityIdentity, + AuthenticationRequestContext authenticationRequestContext) { + invoked = true; + final SecurityIdentity identity; + if (securityIdentity.isAnonymous() || !"authorized-user".equals(securityIdentity.getPrincipal().getName())) { + identity = securityIdentity; + } else { + identity = QuarkusSecurityIdentity.builder(securityIdentity) + .addPermission(new CustomPermission("augmented")).build(); + } + return Uni.createFrom().item(identity); + } + + public static boolean isInvoked() { + return invoked; + } + + public static void resetInvoked() { + invoked = false; + } +} diff --git a/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties b/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties index fa4f560b45c04..f843b520dc74e 100644 --- a/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties +++ b/integration-tests/smallrye-jwt-token-propagation/src/main/resources/application.properties @@ -1,3 +1,5 @@ +quarkus.keycloak.devservices.enabled=false + mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus smallrye.jwt.path.groups=realm_access/roles diff --git a/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java b/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java index 45812da96c15a..a573cb8e6af55 100644 --- a/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java +++ b/integration-tests/smallrye-jwt-token-propagation/src/test/java/io/quarkus/it/keycloak/TestSecurityLazyAuthTest.java @@ -7,6 +7,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import io.quarkus.test.common.http.TestHTTPEndpoint; @@ -21,6 +22,53 @@ @TestHTTPEndpoint(ProtectedJwtResource.class) public class TestSecurityLazyAuthTest { + @Test + public void testTestSecurityAnnotationWithAugmentors_anonymousUser() { + TestSecurityIdentityAugmentor.resetInvoked(); + // user is not authenticated and doesn't have required role granted by the augmentor + RestAssured.get("test-security-with-augmentors").then().statusCode(401); + // identity manager applies augmentors on anonymous identity + // because @TestSecurity is not in action and that's what we do for the anonymous requests + Assertions.assertTrue(TestSecurityIdentityAugmentor.isInvoked()); + } + + @TestSecurity(user = "authenticated-user") + @Test + public void testTestSecurityAnnotationNoAugmentors_authenticatedUser() { + TestSecurityIdentityAugmentor.resetInvoked(); + // user is authenticated, but doesn't have required role granted by the augmentor + // and no augmentors are applied + RestAssured.get("test-security-with-augmentors").then().statusCode(403); + Assertions.assertFalse(TestSecurityIdentityAugmentor.isInvoked()); + } + + @TestSecurity(user = "authenticated-user", augmentors = TestSecurityIdentityAugmentor.class) + @Test + public void testTestSecurityAnnotationWithAugmentors_authenticatedUser() { + TestSecurityIdentityAugmentor.resetInvoked(); + // user is authenticated, but doesn't have required role granted by the augmentor + RestAssured.get("test-security-with-augmentors").then().statusCode(403); + Assertions.assertTrue(TestSecurityIdentityAugmentor.isInvoked()); + } + + @TestSecurity(user = "authorized-user") + @Test + public void testTestSecurityAnnotationNoAugmentors_authorizedUser() { + // should fail because no augmentors are applied + TestSecurityIdentityAugmentor.resetInvoked(); + RestAssured.get("test-security-with-augmentors").then().statusCode(403); + Assertions.assertFalse(TestSecurityIdentityAugmentor.isInvoked()); + } + + @TestSecurity(user = "authorized-user", augmentors = TestSecurityIdentityAugmentor.class) + @Test + public void testTestSecurityAnnotationWithAugmentors_authorizedUser() { + TestSecurityIdentityAugmentor.resetInvoked(); + RestAssured.get("test-security-with-augmentors").then().statusCode(200) + .body(is("authorized-user:authorized-user:authorized-user")); + Assertions.assertTrue(TestSecurityIdentityAugmentor.isInvoked()); + } + @Test @TestAsUser1Viewer public void testWithDummyUser() { diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java index 8521d7407200a..d5f606290bebe 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java @@ -319,13 +319,15 @@ private Set uniqueTestResourceClassEntries(Path testClas * Allows Quarkus to extra basic information about which test resources a test class will require */ public static Set testResourceComparisonInfo(Class testClass, - Path testClassLocation) { + Path testClassLocation, List entriesFromProfile) { Set uniqueEntries = getUniqueTestResourceClassEntries(testClass, testClassLocation, null); - if (uniqueEntries.isEmpty()) { + if (uniqueEntries.isEmpty() && entriesFromProfile.isEmpty()) { return Collections.emptySet(); } - Set result = new HashSet<>(uniqueEntries.size()); - for (TestResourceClassEntry entry : uniqueEntries) { + Set allEntries = new HashSet<>(uniqueEntries); + allEntries.addAll(entriesFromProfile); + Set result = new HashSet<>(allEntries.size()); + for (TestResourceClassEntry entry : allEntries) { Map args = new HashMap<>(entry.args); if (entry.configAnnotation != null) { args.put("configAnnotation", entry.configAnnotation.annotationType().getName()); diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java index 1dd9e846a844f..84f460e27704e 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java @@ -153,7 +153,8 @@ private QuarkusTestExtensionState ensureStarted(ExtensionContext extensionContex // we reload the test resources if we changed test class and if we had or will have per-test test resources boolean reloadTestResources = false; if ((state == null && !failedBoot) || wrongProfile || (reloadTestResources = isNewTestClass - && TestResourceUtil.testResourcesRequireReload(state, extensionContext.getRequiredTestClass()))) { + && TestResourceUtil.testResourcesRequireReload(state, extensionContext.getRequiredTestClass(), + selectedProfile))) { if (wrongProfile || reloadTestResources) { if (state != null) { try { diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java index 71776250160ec..107c2f9b60b81 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java @@ -67,7 +67,8 @@ private void ensurePrepared(ExtensionContext extensionContext, Class nextTestClass) { + static boolean testResourcesRequireReload(QuarkusTestExtensionState state, Class nextTestClass, + Class nextTestClassProfile) { + QuarkusTestProfile profileInstance = null; + if (nextTestClassProfile != null) { + try { + profileInstance = nextTestClassProfile.getConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } Set existingTestResources = existingTestResources(state); - Set nextTestResources = nextTestResources(nextTestClass); + Set nextTestResources = nextTestResources(nextTestClass, + profileInstance); return TestResourceManager.testResourcesRequireReload(existingTestResources, nextTestResources); } @@ -54,9 +64,20 @@ static Set existingTestResources return Collections.emptySet(); } - static Set nextTestResources(Class requiredTestClass) { + static Set nextTestResources(Class requiredTestClass, + QuarkusTestProfile profileInstance) { + + List entriesFromProfile = Collections.emptyList(); + if (profileInstance != null) { + entriesFromProfile = new ArrayList<>(profileInstance.testResources().size()); + for (QuarkusTestProfile.TestResourceEntry entry : profileInstance.testResources()) { + entriesFromProfile.add(new TestResourceManager.TestResourceClassEntry(entry.getClazz(), entry.getArgs(), null, + entry.isParallel(), TestResourceScope.MATCHING_RESOURCES)); + } + } + return TestResourceManager - .testResourceComparisonInfo(requiredTestClass, getTestClassesLocation(requiredTestClass)); + .testResourceComparisonInfo(requiredTestClass, getTestClassesLocation(requiredTestClass), entriesFromProfile); } /** @@ -92,7 +113,7 @@ static List copyEntriesFromProfile( T instance = (T) testResourceClassEntryConstructor.newInstance( Class.forName(testResource.getClazz().getName(), true, classLoader), testResource.getArgs(), null, testResource.isParallel(), - Enum.valueOf(testResourceScopeClass, TestResourceScope.RESTRICTED_TO_CLASS.name())); + Enum.valueOf(testResourceScopeClass, TestResourceScope.MATCHING_RESOURCES.name())); result.add(instance); } diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/AbstractTestHttpAuthenticationMechanism.java b/test-framework/security/src/main/java/io/quarkus/test/security/AbstractTestHttpAuthenticationMechanism.java index 1359b83e041f7..732b7623d79dd 100644 --- a/test-framework/security/src/main/java/io/quarkus/test/security/AbstractTestHttpAuthenticationMechanism.java +++ b/test-framework/security/src/main/java/io/quarkus/test/security/AbstractTestHttpAuthenticationMechanism.java @@ -1,15 +1,24 @@ package io.quarkus.test.security; +import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.ROUTING_CONTEXT_ATTRIBUTE; + import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import jakarta.annotation.PostConstruct; +import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import io.quarkus.runtime.LaunchMode; +import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.security.identity.request.AuthenticationRequest; +import io.quarkus.security.spi.runtime.BlockingSecurityExecutor; import io.quarkus.vertx.http.runtime.security.ChallengeData; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport; @@ -21,7 +30,11 @@ abstract class AbstractTestHttpAuthenticationMechanism implements HttpAuthentica @Inject TestIdentityAssociation testIdentityAssociation; + @Inject + BlockingSecurityExecutor blockingSecurityExecutor; + protected volatile String authMechanism = null; + protected volatile List> augmentors = null; @PostConstruct public void check() { @@ -32,8 +45,21 @@ public void check() { } @Override - public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { - return Uni.createFrom().item(testIdentityAssociation.getTestIdentity()); + public Uni authenticate(RoutingContext event, IdentityProviderManager identityProviderManager) { + var identity = Uni.createFrom().item(testIdentityAssociation.getTestIdentity()); + if (augmentors != null && testIdentityAssociation.getTestIdentity() != null) { + var requestContext = new AuthenticationRequestContext() { + @Override + public Uni runBlocking(Supplier supplier) { + return blockingSecurityExecutor.executeBlocking(supplier); + } + }; + var requestAttributes = Map. of(ROUTING_CONTEXT_ATTRIBUTE, event); + for (var augmentor : augmentors) { + identity = identity.flatMap(i -> augmentor.get().augment(i, requestContext, requestAttributes)); + } + } + return identity; } @Override @@ -55,4 +81,8 @@ public Uni getCredentialTransport(RoutingContext contex void setAuthMechanism(String authMechanism) { this.authMechanism = authMechanism; } + + void setSecurityIdentityAugmentors(List> augmentors) { + this.augmentors = augmentors; + } } diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java b/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java index ef556bad11fc8..fc6ac4d24f67d 100644 --- a/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java +++ b/test-framework/security/src/main/java/io/quarkus/test/security/QuarkusSecurityTestExtension.java @@ -5,9 +5,11 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.security.Permission; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -17,8 +19,12 @@ import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.spi.CDI; +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; import io.quarkus.security.StringPermission; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.runtime.QuarkusPermissionSecurityIdentityAugmentor; import io.quarkus.security.runtime.QuarkusPrincipal; import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback; @@ -34,11 +40,13 @@ public class QuarkusSecurityTestExtension implements QuarkusTestBeforeEachCallba public void afterEach(QuarkusTestMethodContext context) { try { if (getAnnotationContainer(context).isPresent()) { - CDI.current().select(TestAuthController.class).get().setEnabled(true); - for (var testMechanism : CDI.current().select(AbstractTestHttpAuthenticationMechanism.class)) { + final ArcContainer container = Arc.container(); + container.select(TestAuthController.class).get().setEnabled(true); + for (var testMechanism : container.select(AbstractTestHttpAuthenticationMechanism.class)) { testMechanism.setAuthMechanism(null); + testMechanism.setSecurityIdentityAugmentors(null); } - var testIdentity = CDI.current().select(TestIdentityAssociation.class).get(); + var testIdentity = container.select(TestIdentityAssociation.class).get(); testIdentity.setTestIdentity(null); testIdentity.setPathBasedIdentity(false); } @@ -59,7 +67,8 @@ public void beforeEach(QuarkusTestMethodContext context) { var annotationContainer = annotationContainerOptional.get(); Annotation[] allAnnotations = annotationContainer.getElement().getAnnotations(); TestSecurity testSecurity = annotationContainer.getAnnotation(); - CDI.current().select(TestAuthController.class).get().setEnabled(testSecurity.authorizationEnabled()); + final ArcContainer container = Arc.container(); + container.select(TestAuthController.class).get().setEnabled(testSecurity.authorizationEnabled()); if (testSecurity.user().isEmpty()) { if (testSecurity.roles().length != 0) { throw new RuntimeException("Cannot specify roles without a username in @TestSecurity"); @@ -82,12 +91,37 @@ public void beforeEach(QuarkusTestMethodContext context) { } SecurityIdentity userIdentity = augment(user.build(), allAnnotations); - CDI.current().select(TestIdentityAssociation.class).get().setTestIdentity(userIdentity); + container.select(TestIdentityAssociation.class).get().setTestIdentity(userIdentity); if (!testSecurity.authMechanism().isEmpty()) { - for (var testMechanism : CDI.current().select(AbstractTestHttpAuthenticationMechanism.class)) { + for (var testMechanism : container.select(AbstractTestHttpAuthenticationMechanism.class)) { testMechanism.setAuthMechanism(testSecurity.authMechanism()); } - CDI.current().select(TestIdentityAssociation.class).get().setPathBasedIdentity(true); + container.select(TestIdentityAssociation.class).get().setPathBasedIdentity(true); + } + + // run SecurityIdentityAugmentors when: + List> augmentors = new ArrayList<>(); + // 1. user opted-in with @TestSecurity#augmentors, run augmentors listed by user + for (Class augmentorClass : testSecurity.augmentors()) { + var augmentorInstance = container.select(augmentorClass); + if (!augmentorInstance.isResolvable()) { + var testMethodName = context.getTestMethod() == null ? "" : context.getTestMethod().getName(); + throw new RuntimeException(""" + SecurityIdentityAugmentor class '%s' specified with '@TestSecurity#augmentors' annotation + attribute on method '%s' is not available as a CDI bean. + """.formatted(augmentorClass, testMethodName)); + } + augmentors.add(augmentorInstance); + } + // 2. @PermissionChecker is used, run the augmentor that enables this functionality + var quarkusPermissionAugmentor = container.select(QuarkusPermissionSecurityIdentityAugmentor.class); + if (quarkusPermissionAugmentor.isResolvable()) { + augmentors.add(quarkusPermissionAugmentor); + } + if (!augmentors.isEmpty()) { + for (var testMechanism : container.select(AbstractTestHttpAuthenticationMechanism.class)) { + testMechanism.setSecurityIdentityAugmentors(augmentors); + } } } } catch (Exception e) { diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java b/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java index 569a9f266e880..807209452d474 100644 --- a/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java +++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestIdentityAssociation.java @@ -26,12 +26,12 @@ public void check() { } } - volatile SecurityIdentity testIdentity; + private volatile SecurityIdentity testIdentity; /** * Whether authentication is successful only if right mechanism was used to authenticate. */ - volatile boolean isPathBasedIdentity = false; + private volatile boolean isPathBasedIdentity = false; /** * A request scoped delegate that allows the system to function as normal when diff --git a/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java b/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java index 36543039989e4..a0fc93a1725fd 100644 --- a/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java +++ b/test-framework/security/src/main/java/io/quarkus/test/security/TestSecurity.java @@ -8,6 +8,7 @@ import io.quarkus.security.PermissionsAllowed; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) @@ -35,11 +36,18 @@ * That is, permission is separated from actions with {@link PermissionsAllowed#PERMISSION_TO_ACTION_SEPARATOR}. * For example, value {@code see:detail} gives permission to {@code see} action {@code detail}. * All permissions are added as {@link io.quarkus.security.StringPermission}. - * If you need to test custom permissions, you can add them with - * {@link io.quarkus.security.identity.SecurityIdentityAugmentor}. + * {@link io.quarkus.security.PermissionChecker} methods always authorize matched {@link PermissionsAllowed#value()} + * permissions. This annotation attribute cannot grant access to permissions granted by the checker methods. */ String[] permissions() default {}; + /** + * Specify {@link SecurityIdentityAugmentor} CDI beans that should augment {@link SecurityIdentity} created with + * this annotation. By default, no identity augmentors are applied. Use this option if you need to test + * custom {@link PermissionsAllowed#permission()} added with the identity augmentors. + */ + Class[] augmentors() default {}; + /** * Adds attributes to a {@link SecurityIdentity} configured by this annotation. * The attributes can be retrieved by the {@link SecurityIdentity#getAttributes()} method.