diff --git a/applications/mp/pom.xml b/applications/mp/pom.xml index 01db13152e1..38fe118e36a 100644 --- a/applications/mp/pom.xml +++ b/applications/mp/pom.xml @@ -32,7 +32,7 @@ Parent pom for Helidon MP applications - 1.0.6 + 3.1.2 0.14.0 2.7.5.1 6.1.7.Final @@ -43,7 +43,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin ${version.plugin.jandex} diff --git a/applications/parent/pom.xml b/applications/parent/pom.xml index 2590acbc805..d5c493315a8 100644 --- a/applications/parent/pom.xml +++ b/applications/parent/pom.xml @@ -39,7 +39,7 @@ ${maven.compiler.source} ${maven.compiler.source} 3.8.1 - 3.1.2 + 3.6.0 1.6.0 3.0.0-M5 3.0.3 @@ -108,7 +108,6 @@ true true runtime - test diff --git a/archetypes/helidon/src/main/archetype/mp/common/common-mp.xml b/archetypes/helidon/src/main/archetype/mp/common/common-mp.xml index 84e42a8dd19..b9be77a795f 100644 --- a/archetypes/helidon/src/main/archetype/mp/common/common-mp.xml +++ b/archetypes/helidon/src/main/archetype/mp/common/common-mp.xml @@ -1,7 +1,7 @@ - - io.helidon.reactive.webclient - helidon-reactive-webclient-jaxrs - ${helidon.version} - io.helidon.reactive.webclient helidon-reactive-webclient @@ -1492,6 +1487,18 @@ ${helidon.version} + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-processor + ${helidon.version} + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-runtime + ${helidon.version} + + diff --git a/builder/builder-config-processor/src/main/java/io/helidon/builder/config/processor/ConfigBeanBuilderCreator.java b/builder/builder-config-processor/src/main/java/io/helidon/builder/config/processor/ConfigBeanBuilderCreator.java index 85b2ffe1f94..38e2976d9db 100644 --- a/builder/builder-config-processor/src/main/java/io/helidon/builder/config/processor/ConfigBeanBuilderCreator.java +++ b/builder/builder-config-processor/src/main/java/io/helidon/builder/config/processor/ConfigBeanBuilderCreator.java @@ -50,7 +50,7 @@ import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNameDefault; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; import io.helidon.config.metadata.ConfiguredOption; import static io.helidon.builder.config.spi.ConfigBeanInfo.LevelType; @@ -124,7 +124,7 @@ protected void preValidate(TypeName implTypeName, * Generic/simple map types are not supported on config beans, only <String, <Known ConfigBean types>>. */ private void assertNoGenericMaps(TypeInfo typeInfo) { - List list = typeInfo.elementInfo().stream() + List list = typeInfo.interestingElementInfo().stream() .filter(it -> it.typeName().isMap()) .filter(it -> { TypeName typeName = it.typeName(); @@ -292,7 +292,7 @@ protected void appendExtraBuilderMethods(StringBuilder builder, int i = 0; for (String attrName : ctx.allAttributeNames()) { - TypedElementName method = ctx.allTypeInfos().get(i); + TypedElementInfo method = ctx.allTypeInfos().get(i); String configKey = toConfigKey(attrName, method, ctx.builderTriggerAnnotation()); // resolver.of(config, "port", int.class).ifPresent(this::port); @@ -449,7 +449,7 @@ private void javaDocAcceptResolveConfigCtx(StringBuilder builder, } private String toConfigKey(String attrName, - TypedElementName method, + TypedElementInfo method, AnnotationAndValue ignoredBuilderAnnotation) { String configKey = null; Optional configuredOptions = AnnotationAndValueDefault @@ -473,7 +473,7 @@ private static void assertNoAnnotation(String annoTypeName, + " on " + typeInfo.typeName()); } - for (TypedElementName elem : typeInfo.elementInfo()) { + for (TypedElementInfo elem : typeInfo.interestingElementInfo()) { anno = AnnotationAndValueDefault.findFirst(annoTypeName, elem.annotations()); if (anno.isEmpty()) { anno = AnnotationAndValueDefault.findFirst(annoTypeName, elem.elementTypeAnnotations()); diff --git a/builder/builder-config-processor/src/test/java/io/helidon/builder/config/processor/ConfigBeanBuilderCreatorTest.java b/builder/builder-config-processor/src/test/java/io/helidon/builder/config/processor/ConfigBeanBuilderCreatorTest.java index c2bef78c0d6..19b231ece3f 100644 --- a/builder/builder-config-processor/src/test/java/io/helidon/builder/config/processor/ConfigBeanBuilderCreatorTest.java +++ b/builder/builder-config-processor/src/test/java/io/helidon/builder/config/processor/ConfigBeanBuilderCreatorTest.java @@ -29,8 +29,8 @@ import io.helidon.common.types.TypeInfoDefault; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNameDefault; -import io.helidon.common.types.TypedElementName; -import io.helidon.common.types.TypedElementNameDefault; +import io.helidon.common.types.TypedElementInfo; +import io.helidon.common.types.TypedElementInfoDefault; import org.junit.jupiter.api.Test; @@ -111,11 +111,11 @@ void preValidateConfigBeansMustBeRootToHaveDefaults() { @Test void preValidateConfigBeansMustNotHaveDuplicateSingularNames() { - TypedElementName method1 = TypedElementNameDefault.builder() + TypedElementInfo method1 = TypedElementInfoDefault.builder() .elementName("socket") .typeName(String.class) .build(); - TypedElementName method2 = TypedElementNameDefault.builder() + TypedElementInfo method2 = TypedElementInfoDefault.builder() .elementName("socketSet") .typeName(String.class) .addAnnotation(AnnotationAndValueDefault.create(Singular.class, "socket")) @@ -124,7 +124,7 @@ void preValidateConfigBeansMustNotHaveDuplicateSingularNames() { TypeInfo typeInfo = TypeInfoDefault.builder() .typeKind(TypeInfo.KIND_INTERFACE) .typeName(TypeNameDefault.create(getClass())) - .elementInfo(Set.of(method1, method2)) + .interestingElementInfo(Set.of(method1, method2)) .build(); AnnotationAndValueDefault configBeanAnno = AnnotationAndValueDefault.builder() .typeName(TypeNameDefault.create(ConfigBean.class)) @@ -141,7 +141,7 @@ void preValidateConfigBeansMustNotHaveDuplicateSingularNames() { @Test void preValidateConfigBeansMustHaveMapTypesWithNestedConfigBeans() { - TypedElementName method1 = TypedElementNameDefault.builder() + TypedElementInfo method1 = TypedElementInfoDefault.builder() .elementName("socket") .typeName(TypeNameDefault.builder() .type(Map.class) @@ -154,7 +154,7 @@ void preValidateConfigBeansMustHaveMapTypesWithNestedConfigBeans() { TypeInfo typeInfo = TypeInfoDefault.builder() .typeKind(TypeInfo.KIND_INTERFACE) .typeName(TypeNameDefault.create(getClass())) - .elementInfo(Set.of(method1)) + .interestingElementInfo(Set.of(method1)) .build(); AnnotationAndValueDefault configBeanAnno = AnnotationAndValueDefault.builder() .typeName(TypeNameDefault.create(ConfigBean.class)) @@ -172,7 +172,7 @@ void preValidateConfigBeansMustHaveMapTypesWithNestedConfigBeans() { creator.preValidate(implTypeName, typeInfo, configBeanAnno); // now we will validate the exceptions when ConfigBeans are attempted to be embedded - TypedElementName method2 = TypedElementNameDefault.builder() + TypedElementInfo method2 = TypedElementInfoDefault.builder() .elementName("unsupported1") .typeName(TypeNameDefault.builder() .type(Map.class) @@ -182,7 +182,7 @@ void preValidateConfigBeansMustHaveMapTypesWithNestedConfigBeans() { TypeNameDefault.create(getClass()))) .build()) .build(); - TypedElementName method3 = TypedElementNameDefault.builder() + TypedElementInfo method3 = TypedElementInfoDefault.builder() .elementName("unsupported2") .typeName(TypeNameDefault.builder() .type(Map.class) @@ -195,7 +195,7 @@ void preValidateConfigBeansMustHaveMapTypesWithNestedConfigBeans() { TypeInfo typeInfo2 = TypeInfoDefault.builder() .typeKind(TypeInfo.KIND_INTERFACE) .typeName(TypeNameDefault.create(getClass())) - .elementInfo(List.of(method2, method3)) + .interestingElementInfo(List.of(method2, method3)) .referencedTypeNamesToAnnotations(Map.of(TypeNameDefault.create(getClass()), List.of(AnnotationAndValueDefault.create(Builder.class)))) .build(); diff --git a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeInfoCreatorProvider.java b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeInfoCreatorProvider.java index b18db69c59c..1b7bb01b295 100644 --- a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeInfoCreatorProvider.java +++ b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeInfoCreatorProvider.java @@ -25,7 +25,7 @@ import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; /** * Java {@link java.util.ServiceLoader} provider interface used to discover type info creators. @@ -58,13 +58,13 @@ Optional createBuilderTypeInfo(TypeName annoTypeName, * @param mirror the type mirror for the element being processed * @param processingEnv the processing environment * @param elementOfInterest the predicate filter to determine whether the element is of interest, and therefore should be - * included in {@link TypeInfo#elementInfo()}. Otherwise, if the predicate indicates it is not of + * included in {@link TypeInfo#interestingElementInfo()}. Otherwise, if the predicate indicates it is not of * interest then the method will be placed under {@link TypeInfo#otherElementInfo()} instead * @return the type info associated with the arguments being processed, or empty if not able to process the type */ Optional createTypeInfo(TypeElement element, TypeMirror mirror, ProcessingEnvironment processingEnv, - Predicate elementOfInterest); + Predicate elementOfInterest); } diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BeanUtils.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BeanUtils.java index ba6f566493b..335f2eb78f9 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BeanUtils.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BeanUtils.java @@ -138,7 +138,7 @@ public static boolean validateAndParseMethodName(String methodName, * @return true if it appears to be a reserved word */ public static boolean isReservedWord(String word) { - return RESERVED.get().contains(word.toUpperCase()); + return RESERVED.get().stream().anyMatch(word::equalsIgnoreCase); } /** diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BodyContext.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BodyContext.java index ef254c27b01..a6885130a5c 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BodyContext.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BodyContext.java @@ -30,7 +30,7 @@ import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNameDefault; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; import static io.helidon.builder.processor.tools.BeanUtils.isBooleanType; import static io.helidon.builder.processor.tools.BeanUtils.isReservedWord; @@ -54,8 +54,8 @@ public class BodyContext { private final TypeName implTypeName; private final TypeInfo typeInfo; private final AnnotationAndValue builderTriggerAnnotation; - private final Map map = new LinkedHashMap<>(); - private final List allTypeInfos = new ArrayList<>(); + private final Map map = new LinkedHashMap<>(); + private final List allTypeInfos = new ArrayList<>(); private final List allAttributeNames = new ArrayList<>(); private final boolean hasStreamSupportOnImpl; private final boolean hasStreamSupportOnBuilder; @@ -126,10 +126,11 @@ public class BodyContext { searchForBuilderAnnotation("interceptorCreateMethod", builderTriggerAnnotation, typeInfo); this.interceptorCreateMethod = (interceptorCreateMethod == null || interceptorCreateMethod.isEmpty()) ? null : interceptorCreateMethod; - this.publicOrPackagePrivateDecl = (typeInfo.typeKind().equals(TypeInfo.KIND_INTERFACE) - || typeInfo.modifierNames().isEmpty() - || typeInfo.modifierNames().contains(TypeInfo.MODIFIER_PUBLIC)) - ? "public " : ""; + this.publicOrPackagePrivateDecl = ( + typeInfo.typeKind().equals(TypeInfo.KIND_INTERFACE) + || typeInfo.modifierNames().isEmpty() + || typeInfo.modifierNames().stream().anyMatch(TypeInfo.MODIFIER_PUBLIC::equalsIgnoreCase)) + ? "public " : ""; } @Override @@ -179,7 +180,7 @@ public AnnotationAndValue builderTriggerAnnotation() { * * @return the map of elements by name */ - protected Map map() { + protected Map map() { return map; } @@ -188,7 +189,7 @@ protected Map map() { * * @return the list of type elements */ - public List allTypeInfos() { + public List allTypeInfos() { return allTypeInfos; } @@ -427,7 +428,7 @@ public Optional interceptorCreateMethod() { */ public boolean hasOtherMethod(String name, TypeInfo typeInfo) { - for (TypedElementName elem : typeInfo.otherElementInfo()) { + for (TypedElementInfo elem : typeInfo.otherElementInfo()) { if (elem.elementName().equals(name)) { return true; } @@ -447,7 +448,7 @@ public boolean hasOtherMethod(String name, * @param isBeanStyleRequired is bean style required * @return the bean attribute name */ - protected static String toBeanAttributeName(TypedElementName method, + protected static String toBeanAttributeName(TypedElementInfo method, boolean isBeanStyleRequired) { AtomicReference>> refAttrNames = new AtomicReference<>(); validateAndParseMethodName(method.elementName(), method.typeName().name(), isBeanStyleRequired, refAttrNames); @@ -584,9 +585,9 @@ private void gatherAllAttributeNames(TypeInfo typeInfo) { } } - for (TypedElementName method : typeInfo.elementInfo()) { + for (TypedElementInfo method : typeInfo.interestingElementInfo()) { String beanAttributeName = toBeanAttributeName(method, beanStyleRequired); - TypedElementName existing = map.get(beanAttributeName); + TypedElementInfo existing = map.get(beanAttributeName); if (existing != null && isBooleanType(method.typeName().name()) && method.elementName().startsWith("is")) { @@ -629,16 +630,16 @@ && isBooleanType(method.typeName().name()) } } - private static void populateMap(Map map, + private static void populateMap(Map map, TypeInfo typeInfo, boolean isBeanStyleRequired) { if (typeInfo.superTypeInfo().isPresent()) { populateMap(map, typeInfo.superTypeInfo().get(), isBeanStyleRequired); } - for (TypedElementName method : typeInfo.elementInfo()) { + for (TypedElementInfo method : typeInfo.interestingElementInfo()) { String beanAttributeName = toBeanAttributeName(method, isBeanStyleRequired); - TypedElementName existing = map.get(beanAttributeName); + TypedElementInfo existing = map.get(beanAttributeName); if (existing != null) { if (!existing.typeName().equals(method.typeName())) { throw new IllegalStateException(method + " cannot redefine types from super for " + beanAttributeName); @@ -683,7 +684,7 @@ private static TypeName toCtorBuilderAcceptTypeName(TypeInfo typeInfo, return typeInfo.typeName(); } - return (parentAnnotationTypeName != null && typeInfo.elementInfo().isEmpty() + return (parentAnnotationTypeName != null && typeInfo.interestingElementInfo().isEmpty() ? typeInfo.superTypeInfo().orElseThrow().typeName() : typeInfo.typeName()); } diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BuilderTypeTools.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BuilderTypeTools.java index 71c872e9c7f..35521795c00 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BuilderTypeTools.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BuilderTypeTools.java @@ -58,8 +58,8 @@ import io.helidon.common.types.TypeInfoDefault; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNameDefault; -import io.helidon.common.types.TypedElementName; -import io.helidon.common.types.TypedElementNameDefault; +import io.helidon.common.types.TypedElementInfo; +import io.helidon.common.types.TypedElementInfoDefault; // this is really ok! import com.sun.source.util.TreePath; @@ -109,17 +109,15 @@ public Optional createBuilderTypeInfo(TypeName annotationTypeName, throw new IllegalStateException(msg); } - Collection elementInfo = toElementInfo(element, processingEnv, true, wantDefaultMethods); - Collection otherElementInfo = toElementInfo(element, processingEnv, false, wantDefaultMethods); - Set modifierNames = toModifierNames(element.getModifiers()).stream() - .map(String::toUpperCase) - .collect(Collectors.toSet()); + Collection elementInfo = toElementInfo(element, processingEnv, true, wantDefaultMethods); + Collection otherElementInfo = toElementInfo(element, processingEnv, false, wantDefaultMethods); + Set modifierNames = toModifierNames(element.getModifiers()); return Optional.of(TypeInfoDefault.builder() .typeName(typeName) .typeKind(String.valueOf(element.getKind())) .annotations( createAnnotationAndValueListFromElement(element, processingEnv.getElementUtils())) - .elementInfo(elementInfo) + .interestingElementInfo(elementInfo) .otherElementInfo(otherElementInfo) .referencedTypeNamesToAnnotations( toReferencedTypeNamesAndAnnotations( @@ -134,7 +132,7 @@ public Optional createBuilderTypeInfo(TypeName annotationTypeName, public Optional createTypeInfo(TypeElement element, TypeMirror mirror, ProcessingEnvironment processingEnv, - Predicate isOneWeCareAbout) { + Predicate isOneWeCareAbout) { return toTypeInfo(element, mirror, processingEnv, isOneWeCareAbout); } @@ -178,12 +176,12 @@ static Map> toMetaAnnotations(Collection> toReferencedTypeNamesAndAnnotations(ProcessingEnvironment processingEnv, TypeName typeName, - Collection... refs) { + Collection... refs) { Map> result = new LinkedHashMap<>(); - for (Collection ref : refs) { - for (TypedElementName typedElementName : ref) { - collectReferencedTypeNames(result, processingEnv, typeName, List.of(typedElementName.typeName())); - collectReferencedTypeNames(result, processingEnv, typeName, typedElementName.typeName().typeArguments()); + for (Collection ref : refs) { + for (TypedElementInfo typedElementInfo : ref) { + collectReferencedTypeNames(result, processingEnv, typeName, List.of(typedElementInfo.typeName())); + collectReferencedTypeNames(result, processingEnv, typeName, typedElementInfo.typeName().typeArguments()); } } return result; @@ -208,7 +206,7 @@ private void collectReferencedTypeNames(Map> } /** - * Translation the arguments to a collection of {@link io.helidon.common.types.TypedElementName}'s. + * Translation the arguments to a collection of {@link TypedElementInfo}'s. * * @param element the typed element (i.e., class) * @param processingEnv the processing env @@ -216,7 +214,7 @@ private void collectReferencedTypeNames(Map> * @param wantDefaultMethods true to process {@code default} methods * @return the collection of typed elements */ - private Collection toElementInfo(TypeElement element, + private Collection toElementInfo(TypeElement element, ProcessingEnvironment processingEnv, boolean wantWhatWeCanAccept, boolean wantDefaultMethods) { @@ -224,7 +222,7 @@ private Collection toElementInfo(TypeElement element, .filter(it -> it.getKind() == ElementKind.METHOD) .map(ExecutableElement.class::cast) .filter(it -> (wantWhatWeCanAccept == canAccept(it, wantDefaultMethods))) - .map(it -> createTypedElementNameFromElement(it, processingEnv.getElementUtils())) + .map(it -> createTypedElementInfoFromElement(it, processingEnv.getElementUtils())) .filter(Optional::isPresent) .map(Optional::get) .collect(Collectors.toList()); @@ -288,14 +286,14 @@ private Optional toBuilderTypeInfo(TypeName annotationTypeName, * @param mirror the type mirror for the element being processed * @param processingEnv the processing environment * @param isOneWeCareAbout the predicate filter to determine whether the element is of interest, and therefore should be - * included in {@link TypeInfo#elementInfo()}. Otherwise, if the predicate indicates it is not of + * included in {@link TypeInfo#interestingElementInfo()}. Otherwise, if the predicate indicates it is not of * interest then the method will be placed under {@link TypeInfo#otherElementInfo()} instead * @return the type info associated with the arguments being processed, or empty if not able to process the type */ static Optional toTypeInfo(TypeElement element, TypeMirror mirror, ProcessingEnvironment processingEnv, - Predicate isOneWeCareAbout) { + Predicate isOneWeCareAbout) { TypeName fqTypeName = createTypeNameFromMirror(mirror).orElseThrow(); if (fqTypeName.name().equals(Object.class.getName())) { return Optional.empty(); @@ -317,10 +315,10 @@ static Optional toTypeInfo(TypeElement element, createAnnotationAndValueSet(elementUtils.getTypeElement(genericTypeName.name())); Map> referencedAnnotations = new LinkedHashMap<>(toMetaAnnotations(annotations, processingEnv)); - List elementsWeCareAbout = new ArrayList<>(); - List otherElements = new ArrayList<>(); + List elementsWeCareAbout = new ArrayList<>(); + List otherElements = new ArrayList<>(); element.getEnclosedElements().stream() - .map(it -> createTypedElementNameFromElement(it, elementUtils)) + .map(it -> createTypedElementInfoFromElement(it, elementUtils)) .filter(Optional::isPresent) .map(Optional::get) .forEach(it -> { @@ -339,7 +337,7 @@ static Optional toTypeInfo(TypeElement element, .annotations(annotations) .referencedTypeNamesToAnnotations(referencedAnnotations) .modifierNames(toModifierNames(element.getModifiers())) - .elementInfo(elementsWeCareAbout) + .interestingElementInfo(elementsWeCareAbout) .otherElementInfo(otherElements); // add all of the element's and parameters to the references annotation set @@ -356,7 +354,7 @@ static Optional toTypeInfo(TypeElement element, .filter(t -> !t.generic()) .forEach(allInterestingTypeNames::add); it.parameterArguments().stream() - .map(TypedElementName::typeName) + .map(TypedElementInfo::typeName) .map(TypeName::genericTypeName) .filter(t -> !isBuiltInJavaType(t)) .filter(t -> !t.generic()) @@ -706,19 +704,7 @@ public static Map extractValues(Map createTypedElementNameFromElement(Element v, + public static Optional createTypedElementInfoFromElement(Element v, Elements elements) { TypeName type = createTypeNameFromElement(v).orElse(null); TypeMirror typeMirror = null; String defaultValue = null; - List params = List.of(); - List componentTypeNames = List.of(); + List params = List.of(); List elementTypeAnnotations = List.of(); Set modifierNames = v.getModifiers().stream() .map(Modifier::toString) @@ -742,7 +727,7 @@ public static Optional createTypedElementNameFromElement(Eleme ExecutableElement ee = (ExecutableElement) v; typeMirror = Objects.requireNonNull(ee.getReturnType()); params = ee.getParameters().stream() - .map(it -> createTypedElementNameFromElement(it, elements).orElseThrow()) + .map(it -> createTypedElementInfoFromElement(it, elements).orElseThrow()) .collect(Collectors.toList()); AnnotationValue annotationValue = ee.getDefaultValue(); defaultValue = (annotationValue == null) ? null @@ -762,20 +747,13 @@ public static Optional createTypedElementNameFromElement(Eleme type = createTypeNameFromMirror(typeMirror).orElse(createFromGenericDeclaration(typeMirror.toString())); } if (typeMirror instanceof DeclaredType) { - List args = ((DeclaredType) typeMirror).getTypeArguments(); - componentTypeNames = args.stream() - .map(BuilderTypeTools::createTypeNameFromMirror) - .filter(Optional::isPresent) - .map(Optional::orElseThrow) - .collect(Collectors.toList()); elementTypeAnnotations = createAnnotationAndValueListFromElement(((DeclaredType) typeMirror).asElement(), elements); } } - TypedElementNameDefault.Builder builder = TypedElementNameDefault.builder() + TypedElementInfoDefault.Builder builder = TypedElementInfoDefault.builder() .typeName(type) - .componentTypeNames(componentTypeNames) .elementName(v.getSimpleName().toString()) .elementKind(v.getKind().name()) .annotations(createAnnotationAndValueListFromElement(v, elements)) diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/DefaultBuilderCreatorProvider.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/DefaultBuilderCreatorProvider.java index d97a4a1cdec..33d45efda7b 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/DefaultBuilderCreatorProvider.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/DefaultBuilderCreatorProvider.java @@ -47,7 +47,7 @@ import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNameDefault; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; import io.helidon.config.metadata.ConfiguredOption; import static io.helidon.builder.processor.tools.BodyContext.TAG_META_PROPS; @@ -127,7 +127,7 @@ private void assertNoDuplicateSingularNames(TypeInfo typeInfo) { Set names = new LinkedHashSet<>(); Set duplicateNames = new LinkedHashSet<>(); - typeInfo.elementInfo().stream() + typeInfo.interestingElementInfo().stream() .map(DefaultBuilderCreatorProvider::nameOf) .forEach(name -> { if (!names.add(name)) { @@ -342,7 +342,7 @@ protected void appendFields(StringBuilder builder, } for (int i = 0; i < ctx.allTypeInfos().size(); i++) { - TypedElementName method = ctx.allTypeInfos().get(i); + TypedElementInfo method = ctx.allTypeInfos().get(i); String beanAttributeName = ctx.allAttributeNames().get(i); appendAnnotations(builder, method.annotations(), "\t"); builder.append("\tprivate "); @@ -621,7 +621,7 @@ protected void appendVisitAttributes(StringBuilder builder, // void visit(String key, Object value, Object userDefinedCtx, Class type, Class... typeArgument); int i = 0; for (String attrName : ctx.allAttributeNames()) { - TypedElementName method = ctx.allTypeInfos().get(i); + TypedElementInfo method = ctx.allTypeInfos().get(i); TypeName typeName = method.typeName(); List typeArgs = method.typeName().typeArguments().stream() .map(this::normalize) @@ -810,7 +810,7 @@ protected String toConfigKey(String name, */ protected void maybeAppendSingularSetter(StringBuilder builder, BodyContext ctx, - TypedElementName method, + TypedElementInfo method, String beanAttributeName, boolean isList, boolean isMap, boolean isSet) { String singularVal = toValue(Singular.class, method, false, false).orElse(null); @@ -843,7 +843,7 @@ protected static String maybeSingularFormOf(String beanAttributeName) { * @param elem the element * @return the (singular) name of the element */ - protected static String nameOf(TypedElementName elem) { + protected static String nameOf(TypedElementInfo elem) { return AnnotationAndValueDefault.findFirst(Singular.class, elem.annotations()) .flatMap(AnnotationAndValue::value) .filter(BuilderTypeTools::hasNonBlankValue) @@ -863,7 +863,7 @@ protected void appendSetter(StringBuilder mainBuilder, BodyContext ctx, String beanAttributeName, String methodName, - TypedElementName method) { + TypedElementInfo method) { appendSetter(mainBuilder, ctx, beanAttributeName, methodName, method, true, ""); } @@ -871,7 +871,7 @@ private void appendSetter(StringBuilder mainBuilder, BodyContext ctx, String beanAttributeName, String methodName, - TypedElementName method, + TypedElementInfo method, boolean doClear, String prefixName) { TypeName typeName = method.typeName(); @@ -954,7 +954,7 @@ private void appendSetter(StringBuilder mainBuilder, protected void appendDirectNonOptionalSetter(StringBuilder builder, BodyContext ctx, String beanAttributeName, - TypedElementName method, + TypedElementInfo method, String methodName, TypeName genericType) { GenerateMethod.nonOptionalSetter(builder, ctx, beanAttributeName, method, methodName, genericType); @@ -991,7 +991,7 @@ protected void appendAnnotations(StringBuilder builder, * @param upLevelToCollection true if the generics should be "up leveled" * @return the generic decl */ - protected static String toGenerics(TypedElementName method, + protected static String toGenerics(TypedElementInfo method, boolean upLevelToCollection) { return toGenerics(method.typeName(), upLevelToCollection); } @@ -1043,7 +1043,7 @@ protected static String toString(Collection coll, * @param avoidBlanks flag indicating whether blank values should be ignored * @return the default value, or empty if there is no default value applicable for the given arguments */ - protected static Optional toConfiguredOptionValue(TypedElementName method, + protected static Optional toConfiguredOptionValue(TypedElementInfo method, boolean wantTypeElementDefaults, boolean avoidBlanks) { String val = toValue(ConfiguredOption.class, method, wantTypeElementDefaults, avoidBlanks).orElse(null); @@ -1060,7 +1060,7 @@ protected static Optional toConfiguredOptionValue(TypedElementName metho * @return the default value, or empty if there is no default value applicable for the given arguments */ protected static Optional toValue(Class annoType, - TypedElementName method, + TypedElementInfo method, boolean wantTypeElementDefaults, boolean avoidBlanks) { if (wantTypeElementDefaults && method.defaultValue().isPresent()) { @@ -1187,7 +1187,7 @@ private void appendBuilder(StringBuilder builder, } else { int i = 0; for (String beanAttributeName : ctx.allAttributeNames()) { - TypedElementName method = ctx.allTypeInfos().get(i); + TypedElementInfo method = ctx.allTypeInfos().get(i); TypeName typeName = method.typeName(); boolean isList = typeName.isList(); boolean isMap = !isList && typeName.isMap(); @@ -1268,7 +1268,7 @@ private void appendBuilder(StringBuilder builder, } i = 0; for (String beanAttributeName : ctx.allAttributeNames()) { - TypedElementName method = ctx.allTypeInfos().get(i++); + TypedElementInfo method = ctx.allTypeInfos().get(i++); TypeName typeName = method.typeName(); String getterName = method.elementName(); builder.append("\t\t\t").append(beanAttributeName).append("("); @@ -1301,7 +1301,7 @@ private void appendBuilderBody(StringBuilder builder, boolean hasFinal = false; for (int i = 0; i < ctx.allAttributeNames().size(); i++) { String beanAttributeName = ctx.allAttributeNames().get(i); - TypedElementName method = ctx.allTypeInfos().get(i); + TypedElementInfo method = ctx.allTypeInfos().get(i); TypeName typeName = method.typeName(); if (typeName.isList() || typeName.isMap() || typeName.isSet()) { hasFinal = true; @@ -1315,7 +1315,7 @@ private void appendBuilderBody(StringBuilder builder, // then any other field for (int i = 0; i < ctx.allAttributeNames().size(); i++) { String beanAttributeName = ctx.allAttributeNames().get(i); - TypedElementName method = ctx.allTypeInfos().get(i); + TypedElementInfo method = ctx.allTypeInfos().get(i); TypeName typeName = method.typeName(); if (typeName.isList() || typeName.isMap() || typeName.isSet()) { continue; @@ -1341,7 +1341,7 @@ private void appendBuilderBody(StringBuilder builder, private void addBuilderField(StringBuilder builder, BodyContext ctx, - TypedElementName method, + TypedElementInfo method, TypeName type, String beanAttributeName) { GenerateJavadoc.builderField(builder, method); @@ -1362,7 +1362,7 @@ private void addBuilderField(StringBuilder builder, private void addCollectionField(StringBuilder builder, BodyContext ctx, - TypedElementName method, + TypedElementInfo method, TypeName typeName, String beanAttributeName) { GenerateJavadoc.builderField(builder, method); @@ -1474,7 +1474,7 @@ private void appendInterfaceBasedGetters(StringBuilder builder, int i = 0; for (String beanAttributeName : ctx.allAttributeNames()) { - TypedElementName method = ctx.allTypeInfos().get(i); + TypedElementInfo method = ctx.allTypeInfos().get(i); String extraPrefix = prefix + "\t"; appendAnnotations(builder, method.annotations(), extraPrefix); builder.append(extraPrefix) @@ -1537,7 +1537,7 @@ protected void appendCtorCodeBody(StringBuilder builder, } int i = 0; for (String beanAttributeName : ctx.allAttributeNames()) { - TypedElementName method = ctx.allTypeInfos().get(i++); + TypedElementInfo method = ctx.allTypeInfos().get(i++); builder.append("\t\tthis.").append(beanAttributeName).append(" = "); if (method.typeName().isList()) { @@ -1573,7 +1573,7 @@ private void appendHashCodeAndEquals(StringBuilder builder, builder.append("\t\tint hashCode = 1;\n"); } List methods = new ArrayList<>(); - for (TypedElementName method : ctx.allTypeInfos()) { + for (TypedElementInfo method : ctx.allTypeInfos()) { methods.add(method.elementName() + "()"); } builder.append("\t\thashCode = 31 * hashCode + Objects.hash(").append(String.join(", ", methods)).append(");\n"); @@ -1595,7 +1595,7 @@ private void appendHashCodeAndEquals(StringBuilder builder, } else { builder.append("\t\tboolean equals = true;\n"); } - for (TypedElementName method : ctx.allTypeInfos()) { + for (TypedElementInfo method : ctx.allTypeInfos()) { String equalsClass = method.typeName().array() ? Arrays.class.getName() : "Objects"; builder.append("\t\tequals &= ").append(equalsClass).append(".equals(") .append(method.elementName()).append("(), other.") @@ -1630,7 +1630,7 @@ private void appendInnerToStringMethod(StringBuilder builder, int i = 0; for (String beanAttributeName : ctx.allAttributeNames()) { - TypedElementName method = ctx.allTypeInfos().get(i++); + TypedElementInfo method = ctx.allTypeInfos().get(i++); TypeName typeName = method.typeName(); builder.append("\t\tresult += \"").append(beanAttributeName).append("=\" + "); @@ -1674,7 +1674,7 @@ private void appendInnerToStringMethod(StringBuilder builder, } private void appendDefaultValueAssignment(StringBuilder builder, - TypedElementName method, + TypedElementInfo method, String defaultVal) { TypeName type = method.typeName(); boolean isOptional = type.isOptional(); @@ -1714,7 +1714,7 @@ private void appendDefaultValueAssignment(StringBuilder builder, private void appendOverridesOfDefaultValues(StringBuilder builder, BodyContext ctx) { - for (TypedElementName method : ctx.typeInfo().elementInfo()) { + for (TypedElementInfo method : ctx.typeInfo().interestingElementInfo()) { String beanAttributeName = toBeanAttributeName(method, ctx.beanStyleRequired()); if (!ctx.allAttributeNames().contains(beanAttributeName)) { // candidate for override... @@ -1734,7 +1734,7 @@ private String superValue(Optional optSuperTypeInfo, return null; } TypeInfo superTypeInfo = optSuperTypeInfo.get(); - Optional method = superTypeInfo.elementInfo().stream() + Optional method = superTypeInfo.interestingElementInfo().stream() .filter(it -> toBeanAttributeName(it, isBeanStyleRequired).equals(elemName)) .findFirst(); if (method.isPresent()) { @@ -1751,7 +1751,7 @@ private String superValue(Optional optSuperTypeInfo, private void appendDefaultOverride(StringBuilder builder, String attrName, - TypedElementName method, + TypedElementInfo method, String override) { builder.append("\t\t\t").append(attrName).append("("); appendDefaultValueAssignment(builder, method, override); @@ -1771,7 +1771,7 @@ private void appendCustomMapOf(StringBuilder builder) { } private String mapOf(String attrName, - TypedElementName method, + TypedElementInfo method, AtomicBoolean needsCustomMapOf) { Optional configuredOptions = AnnotationAndValueDefault .findFirst(ConfiguredOption.class, method.annotations()); diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateJavadoc.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateJavadoc.java index a1ad7a40b49..b3f45b2ef28 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateJavadoc.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateJavadoc.java @@ -17,7 +17,7 @@ package io.helidon.builder.processor.tools; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; final class GenerateJavadoc { private GenerateJavadoc() { @@ -106,7 +106,7 @@ static void updateConsumer(StringBuilder builder) { } static void builderField(StringBuilder builder, - TypedElementName method) { + TypedElementInfo method) { builder.append("\t\t/**\n" + "\t\t * Field value for {@code ") .append(method) .append("()}.\n\t\t */\n"); diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateMethod.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateMethod.java index 911c13d0254..99d6774910d 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateMethod.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateMethod.java @@ -21,7 +21,7 @@ import java.util.Optional; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; final class GenerateMethod { static final String SINGULAR_PREFIX = "add"; @@ -50,7 +50,7 @@ static String builderMethods(StringBuilder builder, static void stringToCharSetter(StringBuilder builder, BodyContext ctx, String beanAttributeName, - TypedElementName method, + TypedElementInfo method, String methodName) { GenerateJavadoc.setter(builder, beanAttributeName); builder.append("\t\tpublic ").append(ctx.genericBuilderAliasDecl()).append(" ").append(methodName) @@ -76,7 +76,7 @@ static void internalMetaAttributes(StringBuilder builder) { static void nonOptionalSetter(StringBuilder builder, BodyContext ctx, String beanAttributeName, - TypedElementName method, + TypedElementInfo method, String methodName, TypeName genericType) { GenerateJavadoc.setter(builder, beanAttributeName); @@ -102,7 +102,7 @@ static void nonOptionalSetter(StringBuilder builder, static void singularSetter(StringBuilder builder, BodyContext ctx, - TypedElementName method, + TypedElementInfo method, String beanAttributeName, char[] methodName) { TypeName typeName = method.typeName(); @@ -209,7 +209,7 @@ private static TypeName mapValueTypeNameOf(TypeName typeName) { return (typeName.isMap() && typeName.typeArguments().size() > 1) ? typeName.typeArguments().get(1) : null; } - private static String toGenericsDecl(TypedElementName method, + private static String toGenericsDecl(TypedElementInfo method, boolean useSingluarMapValues, TypeName mapValueType) { List compTypeNames = method.typeName().typeArguments(); diff --git a/common/http/src/main/java/io/helidon/common/http/ClientResponseHeaders.java b/common/http/src/main/java/io/helidon/common/http/ClientResponseHeaders.java index 7d9ffbace50..bdc6b99a58e 100644 --- a/common/http/src/main/java/io/helidon/common/http/ClientResponseHeaders.java +++ b/common/http/src/main/java/io/helidon/common/http/ClientResponseHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import io.helidon.common.http.Http.DateTime; import io.helidon.common.http.Http.HeaderValue; +import io.helidon.common.media.type.ParserMode; import static io.helidon.common.http.Http.Header.ACCEPT_PATCH; import static io.helidon.common.http.Http.Header.EXPIRES; @@ -36,12 +37,24 @@ public interface ClientResponseHeaders extends Headers { /** * Create a new instance from headers parsed from client response. + * Strict media type parsing mode is used for {@code Content-Type} header. * * @param responseHeaders client response headers * @return immutable instance of client response HTTP headers */ static ClientResponseHeaders create(Headers responseHeaders) { - return new ClientResponseHeadersImpl(responseHeaders); + return new ClientResponseHeadersImpl(responseHeaders, ParserMode.STRICT); + } + + /** + * Create a new instance from headers parsed from client response. + * + * @param responseHeaders client response headers + * @param parserMode media type parsing mode + * @return immutable instance of client response HTTP headers + */ + static ClientResponseHeaders create(Headers responseHeaders, ParserMode parserMode) { + return new ClientResponseHeadersImpl(responseHeaders, parserMode); } /** diff --git a/common/http/src/main/java/io/helidon/common/http/ClientResponseHeadersImpl.java b/common/http/src/main/java/io/helidon/common/http/ClientResponseHeadersImpl.java index 8197bb5e3b4..82e1a882bdc 100644 --- a/common/http/src/main/java/io/helidon/common/http/ClientResponseHeadersImpl.java +++ b/common/http/src/main/java/io/helidon/common/http/ClientResponseHeadersImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,18 @@ import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.function.Supplier; +import io.helidon.common.media.type.ParserMode; + class ClientResponseHeadersImpl implements ClientResponseHeaders { private final Headers headers; + private final ParserMode parserMode; - ClientResponseHeadersImpl(Headers headers) { + ClientResponseHeadersImpl(Headers headers, ParserMode parserMode) { this.headers = headers; + this.parserMode = parserMode; } @Override @@ -47,6 +52,16 @@ public Http.HeaderValue get(Http.HeaderName name) { return headers.get(name); } + @Override + public Optional contentType() { + if (parserMode == ParserMode.RELAXED) { + return contains(HeaderEnum.CONTENT_TYPE) + ? Optional.of(HttpMediaType.create(get(HeaderEnum.CONTENT_TYPE).value(), parserMode)) + : Optional.empty(); + } + return headers.contentType(); + } + @Override public int size() { return headers.size(); diff --git a/common/http/src/main/java/io/helidon/common/http/DirectHandler.java b/common/http/src/main/java/io/helidon/common/http/DirectHandler.java index 5d8017de0fe..c0a4f7d423c 100644 --- a/common/http/src/main/java/io/helidon/common/http/DirectHandler.java +++ b/common/http/src/main/java/io/helidon/common/http/DirectHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package io.helidon.common.http; +import java.lang.System.Logger.Level; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; @@ -29,11 +30,11 @@ */ @FunctionalInterface public interface DirectHandler { + /** * Default handler will HTML encode the message (if any), * use the default status code for the event type, and copy all headers configured. * - * * @return default direct handler */ static DirectHandler defaultHandler() { @@ -45,6 +46,7 @@ static DirectHandler defaultHandler() { *

* This method should be used to return custom status, header and possible entity. * If there is a need to handle more details, please redirect the client to a proper endpoint to handle them. + * This method shall not send an unsafe message back as an entity to avoid potential data leaks. * * @param request request as received with as much known information as possible * @param eventType type of the event @@ -58,6 +60,41 @@ default TransportResponse handle(TransportRequest request, Http.Status defaultStatus, ServerResponseHeaders responseHeaders, Throwable thrown) { + return handle(request, eventType, defaultStatus, responseHeaders, thrown, null); + } + + /** + * Handler of responses that bypass router. + *

+ * This method should be used to return custom status, header and possible entity. + * If there is a need to handle more details, please redirect the client to a proper endpoint to handle them. + * This method shall not send an unsafe message back as an entity to avoid potential data leaks. + * + * @param request request as received with as much known information as possible + * @param eventType type of the event + * @param defaultStatus default status expected to be returned + * @param responseHeaders headers to be added to response + * @param thrown throwable caught as part of processing with possible additional details about the reason of failure + * @param logger Possibly null logger to use for unsafe messages + * @return response to use to return to original request + */ + default TransportResponse handle(TransportRequest request, + EventType eventType, + Http.Status defaultStatus, + ServerResponseHeaders responseHeaders, + Throwable thrown, + System.Logger logger) { + if (thrown instanceof RequestException re) { + if (re.safeMessage()) { + return handle(request, eventType, defaultStatus, responseHeaders, thrown.getMessage()); + } else { + if (logger != null) { + logger.log(Level.ERROR, thrown); + } + return handle(request, eventType, defaultStatus, responseHeaders, + "Bad request, see server log for more information"); + } + } return handle(request, eventType, defaultStatus, responseHeaders, thrown.getMessage()); } diff --git a/common/http/src/main/java/io/helidon/common/http/Http.java b/common/http/src/main/java/io/helidon/common/http/Http.java index 1d60168cd5f..ed67d836b97 100644 --- a/common/http/src/main/java/io/helidon/common/http/Http.java +++ b/common/http/src/main/java/io/helidon/common/http/Http.java @@ -429,6 +429,11 @@ public static class Status { * HTTP/1.1 documentation. */ public static final Status TEMPORARY_REDIRECT_307 = new Status(307, "Temporary Redirect", true); + /** + * 308 Permanent Redirect, see + * HTTP Status Code 308 documentation. + */ + public static final Status PERMANENT_REDIRECT_308 = new Status(308, "Permanent Redirect", true); /** * 400 Bad Request, see * HTTP/1.1 documentation. diff --git a/common/http/src/main/java/io/helidon/common/http/Http1HeadersParser.java b/common/http/src/main/java/io/helidon/common/http/Http1HeadersParser.java index b5e29d7383e..970147e9d60 100644 --- a/common/http/src/main/java/io/helidon/common/http/Http1HeadersParser.java +++ b/common/http/src/main/java/io/helidon/common/http/Http1HeadersParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/common/http/src/main/java/io/helidon/common/http/HttpMediaType.java b/common/http/src/main/java/io/helidon/common/http/HttpMediaType.java index d7dfadaf817..b846ebb57a5 100644 --- a/common/http/src/main/java/io/helidon/common/http/HttpMediaType.java +++ b/common/http/src/main/java/io/helidon/common/http/HttpMediaType.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; +import io.helidon.common.media.type.ParserMode; /** * Media type used in HTTP headers, in addition to the media type definition, these may contain additional @@ -106,12 +107,24 @@ static HttpMediaType create(MediaType mediaType) { /** * Parse media type from the provided string. + * Strict media type parsing mode is used. * * @param mediaTypeString media type string * @return HTTP media type parsed from the string */ static HttpMediaType create(String mediaTypeString) { - return Builder.parse(mediaTypeString); + return Builder.parse(mediaTypeString, ParserMode.STRICT); + } + + /** + * Parse media type from the provided string. + * + * @param mediaTypeString media type string + * @param parserMode media type parsing mode + * @return HTTP media type parsed from the string + */ + static HttpMediaType create(String mediaTypeString, ParserMode parserMode) { + return Builder.parse(mediaTypeString, parserMode); } /** @@ -310,7 +323,7 @@ MediaType mediaType() { return mediaType; } - private static HttpMediaType parse(String mediaTypeString) { + private static HttpMediaType parse(String mediaTypeString, ParserMode parserMode) { // text/plain; charset=UTF-8 Builder b = builder(); @@ -337,7 +350,7 @@ private static HttpMediaType parse(String mediaTypeString) { } } } else { - b.mediaType(MediaTypes.create(mediaTypeString)); + b.mediaType(MediaTypes.create(mediaTypeString, parserMode)); } return b.build(); } diff --git a/common/http/src/main/java/io/helidon/common/http/RequestException.java b/common/http/src/main/java/io/helidon/common/http/RequestException.java index 737a96a2425..0f2e1b4fa37 100644 --- a/common/http/src/main/java/io/helidon/common/http/RequestException.java +++ b/common/http/src/main/java/io/helidon/common/http/RequestException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ public class RequestException extends RuntimeException { private final DirectHandler.TransportRequest transportRequest; private final boolean keepAlive; private final ServerResponseHeaders responseHeaders; + private final boolean safeMessage; /** * A new exception with a predefined status, even type. @@ -41,6 +42,7 @@ protected RequestException(Builder builder) { this.transportRequest = builder.request; this.keepAlive = builder.keepAlive; this.responseHeaders = builder.responseHeaders; + this.safeMessage = builder.safeMessage; } /** @@ -97,6 +99,16 @@ public ServerResponseHeaders responseHeaders() { return responseHeaders; } + /** + * Safe message flag used to control which messages can be sent as + * part of a response and which should only be logged by the server. + * + * @return safe message flag + */ + public boolean safeMessage() { + return safeMessage; + } + /** * Fluent API builder for {@link RequestException}. */ @@ -108,6 +120,7 @@ public static class Builder implements io.helidon.common.Builder { + HttpMediaType.create("text"); + }, + "Cannot parse media type: text"); + } + + // Calling create method with "text" argument shall return "text/plain" in relaxed mode. + @Test + void parseInvalidTextInRelaxedMode() { + HttpMediaType type = HttpMediaType.create("text", ParserMode.RELAXED); + assertThat(type.text(), is("text/plain")); + } + } diff --git a/common/media-type/src/main/java/io/helidon/common/media/type/MediaTypeImpl.java b/common/media-type/src/main/java/io/helidon/common/media/type/MediaTypeImpl.java index 589beaa456f..311c49b3553 100644 --- a/common/media-type/src/main/java/io/helidon/common/media/type/MediaTypeImpl.java +++ b/common/media-type/src/main/java/io/helidon/common/media/type/MediaTypeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,25 @@ package io.helidon.common.media.type; +import java.util.Optional; + record MediaTypeImpl(String type, String subtype, String text) implements MediaType { - static MediaType parse(String fullType) { + + private static final System.Logger LOGGER = System.getLogger(MediaTypeImpl.class.getName()); + + static MediaType parse(String fullType, ParserMode parserMode) { int slashIndex = fullType.indexOf('/'); if (slashIndex < 1) { + if (parserMode == ParserMode.RELAXED) { + Optional maybeRelaxedType = ParserMode.findRelaxedMediaType(fullType); + if (maybeRelaxedType.isPresent()) { + LOGGER.log(System.Logger.Level.DEBUG, + () -> String.format("Invalid media type value \"%s\" replaced with \"%s\"", + fullType, + maybeRelaxedType.get().text())); + return maybeRelaxedType.get(); + } + } throw new IllegalArgumentException("Cannot parse media type: " + fullType); } return new MediaTypeImpl(fullType.substring(0, slashIndex), diff --git a/common/media-type/src/main/java/io/helidon/common/media/type/MediaTypes.java b/common/media-type/src/main/java/io/helidon/common/media/type/MediaTypes.java index adcb2f37d0b..66fe780fdb1 100644 --- a/common/media-type/src/main/java/io/helidon/common/media/type/MediaTypes.java +++ b/common/media-type/src/main/java/io/helidon/common/media/type/MediaTypes.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -151,13 +151,26 @@ public static MediaType create(String type, String subtype) { /** * Create a new media type from the full media type string. + * Strict media type parsing mode is used. * * @param fullType media type string, such as {@code application/json} * @return media type for the string */ public static MediaType create(String fullType) { MediaTypeEnum types = MediaTypeEnum.find(fullType); - return types == null ? MediaTypeImpl.parse(fullType) : types; + return types == null ? MediaTypeImpl.parse(fullType, ParserMode.STRICT) : types; + } + + /** + * Create a new media type from the full media type string. + * + * @param fullType media type string, such as {@code application/json} + * @param parserMode media type parsing mode + * @return media type for the string + */ + public static MediaType create(String fullType, ParserMode parserMode) { + MediaTypeEnum types = MediaTypeEnum.find(fullType); + return types == null ? MediaTypeImpl.parse(fullType, parserMode) : types; } /** diff --git a/common/media-type/src/main/java/io/helidon/common/media/type/ParserMode.java b/common/media-type/src/main/java/io/helidon/common/media/type/ParserMode.java new file mode 100644 index 00000000000..8f3dfcdd4cc --- /dev/null +++ b/common/media-type/src/main/java/io/helidon/common/media/type/ParserMode.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.media.type; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * Media type parsing mode. + */ +public enum ParserMode { + + /** + * Strict mode (default). + * Media type must match known name. + */ + STRICT, + /** + * Relaxed mode. + * Apply additional rules to identify unknown media types. + */ + RELAXED; + + // Relaxed media types mapping + private static final Map RELAXED_TYPES = Map.of( + "text", MediaTypes.TEXT_PLAIN // text -> text/plain + ); + + /** + * Find relaxed media type mapping for provided value. + * + * @param value source media type value + * @return mapped media type value or {@code Optional.empty()} + * when no mapping for given value exists + */ + static Optional findRelaxedMediaType(String value) { + Objects.requireNonNull(value); + MediaType relaxedValue = RELAXED_TYPES.get(value); + return relaxedValue != null ? Optional.of(relaxedValue) : Optional.empty(); + } + +} diff --git a/common/types/src/main/java/io/helidon/common/types/AnnotationAndValueDefault.java b/common/types/src/main/java/io/helidon/common/types/AnnotationAndValueDefault.java index cc772071ac5..059655480fd 100644 --- a/common/types/src/main/java/io/helidon/common/types/AnnotationAndValueDefault.java +++ b/common/types/src/main/java/io/helidon/common/types/AnnotationAndValueDefault.java @@ -31,7 +31,7 @@ public class AnnotationAndValueDefault implements AnnotationAndValue, Comparable private final Map values; /** - * Ctor. + * Constructor taking the result of the fluent builder. * * @param b the builder * @see #builder() @@ -230,12 +230,11 @@ public int compareTo(AnnotationAndValue other) { */ public static class Builder implements io.helidon.common.Builder { private final Map values = new LinkedHashMap<>(); - private TypeName typeName; private String value; /** - * Default ctor. + * Default fluent builder constructor. */ protected Builder() { } diff --git a/common/types/src/main/java/io/helidon/common/types/TypeInfo.java b/common/types/src/main/java/io/helidon/common/types/TypeInfo.java index ecc7406c645..93cd8017d10 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeInfo.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeInfo.java @@ -16,6 +16,8 @@ package io.helidon.common.types; +import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -28,35 +30,35 @@ public interface TypeInfo { /** * The {@code public} modifier. */ - String MODIFIER_PUBLIC = "PUBLIC"; + String MODIFIER_PUBLIC = "public"; /** * The {@code protected} modifier. */ - String MODIFIER_PROTECTED = "PROTECTED"; + String MODIFIER_PROTECTED = "protected"; /** * The {@code private} modifier. */ - String MODIFIER_PRIVATE = "PRIVATE"; + String MODIFIER_PRIVATE = "private"; /** * The {@code abstract} modifier. */ - String MODIFIER_ABSTRACT = "ABSTRACT"; + String MODIFIER_ABSTRACT = "abstract"; /** * The {@code default} modifier. */ - String MODIFIER_DEFAULT = "DEFAULT"; + String MODIFIER_DEFAULT = "default"; /** * The {@code static} modifier. */ - String MODIFIER_STATIC = "STATIC"; + String MODIFIER_STATIC = "static"; /** * The {@code sealed} modifier. */ - String MODIFIER_SEALED = "SEALED"; + String MODIFIER_SEALED = "sealed"; /** * The {@code final} modifier. */ - String MODIFIER_FINAL = "FINAL"; + String MODIFIER_FINAL = "final"; /** @@ -146,7 +148,7 @@ public interface TypeInfo { * * @return the elements that make up the type that are relevant for processing */ - List elementInfo(); + List interestingElementInfo(); /** * The elements that make up this type that are considered "other", or being skipped because they are irrelevant to @@ -154,7 +156,28 @@ public interface TypeInfo { * * @return the elements that still make up the type, but are otherwise deemed irrelevant for processing */ - List otherElementInfo(); + List otherElementInfo(); + + /** + * Combines {@link #interestingElementInfo()} and {@link #otherElementInfo()} to form all typed element info belonging to this + * instance. + * + * @return all element info + */ + default List allElementInfo() { + List interestingElementInfo = interestingElementInfo(); + List otherElementInfo = otherElementInfo(); + + if (interestingElementInfo.isEmpty()) { + return otherElementInfo; + } else if (otherElementInfo.isEmpty()) { + return interestingElementInfo; + } + + LinkedHashSet all = new LinkedHashSet<>(interestingElementInfo); + all.addAll(otherElementInfo); + return new ArrayList<>(all); + } /** * Any Map, List, Set, or method that has {@link TypeName#typeArguments()} will be analyzed and any type arguments will have diff --git a/common/types/src/main/java/io/helidon/common/types/TypeInfoDefault.java b/common/types/src/main/java/io/helidon/common/types/TypeInfoDefault.java index c3c46d4def3..10c714e6640 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeInfoDefault.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeInfoDefault.java @@ -33,8 +33,8 @@ public class TypeInfoDefault implements TypeInfo { private final TypeName typeName; private final String typeKind; private final List annotations; - private final List elementInfo; - private final List otherElementInfo; + private final List elementInfo; + private final List otherElementInfo; private final Map referencedModuleNames; private final Map> referencedTypeNamesToAnnotations; private final TypeInfo superTypeInfo; @@ -85,12 +85,12 @@ public List annotations() { } @Override - public List elementInfo() { + public List interestingElementInfo() { return elementInfo; } @Override - public List otherElementInfo() { + public List otherElementInfo() { return otherElementInfo; } @@ -131,7 +131,7 @@ public String toString() { */ protected String toStringInner() { return "typeName=" + typeName() - + ", elementInfo=" + elementInfo() + + ", elementInfo=" + allElementInfo() + ", annotations=" + annotations() + ", superTypeInfo=" + superTypeInfo() + ", modifierNames=" + modifierNames(); @@ -142,15 +142,14 @@ protected String toStringInner() { */ public static class Builder implements io.helidon.common.Builder { private final List annotations = new ArrayList<>(); - private final List elementInfo = new ArrayList<>(); - private final List otherElementInfo = new ArrayList<>(); + private final List elementInfo = new ArrayList<>(); + private final List otherElementInfo = new ArrayList<>(); private final Map referencedModuleNames = new LinkedHashMap<>(); private final Map> referencedTypeNamesToAnnotations = new LinkedHashMap<>(); private final List interfaceTypeInfo = new ArrayList<>(); private final Set modifierNames = new LinkedHashSet<>(); private TypeName typeName; private String typeKind; - private TypeInfo superTypeInfo; /** @@ -217,12 +216,12 @@ public Builder addAnnotation(AnnotationAndValue val) { } /** - * Sets the elementInfo to val. + * Sets the interestingElementInfo to val. * * @param val the value * @return this fluent builder */ - public Builder elementInfo(Collection val) { + public Builder interestingElementInfo(Collection val) { Objects.requireNonNull(val); this.elementInfo.clear(); this.elementInfo.addAll(val); @@ -230,12 +229,12 @@ public Builder elementInfo(Collection val) { } /** - * Adds a single elementInfo val. + * Adds a single interestingElementInfo val. * * @param val the value * @return this fluent builder */ - public Builder addElementInfo(TypedElementName val) { + public Builder addInterestingElementInfo(TypedElementInfo val) { Objects.requireNonNull(val); elementInfo.add(val); return identity(); @@ -247,7 +246,7 @@ public Builder addElementInfo(TypedElementName val) { * @param val the value * @return this fluent builder */ - public Builder otherElementInfo(Collection val) { + public Builder otherElementInfo(Collection val) { Objects.requireNonNull(val); this.otherElementInfo.clear(); this.otherElementInfo.addAll(val); @@ -260,7 +259,7 @@ public Builder otherElementInfo(Collection val) { * @param val the value * @return this fluent builder */ - public Builder addOtherElementInfo(TypedElementName val) { + public Builder addOtherElementInfo(TypedElementInfo val) { Objects.requireNonNull(val); otherElementInfo.add(val); return identity(); diff --git a/common/types/src/main/java/io/helidon/common/types/TypedElementName.java b/common/types/src/main/java/io/helidon/common/types/TypedElementInfo.java similarity index 91% rename from common/types/src/main/java/io/helidon/common/types/TypedElementName.java rename to common/types/src/main/java/io/helidon/common/types/TypedElementInfo.java index 298c17b4361..e78903faf8f 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypedElementName.java +++ b/common/types/src/main/java/io/helidon/common/types/TypedElementInfo.java @@ -24,7 +24,7 @@ * Provides a way to describe a {@link TypeInfo#KIND_FIELD}, {@link TypeInfo#KIND_METHOD}, or {@link TypeInfo#KIND_PARAMETER} * of a method. */ -public interface TypedElementName { +public interface TypedElementInfo { /** * The type name for the element (e.g., java.util.List). If the element is a method, then this is the return type of * the method. @@ -70,13 +70,6 @@ public interface TypedElementName { */ List elementTypeAnnotations(); - /** - * Returns the component type names describing the element. - * - * @return the component type names of the element - */ - List componentTypeNames(); - /** * Element modifiers. * @@ -99,6 +92,6 @@ public interface TypedElementName { * * @return the list of parameters belonging to this method if applicable */ - List parameterArguments(); + List parameterArguments(); } diff --git a/common/types/src/main/java/io/helidon/common/types/TypedElementNameDefault.java b/common/types/src/main/java/io/helidon/common/types/TypedElementInfoDefault.java similarity index 87% rename from common/types/src/main/java/io/helidon/common/types/TypedElementNameDefault.java rename to common/types/src/main/java/io/helidon/common/types/TypedElementInfoDefault.java index 0e69011daba..1bee6929ded 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypedElementNameDefault.java +++ b/common/types/src/main/java/io/helidon/common/types/TypedElementInfoDefault.java @@ -28,12 +28,11 @@ import static io.helidon.common.types.TypeNameDefault.create; /** - * Default implementation for {@link io.helidon.common.types.TypedElementName}. + * Default implementation for {@link TypedElementInfo}. */ @SuppressWarnings("unused") -public class TypedElementNameDefault implements TypedElementName { +public class TypedElementInfoDefault implements TypedElementInfo { private final TypeName typeName; - private final List componentTypeNames; private final String elementName; private final String elementKind; private final String defaultValue; @@ -41,7 +40,7 @@ public class TypedElementNameDefault implements TypedElementName { private final List elementTypeAnnotations; private final Set modifierNames; private final TypeName enclosingTypeName; - private final List parameters; + private final List parameters; /** * Constructor taking the fluent builder. @@ -49,9 +48,8 @@ public class TypedElementNameDefault implements TypedElementName { * @param b the builder * @see #builder() */ - protected TypedElementNameDefault(Builder b) { + protected TypedElementInfoDefault(Builder b) { this.typeName = b.typeName; - this.componentTypeNames = List.copyOf(b.componentTypeNames); this.elementName = b.elementName; this.elementKind = b.elementKind; this.defaultValue = b.defaultValue; @@ -92,11 +90,6 @@ public List elementTypeAnnotations() { return elementTypeAnnotations; } - @Override - public List componentTypeNames() { - return componentTypeNames; - } - @Override public Set modifierNames() { return modifierNames; @@ -108,7 +101,7 @@ public Optional enclosingTypeName() { } @Override - public List parameterArguments() { + public List parameterArguments() { return parameters; } @@ -119,11 +112,11 @@ public int hashCode() { @Override public boolean equals(Object another) { - if (!(another instanceof TypedElementName)) { + if (!(another instanceof TypedElementInfo)) { return false; } - TypedElementName other = (TypedElementName) another; + TypedElementInfo other = (TypedElementInfo) another; return Objects.equals(typeName(), other.typeName()) && Objects.equals(elementName(), other.elementName()) && Objects.equals(elementTypeKind(), other.elementTypeKind()) @@ -163,7 +156,7 @@ public String toDeclaration() { } /** - * Creates a builder for {@link io.helidon.common.types.TypedElementName}. + * Creates a builder for {@link TypedElementInfo}. * * @return a fluent builder */ @@ -176,11 +169,10 @@ public static Builder builder() { * The fluent builder. */ public static class Builder { - private final List componentTypeNames = new ArrayList<>(); private final List annotations = new ArrayList<>(); private final List elementTypeAnnotations = new ArrayList<>(); private final Set modifierNames = new LinkedHashSet<>(); - private final List parameters = new ArrayList<>(); + private final List parameters = new ArrayList<>(); private TypeName typeName; private String elementName; @@ -216,19 +208,6 @@ public Builder typeName(Class type) { return typeName(create(type)); } - /** - * Set the component type names. - * - * @param val the component type values - * @return this fluent builder - */ - public Builder componentTypeNames(List val) { - Objects.requireNonNull(val); - this.componentTypeNames.clear(); - this.componentTypeNames.addAll(val); - return this; - } - /** * Set the element name. * @@ -356,7 +335,7 @@ public Builder enclosingTypeName(Class val) { * @param val the parameter values * @return this fluent builder */ - public Builder parameterArguments(List val) { + public Builder parameterArguments(List val) { Objects.requireNonNull(val); this.parameters.clear(); this.parameters.addAll(val); @@ -369,7 +348,7 @@ public Builder parameterArguments(List val) { * @param val the parameter value * @return the fluent builder */ - public Builder addParameterArgument(TypedElementName val) { + public Builder addParameterArgument(TypedElementInfo val) { Objects.requireNonNull(val); this.parameters.add(val); return this; @@ -380,9 +359,9 @@ public Builder addParameterArgument(TypedElementName val) { * * @return the built instance */ - public TypedElementNameDefault build() { + public TypedElementInfoDefault build() { Objects.requireNonNull(typeName); - return new TypedElementNameDefault(this); + return new TypedElementInfoDefault(this); } } diff --git a/common/types/src/test/java/io/helidon/common/types/TypedElementNameDefaultTest.java b/common/types/src/test/java/io/helidon/common/types/TypedElementInfoDefaultTest.java similarity index 64% rename from common/types/src/test/java/io/helidon/common/types/TypedElementNameDefaultTest.java rename to common/types/src/test/java/io/helidon/common/types/TypedElementInfoDefaultTest.java index 68e7c3ec1e8..4d179297f50 100644 --- a/common/types/src/test/java/io/helidon/common/types/TypedElementNameDefaultTest.java +++ b/common/types/src/test/java/io/helidon/common/types/TypedElementInfoDefaultTest.java @@ -16,74 +16,78 @@ package io.helidon.common.types; +import java.util.List; + import org.junit.jupiter.api.Test; import static io.helidon.common.types.TypeNameDefault.create; import static io.helidon.common.types.TypeNameDefault.createFromTypeName; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.sameInstance; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; -class TypedElementNameDefaultTest { +class TypedElementInfoDefaultTest { @Test void declarations() { - assertThat(TypedElementNameDefault.builder() + assertThat(TypedElementInfoDefault.builder() .elementName("arg") .typeName(create(boolean.class)) .build().toString(), is("boolean arg")); - assertThat(TypedElementNameDefault.builder() + assertThat(TypedElementInfoDefault.builder() .elementName("arg") .typeName(create(byte.class)) .build().toString(), is("byte arg")); - assertThat(TypedElementNameDefault.builder() + assertThat(TypedElementInfoDefault.builder() .elementName("arg") .typeName(create(short.class)) .build().toString(), is("short arg")); - assertThat(TypedElementNameDefault.builder() + assertThat(TypedElementInfoDefault.builder() .elementName("arg") .typeName(create(int.class)) .build().toString(), is("int arg")); - assertThat(TypedElementNameDefault.builder() + assertThat(TypedElementInfoDefault.builder() .elementName("arg") .typeName(create(long.class)) .build().toString(), is("long arg")); - assertThat(TypedElementNameDefault.builder() + assertThat(TypedElementInfoDefault.builder() .elementName("arg") .typeName(create(char.class)) .build().toString(), is("char arg")); - assertThat(TypedElementNameDefault.builder() + assertThat(TypedElementInfoDefault.builder() .elementName("arg") .typeName(create(float.class)) .build().toString(), is("float arg")); - assertThat(TypedElementNameDefault.builder() + assertThat(TypedElementInfoDefault.builder() .elementName("arg") .typeName(create(double.class)) .build().toString(), is("double arg")); - assertThat(TypedElementNameDefault.builder() + assertThat(TypedElementInfoDefault.builder() .elementName("arg") .typeName(create(void.class)) .build().toString(), is("void arg")); - assertThat(TypedElementNameDefault.builder() + assertThat(TypedElementInfoDefault.builder() .enclosingTypeName(createFromTypeName("MyClass")) .elementName("hello") .typeName(create(void.class)) .elementKind(TypeInfo.KIND_METHOD) - .addParameterArgument(TypedElementNameDefault.builder() + .addParameterArgument(TypedElementInfoDefault.builder() .elementName("arg1") .typeName(create(String.class)) .elementKind(TypeInfo.KIND_PARAMETER) .build()) - .addParameterArgument(TypedElementNameDefault.builder() + .addParameterArgument(TypedElementInfoDefault.builder() .elementName("arg2") .typeName(create(int.class)) .elementKind(TypeInfo.KIND_PARAMETER) @@ -92,4 +96,37 @@ void declarations() { is("MyClass::void hello(java.lang.String arg1, int arg2)")); } + @Test + void allElementInfo() { + TypedElementInfo m1 = TypedElementInfoDefault.builder() + .typeName(getClass()) + .elementKind(TypeInfo.KIND_METHOD) + .elementName("m1") + .build(); + TypedElementInfo m2 = TypedElementInfoDefault.builder() + .typeName(getClass()) + .elementKind(TypeInfo.KIND_METHOD) + .elementName("m2") + .build(); + + TypeInfo typeInfo = TypeInfoDefault.builder() + .addInterestingElementInfo(m1) + .build(); + assertThat(typeInfo.allElementInfo(), + sameInstance(typeInfo.interestingElementInfo())); + + typeInfo = TypeInfoDefault.builder() + .addOtherElementInfo(m1) + .build(); + assertThat(typeInfo.allElementInfo(), + sameInstance(typeInfo.otherElementInfo())); + + typeInfo = TypeInfoDefault.builder() + .addInterestingElementInfo(m1) + .otherElementInfo(List.of(m2, m1)) + .build(); + assertThat(typeInfo.allElementInfo(), + contains(m1, m2)); + } + } diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfig.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfig.java index 09a635bdd56..4cae9f4b0ec 100644 --- a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfig.java +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package io.helidon.config.mp; +import java.util.HashMap; import java.util.Iterator; +import java.util.Map; import io.helidon.config.ConfigSources; import io.helidon.config.OverrideSources; @@ -48,29 +50,42 @@ public static io.helidon.config.Config toHelidonConfig(Config mpConfig) { return (io.helidon.config.Config) mpConfig; } - // If the mpConfig is based on an SE config (such as when we use meta configuration)pom.xml - // we must reuse that se config instance - Iterator configSources = mpConfig.getConfigSources().iterator(); - ConfigSource first = configSources.hasNext() ? configSources.next() : null; - if (!configSources.hasNext() && first instanceof MpHelidonConfigSource) { - // we only have Helidon SE config as a source - let's just use it - return ((MpHelidonConfigSource) first).unwrap(); + if (mpConfig instanceof MpConfigImpl) { + + // If the mpConfig is based on an SE config (such as when we use meta configuration)pom.xml + // we must reuse that se config instance + Iterator configSources = mpConfig.getConfigSources().iterator(); + ConfigSource first = configSources.hasNext() ? configSources.next() : null; + if (!configSources.hasNext() && first instanceof MpHelidonConfigSource) { + // we only have Helidon SE config as a source - let's just use it + return ((MpHelidonConfigSource) first).unwrap(); + } + + // we use Helidon SE config to handle object mapping (and possible other mappers on classpath) + io.helidon.config.Config mapper = io.helidon.config.Config.builder() + .sources(ConfigSources.empty()) + .overrides(OverrideSources.empty()) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .disableParserServices() + .disableFilterServices() + .disableCaching() + .disableValueResolving() + .changesExecutor(command -> { + }) + .build(); + + return new SeConfig(mapper, mpConfig); } - // we use Helidon SE config to handle object mapping (and possible other mappers on classpath) - io.helidon.config.Config mapper = io.helidon.config.Config.builder() - .sources(ConfigSources.empty()) - .overrides(OverrideSources.empty()) - .disableEnvironmentVariablesSource() - .disableSystemPropertiesSource() - .disableParserServices() - .disableFilterServices() - .disableCaching() - .disableValueResolving() - .changesExecutor(command -> { - }) - .build(); + // Generic Properties convert + Map propertyMap = new HashMap<>(); + for (ConfigSource configSource : mpConfig.getConfigSources()) { + for (String propertyName : configSource.getPropertyNames()) { + propertyMap.putIfAbsent(propertyName, configSource.getValue(propertyName)); + } + } - return new SeConfig(mapper, mpConfig); + return io.helidon.config.Config.create(ConfigSources.create(propertyMap)); } } diff --git a/dependencies/pom.xml b/dependencies/pom.xml index 5c95b762270..7ec97999fc8 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -53,8 +53,8 @@ 2.3.3 3.21.7 22.3.0 - 17.5 - 17.1 + 18.6 + 18.3 2.9.0 1.54.1 31.1-jre @@ -82,7 +82,7 @@ 3.0.0 2.1.0 4.0.0 - 2.4.3.Final + 3.1.2 3.0.2 3.0.2 1.2.5.Final @@ -111,7 +111,7 @@ 4.0 2.1 4.0 - 3.0 + 3.1 3.0 3.0 3.0 @@ -132,12 +132,10 @@ 19.3.0.0 3.14.9 1.17.5 - 1.22.0 1.22.0-alpha 1.22.0-alpha 1.22.0-alpha 1.22.0 - 1.22.0-alpha 0.33.0 0.2.1 0.1.8 @@ -146,7 +144,7 @@ 42.4.3 0.9.0 2.0.0 - 2.1.16 + 3.3.4 2.0 1.4.2 2.0.4 @@ -259,26 +257,11 @@ opentelemetry-instrumentation-annotations ${version.lib.opentelemetry} - - io.opentelemetry - opentelemetry-context - ${version.lib.opentelemetry} - - - io.opentelemetry - opentelemetry-exporter-otlp - ${version.lib.opentelemetry} - io.opentelemetry.instrumentation opentelemetry-instrumentation-api ${version.lib.opentelemetry} - - io.opentelemetry - opentelemetry-sdk - ${version.lib.opentelemetry} - io.opentelemetry opentelemetry-sdk-extension-autoconfigure @@ -776,7 +759,7 @@ ${version.lib.microprofile-lra-api} - org.jboss + io.smallrye jandex ${version.lib.jandex} diff --git a/docs/config/config_reference.adoc b/docs/config/config_reference.adoc index ec42a0dfc42..50d9e921d16 100644 --- a/docs/config/config_reference.adoc +++ b/docs/config/config_reference.adoc @@ -45,8 +45,8 @@ The following section lists all configurable types in Helidon. - xref:{rootdir}/config/io_helidon_security_providers_httpauth_HttpBasicAuthProvider.adoc[HttpBasicAuthProvider (security.providers.httpauth)] - xref:{rootdir}/config/io_helidon_security_providers_httpauth_HttpDigestAuthProvider.adoc[HttpDigestAuthProvider (security.providers.httpauth)] - xref:{rootdir}/config/io_helidon_security_providers_httpsign_HttpSignProvider.adoc[HttpSignProvider (security.providers.httpsign)] -- xref:{rootdir}/config/io_helidon_security_providers_idcs_mapper_IdcsMtRoleMapperRxProvider.adoc[IdcsMtRoleMapperRxProvider (security.providers.idcs.mapper)] -- xref:{rootdir}/config/io_helidon_security_providers_idcs_mapper_IdcsRoleMapperRxProvider.adoc[IdcsRoleMapperRxProvider (security.providers.idcs.mapper)] +- xref:{rootdir}/config/io_helidon_security_providers_idcs_mapper_IdcsMtRoleMapperProvider.adoc[IdcsMtRoleMapperRxProvider (security.providers.idcs.mapper)] +- xref:{rootdir}/config/io_helidon_security_providers_idcs_mapper_IdcsRoleMapperProvider.adoc[IdcsRoleMapperRxProvider (security.providers.idcs.mapper)] - xref:{rootdir}/config/io_helidon_security_providers_httpsign_InboundClientDefinition.adoc[InboundClientDefinition (security.providers.httpsign)] - xref:{rootdir}/config/io_helidon_tracing_jaeger_JaegerTracerBuilder.adoc[JaegerTracer (tracing.jaeger)] - xref:{rootdir}/config/io_helidon_reactive_faulttolerance_Retry_JitterRetryPolicy.adoc[JitterRetryPolicy (faulttolerance.Retry)] @@ -56,15 +56,14 @@ The following section lists all configurable types in Helidon. - xref:{rootdir}/config/io_helidon_common_pki_KeyConfig_KeystoreBuilder.adoc[KeystoreBuilder (common.pki.KeyConfig)] - xref:{rootdir}/config/io_helidon_common_configurable_LruCache.adoc[LruCache (common.configurable)] - xref:{rootdir}/config/io_helidon_reactive_media_common_MediaContext.adoc[MediaContext (media.common)] -- xref:{rootdir}/config/io_helidon_microprofile_openapi_MPOpenAPISupport.adoc[MPOpenAPISupport (microprofile.openapi)] - xref:{rootdir}/config/io_helidon_metrics_api_MetricsSettings.adoc[MetricsSettings (metrics.api)] - xref:{rootdir}/config/io_helidon_metrics_serviceapi_MetricsSupport.adoc[MetricsSupport (metrics.serviceapi)] - xref:{rootdir}/config/io_helidon_integrations_micrometer_MicrometerSupport.adoc[MicrometerSupport (integrations.micrometer)] - xref:{rootdir}/config/io_helidon_config_mp_MpConfigBuilder.adoc[MpConfigBuilder (config.mp)] - xref:{rootdir}/config/io_helidon_security_providers_oidc_common_OidcConfig.adoc[OidcConfig (security.providers.oidc.common)] - xref:{rootdir}/config/io_helidon_security_providers_oidc_OidcProvider.adoc[OidcProvider (security.providers.oidc)] -- xref:{rootdir}/config/io_helidon_openapi_OpenAPISupport.adoc[OpenAPISupport (openapi)] -- xref:{rootdir}/config/io_helidon_openapi_OpenApiUi.adoc[OpenApiUi (openapi)] +// - xref:{rootdir}/config/io_helidon_openapi_OpenAPISupport.adoc[OpenAPISupport (openapi)] +// - xref:{rootdir}/config/io_helidon_openapi_OpenApiUi.adoc[OpenApiUi (openapi)] - xref:{rootdir}/config/io_helidon_security_providers_common_OutboundConfig.adoc[OutboundConfig (security.providers.common)] - xref:{rootdir}/config/io_helidon_security_providers_common_OutboundTarget.adoc[OutboundTarget (security.providers.common)] - xref:{rootdir}/config/io_helidon_common_pki_KeyConfig_PemBuilder.adoc[PemBuilder (common.pki.KeyConfig)] @@ -75,7 +74,6 @@ The following section lists all configurable types in Helidon. - xref:{rootdir}/config/io_helidon_common_configurable_Resource.adoc[Resource (common.configurable)] - xref:{rootdir}/config/io_helidon_servicecommon_rest_RestServiceSettings.adoc[RestServiceSettings (servicecommon.rest)] - xref:{rootdir}/config/io_helidon_reactive_faulttolerance_Retry.adoc[Retry (faulttolerance)] -- xref:{rootdir}/config/io_helidon_openapi_SEOpenAPISupport.adoc[SEOpenAPISupport (openapi)] - xref:{rootdir}/config/io_helidon_common_configurable_ScheduledThreadPoolSupplier.adoc[ScheduledThreadPoolSupplier (common.configurable)] - xref:{rootdir}/config/io_helidon_security_Security.adoc[Security (security)] - xref:{rootdir}/config/io_helidon_security_SecurityTime.adoc[SecurityTime (security)] diff --git a/docs/config/io_helidon_microprofile_openapi_MPOpenAPISupport.adoc b/docs/config/io_helidon_microprofile_openapi_MPOpenAPISupport.adoc deleted file mode 100644 index 0fa3c2e3c94..00000000000 --- a/docs/config/io_helidon_microprofile_openapi_MPOpenAPISupport.adoc +++ /dev/null @@ -1,70 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////// - - Copyright (c) 2022 Oracle and/or its affiliates. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -/////////////////////////////////////////////////////////////////////////////// - -ifndef::rootdir[:rootdir: {docdir}/..] -:description: Configuration of io.helidon.microprofile.openapi.MPOpenAPISupport -:keywords: helidon, config, io.helidon.microprofile.openapi.MPOpenAPISupport -:basic-table-intro: The table below lists the configuration keys that configure io.helidon.microprofile.openapi.MPOpenAPISupport -include::{rootdir}/includes/attributes.adoc[] - -= MPOpenAPISupport (microprofile.openapi) Configuration - -// tag::config[] - - -Type: link:{javadoc-base-url}/io.helidon.microprofile.openapi/io/helidon/microprofile/openapi/MPOpenAPISupport.html[io.helidon.microprofile.openapi.MPOpenAPISupport] - - -[source,text] -.Config key ----- -mp.openapi ----- - - - -== Configuration options - - - -.Optional configuration options -[cols="3,3a,2,5a"] - -|=== -|key |type |default value |description - -|`application-path-disable` |boolean |`false` |Sets whether the app path search should be disabled. -|`cors` |xref:{rootdir}/config/io_helidon_reactive_webserver_cors_CrossOriginConfig.adoc[CrossOriginConfig] |{nbsp} |Assigns the CORS settings for the OpenAPI endpoint. -|`custom-schema-registry-class` |string |{nbsp} |Sets the custom schema registry class. -|`filter` |string |{nbsp} |Sets the developer-provided OpenAPI filter class name. -|`model.reader` |string |{nbsp} |Sets the developer-provided OpenAPI model reader class name. -|`scan.classes` |string[] |{nbsp} |Specify the list of classes to scan. -|`scan.disable` |boolean |`false` |Disable annotation scanning. -|`scan.exclude.classes` |string[] |{nbsp} |Specify the list of classes to exclude from scans. -|`scan.exclude.packages` |string[] |{nbsp} |Specify the list of packages to exclude from scans. -|`scan.packages` |string[] |{nbsp} |Specify the list of packages to scan. -|`schema.*` |string |{nbsp} |Sets the schema for the indicated fully-qualified class name (represented here by '*'); value is the schema in JSON format. Repeat for multiple classes. -|`servers` |string[] |{nbsp} |Sets servers. -|`servers.operation.*` |string[] |{nbsp} |Sets alternative servers to service the indicated operation (represented here by '*'). Repeat for multiple operations. -|`servers.path.*` |string[] |{nbsp} |Sets alternative servers to service all operations at the indicated path (represented here by '*'). Repeat for multiple paths. -|`static-file` |string |`META-INF/openapi.*` |Sets the file system path of the static OpenAPI document file. Default types are `json`, `yaml`, and `yml`. -|`web-context` |string |`/openapi` |Sets the web context path for the OpenAPI endpoint. - -|=== - -// end::config[] \ No newline at end of file diff --git a/docs/config/io_helidon_security_Security.adoc b/docs/config/io_helidon_security_Security.adoc index a2fac16a86b..502160ed8bc 100644 --- a/docs/config/io_helidon_security_Security.adoc +++ b/docs/config/io_helidon_security_Security.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2022 Oracle and/or its affiliates. + Copyright (c) 2022, 2023 Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -51,10 +51,10 @@ Such as: - xref:{rootdir}/config/io_helidon_security_providers_google_login_GoogleTokenProvider.adoc[google-login (GoogleTokenProvider)] - xref:{rootdir}/config/io_helidon_security_providers_oidc_OidcProvider.adoc[oidc (OidcProvider)] - xref:{rootdir}/config/io_helidon_security_providers_httpauth_HttpDigestAuthProvider.adoc[http-digest-auth (HttpDigestAuthProvider)] - - xref:{rootdir}/config/io_helidon_security_providers_idcs_mapper_IdcsMtRoleMapperRxProvider.adoc[idcs-role-mapper (IdcsMtRoleMapperRxProvider)] + - xref:{rootdir}/config/io_helidon_security_providers_idcs_mapper_IdcsMtRoleMapperProvider.adoc[idcs-role-mapper (IdcsMtRoleMapperRxProvider)] - xref:{rootdir}/config/io_helidon_security_providers_jwt_JwtProvider.adoc[jwt (JwtProvider)] - xref:{rootdir}/config/io_helidon_security_providers_header_HeaderAtnProvider.adoc[header-atn (HeaderAtnProvider)] - - xref:{rootdir}/config/io_helidon_security_providers_idcs_mapper_IdcsRoleMapperRxProvider.adoc[idcs-role-mapper (IdcsRoleMapperRxProvider)] + - xref:{rootdir}/config/io_helidon_security_providers_idcs_mapper_IdcsRoleMapperProvider.adoc[idcs-role-mapper (IdcsRoleMapperRxProvider)] - xref:{rootdir}/config/io_helidon_security_providers_abac_AbacProvider.adoc[abac (AbacProvider)] |{nbsp} |Add a provider, works as #addProvider(io.helidon.security.spi.SecurityProvider, String), where the name is set diff --git a/docs/config/io_helidon_security_providers_idcs_mapper_IdcsMtRoleMapperRxProvider.adoc b/docs/config/io_helidon_security_providers_idcs_mapper_IdcsMtRoleMapperProvider.adoc similarity index 95% rename from docs/config/io_helidon_security_providers_idcs_mapper_IdcsMtRoleMapperRxProvider.adoc rename to docs/config/io_helidon_security_providers_idcs_mapper_IdcsMtRoleMapperProvider.adoc index d7f976dd689..aaaae6f9990 100644 --- a/docs/config/io_helidon_security_providers_idcs_mapper_IdcsMtRoleMapperRxProvider.adoc +++ b/docs/config/io_helidon_security_providers_idcs_mapper_IdcsMtRoleMapperProvider.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2022 Oracle and/or its affiliates. + Copyright (c) 2022, 2023 Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,9 +17,9 @@ /////////////////////////////////////////////////////////////////////////////// ifndef::rootdir[:rootdir: {docdir}/..] -:description: Configuration of io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperRxProvider -:keywords: helidon, config, io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperRxProvider -:basic-table-intro: The table below lists the configuration keys that configure io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperRxProvider +:description: Configuration of io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider +:keywords: helidon, config, io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider +:basic-table-intro: The table below lists the configuration keys that configure io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider include::{rootdir}/includes/attributes.adoc[] = IdcsMtRoleMapperRxProvider (security.providers.idcs.mapper) Configuration @@ -29,7 +29,7 @@ include::{rootdir}/includes/attributes.adoc[] Multitenant IDCS role mapping provider -Type: link:{javadoc-base-url}/io.helidon.security.providers.idcs.mapper/io/helidon/security/providers/idcs/mapper/IdcsMtRoleMapperRxProvider.html[io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperRxProvider] +Type: link:{javadoc-base-url}/io.helidon.security.providers.idcs.mapper/io/helidon/security/providers/idcs/mapper/IdcsMtRoleMapperRxProvider.html[io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider] [source,text] diff --git a/docs/config/io_helidon_security_providers_idcs_mapper_IdcsRoleMapperRxProvider.adoc b/docs/config/io_helidon_security_providers_idcs_mapper_IdcsRoleMapperProvider.adoc similarity index 94% rename from docs/config/io_helidon_security_providers_idcs_mapper_IdcsRoleMapperRxProvider.adoc rename to docs/config/io_helidon_security_providers_idcs_mapper_IdcsRoleMapperProvider.adoc index 6c5268f936e..bb2fa41a179 100644 --- a/docs/config/io_helidon_security_providers_idcs_mapper_IdcsRoleMapperRxProvider.adoc +++ b/docs/config/io_helidon_security_providers_idcs_mapper_IdcsRoleMapperProvider.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2022 Oracle and/or its affiliates. + Copyright (c) 2022, 2023 Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,9 +17,9 @@ /////////////////////////////////////////////////////////////////////////////// ifndef::rootdir[:rootdir: {docdir}/..] -:description: Configuration of io.helidon.security.providers.idcs.mapper.IdcsRoleMapperRxProvider -:keywords: helidon, config, io.helidon.security.providers.idcs.mapper.IdcsRoleMapperRxProvider -:basic-table-intro: The table below lists the configuration keys that configure io.helidon.security.providers.idcs.mapper.IdcsRoleMapperRxProvider +:description: Configuration of io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProvider +:keywords: helidon, config, io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProvider +:basic-table-intro: The table below lists the configuration keys that configure io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProvider include::{rootdir}/includes/attributes.adoc[] = IdcsRoleMapperRxProvider (security.providers.idcs.mapper) Configuration @@ -29,7 +29,7 @@ include::{rootdir}/includes/attributes.adoc[] IDCS role mapping provider -Type: link:{javadoc-base-url}/io.helidon.security.providers.idcs.mapper/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProvider.html[io.helidon.security.providers.idcs.mapper.IdcsRoleMapperRxProvider] +Type: link:{javadoc-base-url}/io.helidon.security.providers.idcs.mapper/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProvider.html[io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProvider] [source,text] diff --git a/docs/images/telemetry/telemetry-custom-jaeger.png b/docs/images/telemetry/telemetry-custom-jaeger.png new file mode 100644 index 00000000000..0fca937ce92 Binary files /dev/null and b/docs/images/telemetry/telemetry-custom-jaeger.png differ diff --git a/docs/images/telemetry/telemetry-general.png b/docs/images/telemetry/telemetry-general.png new file mode 100644 index 00000000000..eb7b81d1e95 Binary files /dev/null and b/docs/images/telemetry/telemetry-general.png differ diff --git a/docs/images/telemetry/telemetry-greeting-jaeger.png b/docs/images/telemetry/telemetry-greeting-jaeger.png new file mode 100644 index 00000000000..42c6d4fb2f2 Binary files /dev/null and b/docs/images/telemetry/telemetry-greeting-jaeger.png differ diff --git a/docs/images/telemetry/telemetry-outbound-jaeger.png b/docs/images/telemetry/telemetry-outbound-jaeger.png new file mode 100644 index 00000000000..55785161c93 Binary files /dev/null and b/docs/images/telemetry/telemetry-outbound-jaeger.png differ diff --git a/docs/includes/attributes.adoc b/docs/includes/attributes.adoc index 1684198e8dd..2b801136cf5 100644 --- a/docs/includes/attributes.adoc +++ b/docs/includes/attributes.adoc @@ -75,7 +75,7 @@ endif::[] :version-lib-oracle-jdbc: 21 :version-lib-oracle-ucp: {version-lib-oracle-jdbc} :version-plugin-jib: 0.10.1 -:version-plugin-jandex: 1.0.6 +:version-plugin-jandex: 3.1.2 :version-lib-micrometer: 1.6.6 :version-lib-smallrye-open-api: 2.1.16 @@ -93,7 +93,7 @@ endif::[] :microprofile-open-api-base-url: {microprofile-base-url}/microprofile-open-api-{version-lib-microprofile-openapi-api} :microprofile-open-api-spec-url: {microprofile-open-api-base-url}/microprofile-openapi-spec-{version-lib-microprofile-openapi-api}.html :microprofile-open-api-javadoc-base-url: {microprofile-open-api-base-url}/apidocs -:microprofile-open-api-javadoc-url: {microprofile-open-api-javadoc-base-url}/apidocs/org/eclipse/microprofile/openapi/ +:microprofile-open-api-javadoc-url: {microprofile-open-api-javadoc-base-url}/org/eclipse/microprofile/openapi :microprofile-lra-base-url: {microprofile-base-url}/microprofile-lra-{version-lib-microprofile-lra-api} :microprofile-lra-spec-url: {microprofile-lra-base-url}/microprofile-lra-spec-{version-lib-microprofile-lra-api}.html diff --git a/docs/includes/openapi.adoc b/docs/includes/openapi.adoc deleted file mode 100644 index e0a7952378f..00000000000 --- a/docs/includes/openapi.adoc +++ /dev/null @@ -1,210 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////// - - Copyright (c) 2022 Oracle and/or its affiliates. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -/////////////////////////////////////////////////////////////////////////////// - -ifndef::rootdir[:rootdir: {docdir}/..] - -// tag::overview[] -The link:{openapi-spec-url}[OpenAPI specification] defines a standard way to express the interface exposed by a REST service. - -The link:{microprofile-open-api-spec-url}[MicroProfile OpenAPI spec] explains how MicroProfile embraces OpenAPI, adding annotations, configuration, and a service provider interface (SPI). - -ifdef::mp-flavor[Helidon {flavor-uc} implements the MicroProfile OpenAPI specification.] -ifdef::se-flavor[OpenAPI support in Helidon {flavor-uc} draws its inspiration from MicroProfile OpenAPI but does not implement the spec because Helidon {flavor-uc} does not support annotations.] - -The OpenAPI support in Helidon {flavor-uc} performs two main tasks: - -* Build an in-memory model of the REST API your service implements. -* Expose the model in text format (typically YAML) via the `/openapi` endpoint. - -To construct the model, Helidon gathers information about the service API from whichever of these sources are present in the application: - -* a _model reader_ -+ -The SPI defines an interface you can implement in your application for programmatically providing part or all of the model; -* a static OpenAPI document file packaged as part of your service; -ifdef::mp-flavor[] -* OpenAPI annotations; -endif::[] -* a _filter_ class -+ -The SPI defines an interface you can implement in your application which can mask parts of the model. - - -// end::overview[] - -// tag::furnish-openapi-info[] - -==== Furnish OpenAPI information about your endpoints -// It's a bit odd to intermix the SE and MP content in this common file this way. -// But I tried having a level 3 section in the SE file include a sequence of -// level 4 sections from here, and that led to errors with headers being out of sequence. -// With the entire level 3 section here and conditional text for SE and MP, AsciiDoctor is happy. -ifdef::se-flavor[] -OpenAPI support in Helidon SE largely follows the link:{microprofile-open-api-spec-url}[MicroProfile OpenAPI spec]. -But because Helidon SE does not process annotations, your application supplies data for the OpenAPI model in the other ways listed earlier. -endif::[] - -ifdef::mp-flavor[] -Helidon MP OpenAPI combines information from all of the following sources as it -builds its in-memory model of your application's API. It constructs the OpenAPI -document from this internal model. Your application can use one or more of these -techniques. - -===== Annotate the endpoints in your app -You can add MicroProfile OpenAPI annotations to the endpoints in your source code. -These annotations allow the Helidon MP OpenAPI runtime to discover the endpoints -and information about them via CDI at app start-up. - -Here is one of the endpoints, annotated for OpenAPI, from the example mentioned earlier: - -[source,java] ----- -@GET -@Operation(summary = "Returns a generic greeting", // <1> - description = "Greets the user generically") -@APIResponse(description = "Simple JSON containing the greeting", // <2> - content = @Content(mediaType = "application/json", - schema = @Schema(implementation = GreetingMessage.class))) -@Produces(MediaType.APPLICATION_JSON) -public JsonObject getDefaultMessage() {...} ----- -<1> `@Operation` gives information about this endpoint. -<2> `@APIResponse` describes the HTTP response and declares its media type and contents. - -You can also define any request parameters the endpoint expects, although this -endpoint uses none. - -This excerpt shows only a few annotations for illustration. The -link:{helidon-github-tree-url}/examples/microprofile/openapi-basic[Helidon MP OpenAPI example] illustrates more, -and the link:{microprofile-open-api-spec-url}[MicroProfile OpenAPI spec] describes them all. - -===== Provide a static OpenAPI file -Add a static file at `META-INF/openapi.yml`, `META-INF/openapi.yaml`, -or `META-INF/openapi.json`. Tools such as Swagger let you describe your app's API -and they then generate an OpenAPI document file which you can include in your application -so OpenAPI can use it. - -===== Write and configure a model reader class -Write a Java class that implements the OpenAPI -link:{microprofile-open-api-javadoc-url}/OASModelReader.html[`org.eclipse.microprofile.openapi.OASModelReader`] interface. Your -model reader code programmatically adds elements to the internal model that OpenAPI -builds. - -endif::[] - -===== Provide a static OpenAPI file -Add a static file at `META-INF/openapi.yml`, `META-INF/openapi.yaml`, -or `META-INF/openapi.json`. Tools such as Swagger let you describe your app's API -and they then generate an OpenAPI document file which you can include in your application -so OpenAPI can use it. - -===== Write and configure a model reader class -Write a Java class that implements the OpenAPI -link:{microprofile-open-api-javadoc-url}/OASModelReader.html[`org.eclipse.microprofile.openapi.OASModelReader`] interface. Your -model reader code programmatically adds elements to the internal model that OpenAPI -builds. - -Change your application's MP configuration to set `mp.openapi.model.reader` as the -fully-qualified class name of your class. - -===== Write and configure a filter class -Write a Java class that implements the OpenAPI -link:{microprofile-open-api-javadoc-url}/OASFilter.html[`org.eclipse.microprofile.openapi.OASFilter`] interface. -As OpenAPI composes its internal model, it invokes your filter with each -model element _before_ adding the element to the model. Your filter can -accept the element as-is, modify it, or suppress it. - -Change your application's configuration to set `mp.openapi.filter` as the full-qualified -class name of your class. - -// end::furnish-openapi-info[] - -// tag::usage-access-endpoint[] -=== Accessing the REST Endpoint -Once you add the {flavor-uc} OpenAPI dependency to your -ifdef::mp-flavor[project,] -ifdef::se-flavor[project and add code to create the `OpenAPISupport` object to your routing,] -your application will automatically respond to the built-in endpoint -- -`/openapi` -- and it will return the OpenAPI document describing the endpoints -in your application. - -By default, per the MicroProfile OpenAPI spec, the default format of the OpenAPI document is YAML. -There is not yet an adopted IANA YAML media type, but a proposed one specifically -for OpenAPI documents that has some support is `application/vnd.oai.openapi`. -That is what Helidon returns, by default. - -In addition, a client can specify the HTTP header `Accept` as either `application/vnd.oai.openapi+json` or -`application/json` to request JSON. Alternatively, the client can pass the query parameter `format` as either `JSON` -or `YAML` to receive `application/json` or `application/vnd.oai.openapi` (YAML) output, respectively. -// end::usage-access-endpoint[] - -// tag::api[] -ifdef::mp-flavor[] -The link:{microprofile-open-api-spec-url}[MicroProfile OpenAPI specification] gives a listing and brief examples of the annotations you can add to your code to convey OpenAPI information. -endif::[] - -The link:{microprofile-open-api-javadoc-base-url}[MicroProfile OpenAPI JavaDocs] give full details of the -ifdef::mp-flavor[annotations and the other] -classes and interfaces you can use in your code. -ifdef::se-flavor[] -Remember that, although the JavaDocs describe annotations, Helidon {flavor-uc} does not support them. -endif::[] - -// end::api[] - - -// tag::additional-building-jandex[] - -=== Building the Jandex index - -A Jandex index stores information about the classes and methods in your app and -what annotations they have. It allows CDI to process annotations faster during your -application's start-up. - -Add the link:https://github.com/wildfly/jandex-maven-plugin[Jandex maven plug-in] to the `` -section of your `pom.xml`: - -[source,xml,subs="attributes+"] ----- - - org.jboss.jandex - jandex-maven-plugin - {jandex-plugin-version} - - - make-index - - jandex - - - - ----- -When you build your app `maven` should include the index `META-INF/jandex.idx` in -the JAR. - -[NOTE] -==== -If you _do not_ modify your build to create -the index then the Helidon MP OpenAPI runtime automatically creates one in memory during -app start-up. This slows down your app start-up and, depending on how CDI is -configured, might inadvertently miss information. - -We _strongly recommend_ using the Jandex plug-in to build the index into your app. -==== -// end::additional-building-jandex[] \ No newline at end of file diff --git a/docs/includes/openapi/openapi.adoc b/docs/includes/openapi/openapi.adoc index 3d704d476bd..ea3e628d03f 100644 --- a/docs/includes/openapi/openapi.adoc +++ b/docs/includes/openapi/openapi.adoc @@ -185,13 +185,13 @@ A Jandex index stores information about the classes and methods in your app and what annotations they have. It allows CDI to process annotations faster during your application's start-up. -Add the link:https://github.com/wildfly/jandex-maven-plugin[Jandex maven plug-in] to the `` +Add the link:https://github.com/smallrye/jandex/maven-plugin[Jandex maven plug-in] to the `` section of your `pom.xml`: [source,xml,subs="attributes+"] ---- - org.jboss.jandex + io.smallrye jandex-maven-plugin {jandex-plugin-version} diff --git a/docs/includes/security/providers/idcs-role-mapper.adoc b/docs/includes/security/providers/idcs-role-mapper.adoc index 463689e6d4e..6785a93866f 100644 --- a/docs/includes/security/providers/idcs-role-mapper.adoc +++ b/docs/includes/security/providers/idcs-role-mapper.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2020, 2022 Oracle and/or its affiliates. + Copyright (c) 2020, 2023 Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -38,11 +38,11 @@ A role mapper to retrieve roles from Oracle IDCS. ==== Single-tenant IDCS Role Mapper -include::{rootdir}/config/io_helidon_security_providers_idcs_mapper_IdcsRoleMapperRxProvider.adoc[leveloffset=+2,tag=config] +include::{rootdir}/config/io_helidon_security_providers_idcs_mapper_IdcsRoleMapperProvider.adoc[leveloffset=+2,tag=config] ==== Multi-tenant IDCS Role Mapper -include::{rootdir}/config/io_helidon_security_providers_idcs_mapper_IdcsMtRoleMapperRxProvider.adoc[leveloffset=+2,tag=config] +include::{rootdir}/config/io_helidon_security_providers_idcs_mapper_IdcsMtRoleMapperProvider.adoc[leveloffset=+2,tag=config] ==== Example code diff --git a/docs/mp/graphql.adoc b/docs/mp/graphql.adoc index 45a8ecd04a5..4c97223a0d4 100644 --- a/docs/mp/graphql.adoc +++ b/docs/mp/graphql.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2019, 2022 Oracle and/or its affiliates. + Copyright (c) 2019, 2023 Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -147,7 +147,7 @@ using the `jandex-maven-plugin` for all API and POJO classes. .Generate Jandex index ---- - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/docs/mp/guides/mp-tutorial.adoc b/docs/mp/guides/mp-tutorial.adoc index 62abc0557ca..1d7b2fbf28d 100644 --- a/docs/mp/guides/mp-tutorial.adoc +++ b/docs/mp/guides/mp-tutorial.adoc @@ -84,7 +84,7 @@ Create a new Maven POM file (called `pom.xml`) and add the following - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/docs/mp/introduction/microprofile.adoc b/docs/mp/introduction/microprofile.adoc index fabd23e2c88..cca194b1f8d 100644 --- a/docs/mp/introduction/microprofile.adoc +++ b/docs/mp/introduction/microprofile.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2019, 2020 Oracle and/or its affiliates. + Copyright (c) 2019, 2023 Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -132,9 +132,9 @@ To use Jandex, configure a Maven plugin that adds the index to your .jandex dependency ---- - org.jboss + io.smallrye jandex - 2.0.4.Final + {version.plugin.jandex} ---- @@ -144,9 +144,9 @@ To use Jandex, configure a Maven plugin that adds the index to your - org.jboss.jandex + io.smallrye jandex-maven-plugin - 1.0.5 + 3.1.2 make-index diff --git a/docs/mp/openapi.adoc b/docs/mp/openapi.adoc deleted file mode 100644 index 2707736fcf5..00000000000 --- a/docs/mp/openapi.adoc +++ /dev/null @@ -1,272 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////// - - Copyright (c) 2019, 2023 Oracle and/or its affiliates. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -/////////////////////////////////////////////////////////////////////////////// - -= OpenAPI -:toc: -:toc-placement: preamble -:description: Helidon MP OpenAPI Support -:keywords: helidon, mp, microprofile, openapi -:feature-name: MicroProfile OpenAPI -:microprofile-bundle: true -:rootdir: {docdir}/.. - -include::{rootdir}/includes/mp.adoc[] - -== Contents - -- <> -- <> -- <> -- <> -- <> -- <> -- <> - -== Overview - -include::{rootdir}/includes/openapi.adoc[tag=overview] - -include::{rootdir}/includes/dependencies.adoc[] - -[source,xml,subs="attributes+"] ----- - - - org.eclipse.microprofile.openapi - microprofile-openapi-api - - - io.helidon.microprofile.openapi - helidon-microprofile-openapi - runtime - - ----- -<1> Defines the MicroProfile OpenAPI annotations so you can use them in your code. -<2> Adds the Helidon MP OpenAPI runtime support. - -== Usage - -=== OpenAPI support in Helidon MP - -You can very simply add support for OpenAPI to your Helidon MP application. This -document shows what changes you need to make to your application and how to access -the OpenAPI document for your application at runtime. - -=== Changing your application - -To use OpenAPI from your Helidon MP app, in addition to adding dependencies as described above: - -1. Furnish OpenAPI information about your application's endpoints. -2. Update your application's configuration (optional). - -include::{rootdir}/includes/openapi.adoc[tag=furnish-openapi-info] - -=== Update your application configuration -Beyond the two config properties that denote the model reader and filter, Helidon -MP OpenAPI supports a number of other mandated settings. These are described in the -link:{microprofile-open-api-spec-url}#configuration[configuration section] of the MicroProfile -OpenAPI spec. - -include::{rootdir}/includes/openapi.adoc[tag=usage-access-endpoint] - -== API - -include::{rootdir}/includes/openapi.adoc[tag=api] - -== Configuration - -Helidon OpenAPI configuration supports the following settings: - -include::{rootdir}/config/io_helidon_microprofile_openapi_MPOpenAPISupport.adoc[leveloffset=+1,tag=config] - -== Examples - -Helidon MP includes a link:{helidon-github-tree-url}/examples/microprofile/openapi-basic[complete OpenAPI example] -based on the MP quick-start sample app. The rest of this section shows, step-by-step, how one might change the original QuickStart service to adopt OpenAPI. - -=== Helidon MP Basic OpenAPI Example - -This example shows a simple greeting application, similar to the one from the -Helidon MP QuickStart, enhanced with OpenAPI support. - -[source,java] ----- -@Path("/greeting") -@PUT -@Operation(summary = "Set the greeting prefix", - description = "Permits the client to set the prefix part of the greeting (\"Hello\")") //<1> -@RequestBody( //<2> - name = "greeting", - description = "Conveys the new greeting prefix to use in building greetings", - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = GreetingUpdateMessage.class), - examples = @ExampleObject( - name = "greeting", - summary = "Example greeting message to update", - value = "{\"greeting\": \"New greeting message\"}"))) -@Consumes(MediaType.APPLICATION_JSON) -@Produces(MediaType.APPLICATION_JSON) -public Response updateGreeting(JsonObject jsonObject) { - ... -} ----- -<1> With `@Operation` annotation we document the current method. -<2> With `@RequestBody` annotation we document the content produced. Internal annotations `@Content`, `@Schema` and - `@ExampleObjects` are used to give more details about the returned data. - -If we want to hide a specific path an `OASFilter` is used. - -The OASFilter interface allows application developers to receive callbacks for various key OpenAPI elements. The - interface has a default implementation for every method, which allows application developers to only override the - methods they care about. To use it, simply create an implementation of this interface and register it using the - `mp.openapi.filter configuration` key, where the value is the fully qualified name of the filter class. - -The following example filter prevents information about a given path from appearing in the OpenAPI document. - -[source, java] ----- -import org.eclipse.microprofile.openapi.models.Operation; -import org.eclipse.microprofile.openapi.models.PathItem; - -public class SimpleAPIFilter implements OASFilter { - - @Override - public PathItem filterPathItem(PathItem pathItem) { - for (Map.Entry methodOp - : pathItem.getOperations().entrySet()) { - if (SimpleAPIModelReader.DOOMED_OPERATION_ID - .equals(methodOp.getValue().getOperationId())) { - return null; - } - } - return OASFilter.super.filterPathItem(pathItem); - } -} ----- - -You can implement a model reader to provide all or part of the in-memory `OpenAPI` model programmatically. Helidon - `OpenAPI` merges the model from the model reader with models from the other sources (a static file and annotations). - -The example model reader below creates an `OpenAPI` object describing two paths. It turns out that the filter described -earlier will suppress one of the paths, but the model reader does not know or care. - -[source,java] ----- -import org.eclipse.microprofile.openapi.OASFactory; -import org.eclipse.microprofile.openapi.OASModelReader; -import org.eclipse.microprofile.openapi.models.OpenAPI; -import org.eclipse.microprofile.openapi.models.PathItem; -import org.eclipse.microprofile.openapi.models.Paths; - -/** - * Defines two paths using the OpenAPI model reader mechanism, one that should - * be suppressed by the filter class and one that should appear in the published - * OpenAPI document. - */ -public class SimpleAPIModelReader implements OASModelReader { - - /** - * Path for the example endpoint added by this model reader that should be visible. - */ - public static final String MODEL_READER_PATH = "/test/newpath"; - - /** - * Path for an endpoint that the filter should hide. - */ - public static final String DOOMED_PATH = "/test/doomed"; - - /** - * ID for an endpoint that the filter should hide. - */ - public static final String DOOMED_OPERATION_ID = "doomedPath"; - - /** - * Summary text for the endpoint. - */ - public static final String SUMMARY = "A sample test endpoint from ModelReader"; - - @Override - public OpenAPI buildModel() { - /* - * Add two path items, one of which we expect to be removed by - * the filter and a very simple one that will appear in the - * published OpenAPI document. - */ - PathItem newPathItem = OASFactory.createPathItem() - .GET(OASFactory.createOperation() - .operationId("newPath") - .summary(SUMMARY)); - PathItem doomedPathItem = OASFactory.createPathItem() - .GET(OASFactory.createOperation() - .operationId(DOOMED_OPERATION_ID) - .summary("This should become invisible")); - OpenAPI openAPI = OASFactory.createOpenAPI(); - Paths paths = OASFactory.createPaths() - .addPathItem(MODEL_READER_PATH, newPathItem) - .addPathItem(DOOMED_PATH, doomedPathItem); - openAPI.paths(paths); - - return openAPI; - } -} ----- - -Having written the filter and model reader classes, identify them by adding configuration to - `META-INF/microprofile-config.properties` as the following example shows. - -[source,properties] ----- -mp.openapi.filter=io.helidon.microprofile.examples.openapi.basic.internal.SimpleAPIFilter -mp.openapi.model.reader=io.helidon.microprofile.examples.openapi.basic.internal.SimpleAPIModelReader ----- - - -Now just build and run: - -[source,bash] ----- -mvn package -java -jar target/helidon-examples-microprofile-openapi-basic.jar ----- - -Try the endpoints: - -[source,bash] ----- -curl -X GET http://localhost:8080/greet -{"message":"Hello World!"} - -curl -X GET http://localhost:8080/openapi -[lengthy OpenAPI document] ----- - -The output describes not only then endpoints from `GreetResource` but -also one contributed by the `SimpleAPIModelReader`. - -Full example is available link:{helidon-github-tree-url}}/examples/microprofile/openapi-basic[in our official repository] - - -== Additional Information -include::{rootdir}/includes/openapi.adoc[tag=additional-building-jandex] - -== Reference - -* link:https://github.com/eclipse/microprofile-open-api[MicroProfile OpenAPI GitHub Repository] -* link:{microprofile-open-api-spec-url}[MicroProfile OpenAPI Specification] diff --git a/docs/mp/openapi/openapi.adoc b/docs/mp/openapi/openapi.adoc index d418130b491..78700f59f86 100644 --- a/docs/mp/openapi/openapi.adoc +++ b/docs/mp/openapi/openapi.adoc @@ -85,10 +85,10 @@ include::{incdir}/openapi.adoc[tag=usage-access-endpoint] include::{incdir}/openapi.adoc[tag=api] == Configuration - -Helidon OpenAPI configuration supports the following settings: - -include::{rootdir}/config/io_helidon_microprofile_openapi_MPOpenAPISupport.adoc[leveloffset=+1,tag=config] +// +// Helidon OpenAPI configuration supports the following settings: +// +//include::{rootdir}/config/io_helidon_microprofile_openapi_MPOpenAPISupport.adoc[leveloffset=+1,tag=config] == Examples diff --git a/docs/mp/telemetry.adoc b/docs/mp/telemetry.adoc new file mode 100644 index 00000000000..f4545834241 --- /dev/null +++ b/docs/mp/telemetry.adoc @@ -0,0 +1,408 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2023 Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + += Telemetry +:description: Helidon MP Telemetry Support +:feature-name: MicroProfile Telemetry +:keywords: helidon, telemetry, microprofile, micro-profile +:microprofile-bundle: true +:rootdir: {docdir}/.. + +include::{rootdir}/includes/mp.adoc[] + +== Contents + +- <> +- <> +- <> +- <> +- <> +- <> +- <> + +== Overview + +include::{rootdir}/includes/dependencies.adoc[] + +[source,xml] +---- + + io.helidon.microprofile.telemetry + helidon-microprofile-telemetry + +---- + +== Usage + +link:https://opentelemetry.io/[OpenTelemetry] comprises a collection of APIs, SDKs, integration tools, and other software components intended to facilitate the generation and control of telemetry data, including traces, metrics, and logs. In an environment where distributed tracing is enabled via OpenTelemetry (which combines OpenTracing and OpenCensus), this specification establishes the necessary behaviors for MicroProfile applications to participate seamlessly. + +MicroProfile Telemetry 1.0 allows for the exportation of the data it collects to Jaeger or Zipkin and to other systems using variety of exporter. + +In a distributed tracing system, *traces* are used to capture a series of requests and are composed of multiple *spans* that represent individual operations within those requests. Each *span* includes a name, timestamps, and metadata that provide insights into the corresponding operation. + +*Context* is included in each span to identify the specific request that it belongs to. This context information is crucial for tracking requests across various components in a distributed system, enabling developers to trace a single request as it traverses through multiple services. + +Finally, *exporters* are responsible for transmitting the collected trace data to a backend service for monitoring and visualization. This enables developers to gain a comprehensive understanding of the system's behavior and detect any issues or bottlenecks that may arise. + +image::telemetry/telemetry-general.png[General understanding of OpenTelemetry Tracing] + +There are two ways to work with Telemetry, using: + +- Automatic Instrumentation +- Manual Instrumentation + +For Automatic Instrumentation, OpenTelemetry provides a JavaAgent. The Tracing API allows for the automatic participation in distributed tracing of Jakarta RESTful Web Services (both server and client) as well as MicroProfile REST Clients, without requiring any modifications to the code. This is achieved through automatic instrumentation. + +For Manual Instrumentation there is a set of annotations and access to OpenTelemetry API. + +`@WithSpan` - By adding this annotation to a method in any Jakarta CDI aware bean, a new Span will be created and any necessary connections to the current Trace context will be established. Additionally, the `SpanAttribute` annotation can be used to mark method parameters that should be included in the Trace. + +Helidon provides full access to OpenTelemetry Tracing API: + +* `io.opentelemetry.api.OpenTelemetry` +* `io.opentelemetry.api.trace.Tracer` +* `io.opentelemetry.api.trace.Span` +* `io.opentelemetry.api.baggage.Baggage` + +Accessing and using these objects can be done as follows. For span: + +.Span sample +[source, java] +---- +@ApplicationScoped +class HelidonBean { + + @WithSpan <1> + void doSomethingWithinSpan() { + // do something here + } + + @WithSpan("name", kind = SpanKind.SERVER, @SpanAttribute(value = "arg") String arg) <2> + void complexSpan() { + // do something here + } +} +---- +<1> Simple `@WithSpan` annotation usage. +<2> Additional attributes can be set to the annotation. + +You can also inject OpenTelemetry `Tracer` using the regular `@Inject` annotation and use `SpanBuilder` to manually create, star, and stop Spans. + +.SpanBuilder usage +[source, java] +---- +@Path("/") +public class HelidonEndpoint { + + @Inject + Tracer tracer; <1> + + @GET + @Path("/span") + public Response span() { + Span span = tracer.spanBuilder("new") <2> + .setSpanKind(SpanKind.CLIENT) + .setAttribute("someAttribute", "someValue") + .startSpan(); + + span.end(); + + return Response.ok().build(); + } +} +---- +<1> Inject `Tracer`. +<2> Use `Tracer.spanBuilder` to create and start new `Span`. + +To obtain the current span, it can be injected by CDI. The current span can also be obtained using the static method `Span.current()`. + +.Inject the current span +[source, java] +---- +@Path("/") +public class HelidonEndpoint { + @Inject + Span span; <1> + + @GET + @Path("/current") + public Response currentSpan() { + return Response.ok(span.getAttribute("someAttribute")).build(); <2> + } + + + @GET + @Path("/current/static") + public Response currentSpanStatic() { + return Response.ok(Span.current().getAttribute("someAttribute")).build(); <3> + } +} +---- +<1> Inject the current span. +<2> Use the injected span. +<3> Use `Span.current()` to access the current span. + +The same functionality is available for the `Baggage` API: + +.Inject the current baggage +[source, java] +---- +@Path("/") +public class HelidonEndpoint { + @Inject + Baggage baggage; <1> + + @GET + @Path("/current") + public Response currentBaggage() { + return Response.ok(baggage.get("baggageKey")).build(); <2> + } + + + @GET + @Path("/current/static") + public Response currentBaggageStatic() { + return Response.ok(Baggage.current().get("baggageKey")).build(); <3> + } +} +---- +<1> Inject the current baggage. +<2> Use the injected baggage. +<3> Use `Baggage.current()` to access the current baggage. + + +== Configuration + +IMPORTANT: MicroProfile Telemetry is not activated by default. To activate this feature, you need to specify the configuration `otel.sdk.disabled=false` in one of the MicroProfile Config or other config sources. + +To configure OpenTelemetry, MicroProfile Config must be used, and the configuration properties outlined in the following sections must be followed: + +- link:https://github.com/open-telemetry/opentelemetry-java/tree/v1.19.0/sdk-extensions/autoconfigure[OpenTelemetry SDK Autoconfigure] (excluding properties related to Metrics and Logging) +- link:https://opentelemetry.io/docs/instrumentation/java/manual/[Manual Instrumentation] + +Please consult with the links above for all configurations properties usage. + +The property should be declared in `microprofile-config.properties` file in order to be processed correctly. + + +=== OpenTelemetry Java Agent + +The OpenTelemetry Java Agent may influence the work of MicroProfile Telemetry, on how the objects are created and configured. Helidon will do "best effort" detect the use of the agent. But, if there is a decision to run the Helidon app with the agent, a configuration property should be set: + +`otel.agent.present=true` + +This way, Helidon will explicitly get all the configuration and objects from the Agent, thus allowing correct Span hierarchy settings. + +== Examples + +This guide demonstrates how to incorporate MicroProfile Telemetry into Helidon and provides illustrations of how to view traces. Jaeger is employed in all the examples, and the Jaeger UI is used to view the traces. + +=== Set Up Jaeger + +For the examples Jaeger will be used for gathering of the tracing information. + +.Run Jaeger in a docker container. +[source, bash] +---- +docker run -d --name jaeger \ + -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ + -e COLLECTOR_OTLP_ENABLED=true \ + -p 6831:6831/udp \ + -p 6832:6832/udp \ + -p 5778:5778 \ + -p 16686:16686 \ + -p 4317:4317 \ + -p 4318:4318 \ + -p 14250:14250 \ + -p 14268:14268 \ + -p 14269:14269 \ + -p 9411:9411 \ + jaegertracing/all-in-one:1.41 +---- + +All the tracing information gathered from the examples runs is accessible from the browser in the Jaeger UI under link:http://localhost:16686/[] + +=== Enable MicroProfile Telemetry in Helidon Application + +Together with Helidon Telemetry dependency, an OpenTelemetry Exporter dependency should be added to project's pom.xml file. + +[source,xml] +---- + + io.helidon.microprofile.telemetry + helidon-microprofile-telemetry <1> + + + io.opentelemetry + opentelemetry-exporter-jaeger <2> + +---- +<1> Helidon Telemetry dependency. +<2> OpenTelemetry Jaeger exporter. + +Add these lines to `META-INF/microprofile-config.properties`: + +.MicroProfile Telemetry properties +[source,properties] +---- +otel.sdk.disabled=false <1> +otel.traces.exporter=jaeger <2> +otel.exporter.name=greeting-service <3> +---- +<1> Enable MicroProfile Telemetry. +<2> Set exporter to Jaeger. +<3> Name of our service. + +Here we enable MicroProfile Telemetry, set tracer to "jaeger" and give a name, which will be used to identify our service in the tracer. + +[NOTE] +==== + +For this example, you will use Jaeger to manage data tracing. If you prefer to use Zipkin, please set `otel.traces.exporter` property to "zipkin". For more information using about Zipkin, see link:https://zipkin.io/[]. Also a corresponding Maven dependency for the exporter should be added: +---- + + io.opentelemetry + opentelemetry-exporter-zipkin + +---- +==== + + +=== Tracing at Method Level + +To create simple services, use `@WithSpan` and `Tracer` to create span and let MicroProfile OpenTelemetry handle them. + +[source, java] +---- +@Path("/greet") +public class GreetResource { + + @GET + @WithSpan("default") <1> + public String getDefaultMessage() { + return "Hello World"; + } +} +---- +<1> Use of `@WithSpan` with name "default". + +Now let's call the Greeting endpoint: + +[source,bash] +---- +curl localhost:8080/greet +Hello World +---- + +Next, launch the Jaeger UI at link:http://localhost:16686/[]. The expected output is: + +image::telemetry/telemetry-greeting-jaeger.png[Greeting service tracing output] + +.Custom method +[source,java] +---- +@Inject +private Tracer tracer; <1> + +@GET +@Path("custom") +@Produces(MediaType.APPLICATION_JSON) +@WithSpan <2> +public JsonObject useCustomSpan(){ + Span span = tracer.spanBuilder("custom") <3> + .setSpanKind(SpanKind.INTERNAL) + .setAttribute("attribute", "value") + .startSpan(); + span.end(); <4> + + return JSON.createObjectBuilder() + .add("Custom Span", span.toString()) + .build(); +} +---- +<1> Inject Opentelemetry `Tracer`. +<2> Create Span around the method `useCustomSpan()`. +<3> Create a custom `INTERNAL` span and start it. +<4> End the custom span. + +Let us call the custom endpoint: + +[source,bash] +---- +curl localhost:8080/greeting/custom +---- + +Again you can launch the Jaeger UI at link:http://localhost:16686/[]. The expected output is: + +image::telemetry/telemetry-custom-jaeger.png[Custom span usage] + +Now let us use multiple services calls. In the example below our main service will call the `secondary` services. Each method in each service will be annotated with `@WithSpan` annotation. + +.Outbound method +[source,java] +---- +@Uri("http://localhost:8081/secondary") +private WebTarget target; <1> + +@GET +@Path("/outbound") +@WithSpan("outbound") <2> +public String outbound() { + return target.request().accept(MediaType.TEXT_PLAIN).get(String.class); <3> +} +---- +<1> Inject `WebTarget` pointing to Secondary service. +<2> Wrap method using `WithSpan`. +<3> Call the secondary service. + +The secondary service is very simple, it has only one method, which is also annotated with `@WithSpan`. + +.Secondary service +[source, java] +---- +@GET +@WithSpan <1> +public String getSecondaryMessage() { + return "Secondary"; <2> +} +---- +<1> Wrap method in a span. +<2> Return a string. + +Let us call the _Outbound_ endpoint: + +[source,bash] +---- +curl localhost:8080/greet/outbound +Secondary +---- + +The `greeting-service` call `secondary-service`. Each service will create Spans with corresponding names, and a service class hierarchy will be created. + +Launch the Jaeger UI at link:http://localhost:16686/[] to see the expected output (shown below). + +image::telemetry/telemetry-outbound-jaeger.png[Secondary service outbound call] + + +== Additional Information + + +== Reference + +* link:https://download.eclipse.org/microprofile/microprofile-telemetry-1.0/tracing/microprofile-telemetry-tracing-spec-1.0.pdf[MicroProfile Telemetry Specification] +* link:https://opentelemetry.io/docs/[OpenTelemetry Documentation] \ No newline at end of file diff --git a/docs/nima/testing.adoc b/docs/nima/testing.adoc new file mode 100644 index 00000000000..df2d2b05f9b --- /dev/null +++ b/docs/nima/testing.adoc @@ -0,0 +1,393 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2023 Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + += Helidon Níma Testing +:h1Prefix: Níma +:pagename: Helidon Níma Testing +:description: Testing in Helidon Níma +:keywords: helidon, nima, test, testing, junit +:feature-name: Helidon Níma Testing Framework +:rootdir: {docdir}/.. + +== Contents + +- <> +- <> +- <> +- <> +- <> +- <> + +== Overview + +Helidon provides built-in test support for Helidon Níma testing with JUnit 5. + +include::{rootdir}/includes/dependencies.adoc[] +[source,xml] +---- + + io.helidon.nima.testing.junit5 + helidon-nima-testing-junit5-webserver + test + +---- + +== Usage + +Helidon provides a rich set of extensions based on JUnit 5 for Helidon Níma testing. Testing can be done with automatic server start-up, configuration, and shutdown. Testing can also be done without full server start-up with `DirectClient` when no real sockets are created. + +== API + +There are two main annotations that you can use to test the Níma server. + +* `@ServerTest` is an integration test annotation that starts the server (opens ports) and provides client injection pre-configured for the server port(s). +* `@RoutingTest` is a unit test annotation that does not start the server and does not open ports but provides a direct client (with the same API as the usual network client) to test routing. + +The additional annotation `@Socket` can be used to qualify the injection of parameters into test constructors or methods, such as to obtain a client configured for the named socket. + +The following table lists the supported types of parameters for the `@SetUpRoute` annotated methods. Such methods MUST be static +and may have any name. The `@SetUpRoute` annotation has `value` with socket name (to customize the setup for a different socket). + +- Parameter type - supported class of a parameter +- Annotation - which annotations support this parameter +- Modules - which Níma extension modules support this signature + +.Parameters for the `@SetUpRoute` annotated methods. +|=== +|Parameter Type |Annotation |Modules |Notes + +|`HttpRouting.Builder` +|`@ServerTest`, `@RoutingTest` +| +| + +|`HttpRules` +|`@ServerTest`, `@RoutingTest` +| +|Same as `HttpRouting.Builder`, only routing setup + +|`Router.RouterBuilder` +|`@ServerTest`, `@RoutingTest` +| +| + +|`SocketListener.Builder` +|`@ServerTest` +| +| + +|`WebSocketRouting.Builder` +|`@ServerTest`, `@RoutingTest` +|websocket +| + +|=== + +In addition, a static method annotated with `@SetUpServer` can be defined for `@ServerTest`, which has a single parameter of `WebServer.Builder`. + +The following table lists the injectable types (through constructor or method injection). + +- Type - type that can be injected +- Socket - if checked, you can use the `@Socket` annotation to obtain a value specific to that named socket +- Annotation - which annotations support this injection +- Modules - which Níma extension modules support this injection +- Notes - additional details + +.Injectable types. +|=== +|Type |Socket? |Annotation |Modules |Notes +|`WebServer` +| +|`@ServerTest` +| +|Server instance (already started) + +|`URI` +|x +|`@ServerTest` +| +|URI pointing to a port of the webserver + +|`SocketHttpClient` +|x +|`@ServerTest` +| +|This client allows you to send anything in order to test for bad requests or other issues. + +|`Http1Client` +|x +|`@ServerTest` +| +| + +|`DirectClient` +|x +|`@RoutingTest` +| +|Implements `Http1Client` API + +|`WsClient` +|x +|`@ServerTest` +|websocket +| + +|`DirectWsClient` +|x +|`@RoutingTest` +|websocket +|Implements `WsClient` API + +|=== + + +Extensions can enhance the features for the module `helidon-nima-testing-junit5-webserver` to support additional protocols. + + +== Examples + +You can create the following test to validate that the server returns the correct response: + +.Basic Helidon Níma test framework usage. +[source, java] +---- +@ServerTest <1> +class IntegrationTest { + + private final Http1Client client; + + protected IntegrationTest(Http1Client client) { <2> + this.client = client; + } + + @SetUpRoute <3> + static void routing(HttpRouting.Builder builder) { + QuickstartMain.routing(builder); + } + + @Test <4> + void testRootRoute() { + try (Http1ClientResponse response = client.get("/greet") + .request()) { <5> + + assertThat(response.status(), is(Http.Status.OK_200)); <6> + } + } +} +---- +<1> Use `@ServerTest` to trigger the testing framework. +<2> Inject `Http1Client` for the test. +<3> SetUp routing for the test. +<4> Regular `JUnit` test method. +<5> Call the `client` to obtain server response +<6> Perform the necessary assertions. + +To trigger the framework to start and configure the Níma server, annotate the testing class with the `@ServerTest` annotation. + +In this test, the `Http1Client` client is used, which means that the framework will create, configure, and inject this object as a parameter to the constructor. + +To set up routing, a static method annotated with `@SetUpRoute` is present. The framework uses this method to inject the configured routing to the subject of testing – in the current case, the `Quickstart` application. + +As everything above is performed by the testing framework, regular unit tests can be done. After completing all tests, the testing framework will shut down the Níma server. + +=== Routing Tests + +If there is no need to set up and run a server, a `DirectClient` client can be used. It is a testing client that bypasses HTTP transport and directly invokes the router. + +.Routing test using `@RoutingTest` and `DirectClient`. +[source, java] +---- +@RoutingTest <1> +class RoutingTest { + + private final Http1Client client; + + protected RoutingTest(DirectClient client) { <2> + this.client = client; + } + + @SetUpRoute <3> + static void routing(HttpRouting.Builder builder) { + QuickstartMain.routing(builder); + } + + @Test <4> + void testRootRoute() { + try (Http1ClientResponse response = client.get("/greet") + .request()) { <5> + + JsonObject json = response.as(JsonObject.class); <6> + assertThat(json.getString("message"), is("Hello World!")); + } + } +} +---- +<1> Use `@RoutingTest` to trigger the testing framework. +<2> Inject `DirectClient` for the test. +<3> SetUp routing for the test. +<4> A regular `JUnit` test method. +<5> Call the `client` to obtain server response. +<6> Perform the necessary assertions. + +If only routing tests are required, this is a "lighter" way of testing because the framework will not configure and run the full Níma server. This way, no real ports will be opened. All the communication will be done through `DirectClient`, which makes the tests very effective. + +It is required to annotate the test class with the `@RoutingTest` annotation to trigger the server to do the configuration. Thus, it will inject the DirectClient client, which can then be used in unit tests. + +Routing is configured the same way as in full server testing using the `@SetUpRoute` annotation. + +== Additional Information + +=== WebSocket Testing + +If WebSocket testing is required, there is an additional module for it. It is necessary to include the following Maven dependency to the Project's pom file: + +[source,xml] +---- + + io.helidon.nima.testing.junit5 + helidon-nima-testing-junit5-websocket + test + +---- + + +=== WebSocket Testing Example + +The WebSocket Testing extension adds support for routing configuration and injection of WebSocket related artifacts, such as WebSockets and DirectWsClient in Helidon Níma unit tests. + +.WebSocket sample test. +[source,java] +---- +@ServerTest +class WsSocketTest { + + private static final ServerSideListener WS_LISTENER = new ServerSideListener(); + + private final WsClient wsClient; <1> + + protected WsSocketTest(WsClient wsClient) { + this.wsClient = wsClient; + } + + @SetUpRoute + static void routing(WsRouting.Builder ws) { <2> + ws.endpoint("/testWs", WS_LISTENER); + } + + @Test + void testWsEndpoint() { <3> + WS_LISTENER.reset(); + + ClientSideListener clientListener = new ClientSideListener(); + wsClient.connect("/testWs", clientListener); <4> + + assertThat(clientListener.message, is("ws")); <5> + } +} +---- +<1> Declare `WsClient` and later inject it in the constructor. +<2> Using @SetUpRoute, create WebSocket routing and assign a serverside listener. +<3> Test the WebSocket endpoint using the regular @Test annotation. +<4> Create and assign the clientside listener. +<5> Check if the received message is correct. + +[NOTE] +==== +The WebSocket `ClientSideListener` is a helper class that implements `WsListener`. `ClientSideListener` is very simple and looks as follows: + +.`ClientSideListener` helper class. +[source,java] +---- +private static class ClientSideListener implements WsListener { + private final CountDownLatch cdl = new CountDownLatch(1); + private String message; + private volatile Throwable throwable; + + @Override + public void onOpen(WsSession session) { <1> + session.send("hello", true); + } + + @Override + public void onMessage(WsSession session, String text, boolean last) { <2> + this.message = text; + session.close(WsCloseCodes.NORMAL_CLOSE, "End"); + } + + @Override + public void onClose(WsSession session, int status, String reason) { <3> + cdl.countDown(); + } + + @Override + public void onError(WsSession session, Throwable t) { <4> + this.throwable = t; + cdl.countDown(); + } +} +---- +<1> Send "Hello" when a connection is opened. +<2> Save the message when received and close the connection. +<3> Close the connection. +<4> React on an error. + +The WebSocket `ClientSideListener` is also a helper class that implements `WsListener` and is very straightforward: + +.`ServerSideListener` helper class. +[source,java] +---- +private static class ServerSideListener implements WsListener { + boolean opened; + boolean closed; + String message; + + @Override + public void onMessage(WsSession session, String text, boolean last) { <1> + message = text; + session.send("ws", true); + } + + @Override + public void onClose(WsSession session, int status, String reason) { <2> + closed = true; + } + + @Override + public void onOpen(WsSession session) { <3> + opened = true; + } + + void reset() { <4> + opened = false; + closed = false; + message = null; + } +} +---- +<1> Send "ws" on a received message. +<2> Called when the connection is called. +<3> Called on connection is opened. +<4> Used to reset the state. +==== + +The testing class should be annotated with `@RoutingTest` only if routing tests are required without real port opening. Instead of `WsClient`, use `DirectWsClient`. + + +== Reference + +* https://junit.org/junit5/docs/current/user-guide/[JUnit 5 User Guide] diff --git a/docs/se/integrations/neo4j.adoc b/docs/se/integrations/neo4j.adoc index 7faad4177c6..b2e2156b6c5 100644 --- a/docs/se/integrations/neo4j.adoc +++ b/docs/se/integrations/neo4j.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2022 Oracle and/or its affiliates. + Copyright (c) 2022, 2023 Oracle and/or its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -221,7 +221,7 @@ private static Routing createRouting(Config config) { HealthSupport health = HealthSupport.builder() .add(HealthChecks.healthChecks()) // Adds a convenient set of checks - .addReadiness(healthCheck) + .add(healthCheck) .build(); return Routing.builder() // <5> @@ -329,7 +329,7 @@ To enable health checks run the following code: Neo4jHealthCheck healthCheck = Neo4jHealthCheck.create(neo4j.driver()); HealthSupport health = HealthSupport.builder() - .addReadiness(healthCheck) + .add(healthCheck) .build(); ---- diff --git a/docs/se/openapi.adoc b/docs/se/openapi.adoc index 804a06e8637..7b2b52ae1bb 100644 --- a/docs/se/openapi.adoc +++ b/docs/se/openapi.adoc @@ -38,7 +38,7 @@ include::{rootdir}/includes/se.adoc[] == Overview -include::{rootdir}/includes/openapi.adoc[tag=overview] +include::{rootdir}/includes/openapi/openapi.adoc[tag=overview] include::{rootdir}/includes/dependencies.adoc[] @@ -62,7 +62,7 @@ the OpenAPI document for your application at runtime. Helidon SE provides the link:{openapi-javadoc-base-url}/OpenAPISupport.html[`OpenAPISupport`] class which your application uses to assemble the in-memory model and expose the `/openapi` endpoint to clients. You can create an instance either using a static `create` method or by instantiating its link:{openapi-javadoc-base-url}/OpenAPISupport.Builder.html[`Builder`]. The xref:#register_openapisupport[example below] illustrates one way to do this. -include::{rootdir}/includes/openapi.adoc[tag=furnish-openapi-info] +include::{rootdir}/includes/openapi/openapi.adoc[tag=furnish-openapi-info] ==== Add OpenAPI dependency If you implement either a model reader or a filter, add this dependency to your @@ -77,11 +77,11 @@ If you implement either a model reader or a filter, add this dependency to your ---- -include::{rootdir}/includes/openapi.adoc[tag=usage-access-endpoint] +include::{rootdir}/includes/openapi/openapi.adoc[tag=usage-access-endpoint] == API -include::{rootdir}/includes/openapi.adoc[tag=api] +include::{rootdir}/includes/openapi/openapi.adoc[tag=api] Helidon {flavor-uc} provides an API for creating and setting up the REST endpoint which serves OpenAPI documents to clients at the `/openapi` path. Use either static methods on link:{openapi-javadoc-base-url}/OpenAPISupport.html[`OpenAPISupport`] or use its link:{openapi-javadoc-base-url}/OpenAPISupport.Builder.html[`Builder`] to create an instance of `OpenAPISupport`. Then add that instance to your application's routing. The <<#register_openapisupport,example>> below shows how to do this. @@ -120,4 +120,4 @@ return Routing.builder() If you need more control over the `OpenAPISupport` instance, invoke `OpenAPISupport.builder()` to get an `OpenAPISupport.Builder` object and work with it. == Additional Information -include::{rootdir}/includes/openapi.adoc[tag=additional-building-jandex] \ No newline at end of file +include::{rootdir}/includes/openapi/openapi.adoc[tag=additional-building-jandex] \ No newline at end of file diff --git a/etc/checkstyle-suppressions.xml b/etc/checkstyle-suppressions.xml index 1f2c11c3f36..04c6792b6d4 100644 --- a/etc/checkstyle-suppressions.xml +++ b/etc/checkstyle-suppressions.xml @@ -104,6 +104,8 @@ checks=".*"/> + + + io.helidon.nima.webserver + helidon-nima-webserver + + + io.helidon.nima.observe + helidon-nima-observe-health + io.helidon.examples.grpc helidon-examples-grpc-common diff --git a/examples/grpc/basics/src/main/java/io/helidon/grpc/examples/basics/Server.java b/examples/grpc/basics/src/main/java/io/helidon/grpc/examples/basics/Server.java index a42eba1aefc..f78094cf357 100644 --- a/examples/grpc/basics/src/main/java/io/helidon/grpc/examples/basics/Server.java +++ b/examples/grpc/basics/src/main/java/io/helidon/grpc/examples/basics/Server.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,11 +23,10 @@ import io.helidon.grpc.server.GrpcRouting; import io.helidon.grpc.server.GrpcServer; import io.helidon.grpc.server.GrpcServerConfiguration; -import io.helidon.health.checks.HealthChecks; import io.helidon.logging.common.LogConfig; -import io.helidon.reactive.health.HealthSupport; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.WebServer; +import io.helidon.nima.observe.ObserveFeature; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http.HttpRouting; /** * A basic example of a Helidon gRPC server. @@ -53,7 +52,7 @@ public static void main(String[] args) { GrpcServerConfiguration serverConfig = GrpcServerConfiguration.builder(config.get("grpc")).build(); - GrpcServer grpcServer = GrpcServer.create(serverConfig, createRouting(config)); + GrpcServer grpcServer = GrpcServer.create(serverConfig, createGrpcRouting(config)); // Try to start the server. If successful, print some info and arrange to // print a message at shutdown. If unsuccessful, print the exception. @@ -68,31 +67,18 @@ public static void main(String[] args) { return null; }); - // add support for standard and gRPC health checks - HealthSupport health = HealthSupport.builder() - .add(HealthChecks.healthChecks()) - .addLiveness(grpcServer.healthChecks()) - .build(); + WebServer server = WebServer.builder() + .routing(Server::routing) + .start(); - // start web server with health endpoint - Routing routing = Routing.builder() - .register(health) - .build(); + System.out.println("WEB server is up! http://localhost:" + server.port()); + } - WebServer.create(routing, config.get("webserver")) - .start() - .thenAccept(s -> { - System.out.println("HTTP server is UP! http://localhost:" + s.port()); - s.whenShutdown().thenRun(() -> System.out.println("HTTP server is DOWN. Good bye!")); - }) - .exceptionally(t -> { - System.err.println("Startup failed: " + t.getMessage()); - t.printStackTrace(System.err); - return null; - }); + private static void routing(HttpRouting.Builder routing) { + routing.addFeature(ObserveFeature.create()); } - private static GrpcRouting createRouting(Config config) { + private static GrpcRouting createGrpcRouting(Config config) { GreetService greetService = new GreetService(config); GreetServiceJava greetServiceJava = new GreetServiceJava(config); diff --git a/examples/grpc/microprofile/basic-client/pom.xml b/examples/grpc/microprofile/basic-client/pom.xml index feb56f87a33..bf9374cd73e 100644 --- a/examples/grpc/microprofile/basic-client/pom.xml +++ b/examples/grpc/microprofile/basic-client/pom.xml @@ -51,7 +51,7 @@ helidon-microprofile-grpc-client - org.jboss + io.smallrye jandex runtime true @@ -70,7 +70,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/grpc/microprofile/basic-server-implicit/pom.xml b/examples/grpc/microprofile/basic-server-implicit/pom.xml index b2bc5b7fbd8..7a648112726 100644 --- a/examples/grpc/microprofile/basic-server-implicit/pom.xml +++ b/examples/grpc/microprofile/basic-server-implicit/pom.xml @@ -55,7 +55,7 @@ helidon-microprofile-grpc-client - org.jboss + io.smallrye jandex runtime true @@ -74,7 +74,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/grpc/microprofile/metrics/pom.xml b/examples/grpc/microprofile/metrics/pom.xml index f75a28384f0..1352fb3c4f9 100644 --- a/examples/grpc/microprofile/metrics/pom.xml +++ b/examples/grpc/microprofile/metrics/pom.xml @@ -59,7 +59,7 @@ helidon-microprofile-grpc-client - org.jboss + io.smallrye jandex runtime true @@ -78,7 +78,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/AtnProvider.java b/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/AtnProvider.java index 5c3febccd5e..fbc99b198fc 100644 --- a/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/AtnProvider.java +++ b/examples/grpc/security-abac/src/main/java/io/helidon/grpc/examples/security/abac/AtnProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,12 +44,11 @@ import io.helidon.security.Subject; import io.helidon.security.SubjectType; import io.helidon.security.spi.AuthenticationProvider; -import io.helidon.security.spi.SynchronousProvider; /** * Example authentication provider that reads annotation to create a subject. */ -public class AtnProvider extends SynchronousProvider implements AuthenticationProvider { +public class AtnProvider implements AuthenticationProvider { /** * The configuration key for this provider. @@ -63,7 +62,7 @@ private AtnProvider(Config config) { } @Override - protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { EndpointConfig endpointConfig = providerRequest.endpointConfig(); Config atnConfig = endpointConfig.config(CONFIG_KEY).orElse(null); Subject user = null; diff --git a/examples/grpc/security-outbound/src/main/java/io/helidon/grpc/examples/security/outbound/SecureServer.java b/examples/grpc/security-outbound/src/main/java/io/helidon/grpc/examples/security/outbound/SecureServer.java index b52c9131add..cd227f9aece 100644 --- a/examples/grpc/security-outbound/src/main/java/io/helidon/grpc/examples/security/outbound/SecureServer.java +++ b/examples/grpc/security-outbound/src/main/java/io/helidon/grpc/examples/security/outbound/SecureServer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -121,9 +121,9 @@ private static GrpcServer createGrpcServer(Config config, Security security) { private static WebServer createWebServer(Config config, Security security) { Routing routing = Routing.builder() - .register(WebSecurity.create(security).securityDefaults(WebSecurity.authenticate())) - .register(new RestService()) - .build(); + .register(WebSecurity.create(security).securityDefaults(WebSecurity.authenticate())) + .register(new RestService()) + .build(); WebServer webServer = WebServer.create(routing, config); diff --git a/examples/health/basics/pom.xml b/examples/health/basics/pom.xml index eba56e83721..36414b3ff33 100644 --- a/examples/health/basics/pom.xml +++ b/examples/health/basics/pom.xml @@ -39,16 +39,16 @@ - io.helidon.reactive.health - helidon-reactive-health + io.helidon.nima.webserver + helidon-nima-webserver - io.helidon.health - helidon-health-checks + io.helidon.nima.observe + helidon-nima-observe-health - io.helidon.reactive.webserver - helidon-reactive-webserver + io.helidon.health + helidon-health-checks org.junit.jupiter diff --git a/examples/health/basics/src/main/java/io/helidon/examples/health/basics/Main.java b/examples/health/basics/src/main/java/io/helidon/examples/health/basics/Main.java index f643fede6af..e8ed67ff06d 100644 --- a/examples/health/basics/src/main/java/io/helidon/examples/health/basics/Main.java +++ b/examples/health/basics/src/main/java/io/helidon/examples/health/basics/Main.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,14 @@ import java.time.Duration; -import io.helidon.health.checks.HealthChecks; -import io.helidon.reactive.health.HealthSupport; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.WebServer; - -import org.eclipse.microprofile.health.HealthCheckResponse; +import io.helidon.health.HealthCheckResponse; +import io.helidon.health.HealthCheckType; +import io.helidon.logging.common.LogConfig; +import io.helidon.nima.observe.ObserveFeature; +import io.helidon.nima.observe.health.HealthFeature; +import io.helidon.nima.observe.health.HealthObserveProvider; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http.HttpRouting; /** * Main class of health check integration example. @@ -41,33 +43,42 @@ private Main() { */ public static void main(String[] args) { serverStartTime = System.currentTimeMillis(); - HealthSupport health = HealthSupport.builder() - .add(HealthChecks.healthChecks()) - .addReadiness(() -> HealthCheckResponse.named("exampleHealthCheck") - .up() - .withData("time", System.currentTimeMillis()) - .build()) - .addStartup(() -> HealthCheckResponse.named("exampleStartCheck") - .status(isStarted()) - .withData("time", System.currentTimeMillis()) - .build()) - .build(); - Routing routing = Routing.builder() - .register(health) - .get("/hello", (req, res) -> res.send("Hello World!")) - .build(); + // load logging + LogConfig.configureRuntime(); + + WebServer server = WebServer.builder() + .routing(Main::routing) + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port()); + } - WebServer ws = WebServer.create(routing); + /** + * Set up HTTP routing. + * This method is used from tests as well. + * + * @param router HTTP routing builder + */ + static void routing(HttpRouting.Builder router) { + ObserveFeature observe = ObserveFeature.builder() + .useSystemServices(true) + .addProvider(HealthObserveProvider.create(HealthFeature.builder() + .useSystemServices(true) + .addCheck(() -> HealthCheckResponse.builder() + .status(HealthCheckResponse.Status.UP) + .detail("time", System.currentTimeMillis()) + .build(), HealthCheckType.READINESS) + .addCheck(() -> HealthCheckResponse.builder() + .status(isStarted()) + .detail("time", System.currentTimeMillis()) + .build(), HealthCheckType.STARTUP) + .build())) + .build(); - ws.start() - .thenApply(webServer -> { - String endpoint = "http://localhost:" + webServer.port(); - System.out.println("Hello World started on " + endpoint + "/hello"); - System.out.println("Health checks available on " + endpoint + "/health"); - return null; - }); + router.get("/hello", (req, res) -> res.send("Hello World!")) + .addFeature(observe); } private static boolean isStarted() { diff --git a/examples/integrations/cdi/datasource-hikaricp-h2/pom.xml b/examples/integrations/cdi/datasource-hikaricp-h2/pom.xml index b8d580908d6..d4d05758adf 100644 --- a/examples/integrations/cdi/datasource-hikaricp-h2/pom.xml +++ b/examples/integrations/cdi/datasource-hikaricp-h2/pom.xml @@ -61,7 +61,7 @@ runtime - org.jboss + io.smallrye jandex runtime true @@ -90,7 +90,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/integrations/cdi/datasource-hikaricp-mysql/pom.xml b/examples/integrations/cdi/datasource-hikaricp-mysql/pom.xml index a9b1814c5e4..c802b4e21fa 100644 --- a/examples/integrations/cdi/datasource-hikaricp-mysql/pom.xml +++ b/examples/integrations/cdi/datasource-hikaricp-mysql/pom.xml @@ -62,7 +62,7 @@ runtime - org.jboss + io.smallrye jandex runtime true @@ -91,7 +91,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/integrations/cdi/datasource-hikaricp/pom.xml b/examples/integrations/cdi/datasource-hikaricp/pom.xml index 7c84fc34e56..c2850ca4680 100644 --- a/examples/integrations/cdi/datasource-hikaricp/pom.xml +++ b/examples/integrations/cdi/datasource-hikaricp/pom.xml @@ -67,7 +67,7 @@ runtime - org.jboss + io.smallrye jandex runtime true @@ -96,7 +96,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/integrations/cdi/jedis/pom.xml b/examples/integrations/cdi/jedis/pom.xml index 16dddf17592..02fb490a043 100644 --- a/examples/integrations/cdi/jedis/pom.xml +++ b/examples/integrations/cdi/jedis/pom.xml @@ -60,7 +60,7 @@ runtime - org.jboss + io.smallrye jandex runtime true @@ -89,7 +89,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/integrations/cdi/jpa/pom.xml b/examples/integrations/cdi/jpa/pom.xml index 934d875f47a..359982a67ee 100644 --- a/examples/integrations/cdi/jpa/pom.xml +++ b/examples/integrations/cdi/jpa/pom.xml @@ -92,7 +92,7 @@ runtime - org.jboss + io.smallrye jandex runtime true @@ -152,7 +152,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/integrations/cdi/pokemons/pom.xml b/examples/integrations/cdi/pokemons/pom.xml index 9525d81bc49..ecae02349fe 100644 --- a/examples/integrations/cdi/pokemons/pom.xml +++ b/examples/integrations/cdi/pokemons/pom.xml @@ -90,7 +90,7 @@ runtime - org.jboss + io.smallrye jandex runtime true @@ -151,7 +151,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/integrations/micrometer/mp/pom.xml b/examples/integrations/micrometer/mp/pom.xml index c523c2de060..78ffa4d105e 100644 --- a/examples/integrations/micrometer/mp/pom.xml +++ b/examples/integrations/micrometer/mp/pom.xml @@ -87,7 +87,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/integrations/micronaut/data/pom.xml b/examples/integrations/micronaut/data/pom.xml index 499160e2025..d53c303aafd 100644 --- a/examples/integrations/micronaut/data/pom.xml +++ b/examples/integrations/micronaut/data/pom.xml @@ -79,7 +79,7 @@ runtime - org.jboss + io.smallrye jandex runtime true @@ -144,7 +144,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/integrations/microstream/greetings-mp/pom.xml b/examples/integrations/microstream/greetings-mp/pom.xml index 60afd4b4ecd..320cfd01226 100644 --- a/examples/integrations/microstream/greetings-mp/pom.xml +++ b/examples/integrations/microstream/greetings-mp/pom.xml @@ -70,7 +70,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/integrations/microstream/greetings-mp/src/test/java/io/helidon/examples/integrations/microstream/greetings/mp/MicrostreamExampleGreetingsMpTest.java b/examples/integrations/microstream/greetings-mp/src/test/java/io/helidon/examples/integrations/microstream/greetings/mp/MicrostreamExampleGreetingsMpTest.java index 59945fc6240..3ae71957af4 100644 --- a/examples/integrations/microstream/greetings-mp/src/test/java/io/helidon/examples/integrations/microstream/greetings/mp/MicrostreamExampleGreetingsMpTest.java +++ b/examples/integrations/microstream/greetings-mp/src/test/java/io/helidon/examples/integrations/microstream/greetings/mp/MicrostreamExampleGreetingsMpTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,8 +31,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @HelidonTest -@Disabled("3.0.0-JAKARTA") // OpenAPI -// Caused by: java.lang.NoSuchMethodError: 'java.util.List org.jboss.jandex.ClassInfo.unsortedFields()' class MicrostreamExampleGreetingsMpTest { @Inject diff --git a/examples/integrations/neo4j/README.md b/examples/integrations/neo4j/README.md new file mode 100644 index 00000000000..d35611349cd --- /dev/null +++ b/examples/integrations/neo4j/README.md @@ -0,0 +1,38 @@ +# Helidon SE integration with Neo4J example + +## Build and run + +Bring up a Neo4j instance via Docker + +```bash +docker run --publish=7474:7474 --publish=7687:7687 -e 'NEO4J_AUTH=neo4j/secret' neo4j:4.0 +``` + +Goto the Neo4j browser and play the first step of the movies graph: [`:play movies`](http://localhost:7474/browser/?cmd=play&arg=movies). + +Build and run with JDK20 +```bash +mvn package +java -jar target/helidon-examples-integration-neo4j-nima.jar +``` + +Then access the rest API like this: + +```` +curl localhost:8080/api/movies +```` + +# Health and metrics + +Neo4jSupport provides health checks and metrics reading from Neo4j. + +Enable them in the driver: +```yaml + pool: + metricsEnabled: true +``` + +```` +curl localhost:8080/observe/health +curl localhost:8080/observe/metrics +```` diff --git a/examples/integrations/neo4j/neo4j-mp/.dockerignore b/examples/integrations/neo4j/neo4j-mp/.dockerignore deleted file mode 100644 index c8b241f2215..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -target/* \ No newline at end of file diff --git a/examples/integrations/neo4j/neo4j-mp/Dockerfile b/examples/integrations/neo4j/neo4j-mp/Dockerfile deleted file mode 100644 index 8eeb50924e4..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -# -# Copyright (c) 2021 Oracle and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# 1st stage, build the app -FROM maven:3.6.3-openjdk-17-slim as build - -WORKDIR /helidon - -# Create a first layer to cache the "Maven World" in the local repository. -# Incremental docker builds will always resume after that, unless you update -# the pom -ADD pom.xml . -RUN mvn package -Dmaven.test.skip -Declipselink.weave.skip - -# Do the Maven build! -# Incremental docker builds will resume here when you change sources -ADD src src -RUN mvn package -DskipTests -RUN echo "done!" - -# 2nd stage, build the runtime image -FROM openjdk:17-jdk-slim -WORKDIR /helidon - -# Copy the binary built in the 1st stage -COPY --from=build /helidon/target/helidon-examples-integration-neo4j-mp.jar ./ -COPY --from=build /helidon/target/libs ./libs - -CMD ["java", "-jar", "helidon-examples-integration-neo4j-mp.jar"] - -EXPOSE 8080 diff --git a/examples/integrations/neo4j/neo4j-mp/Dockerfile.jlink b/examples/integrations/neo4j/neo4j-mp/Dockerfile.jlink deleted file mode 100644 index b011b1ebb35..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/Dockerfile.jlink +++ /dev/null @@ -1,40 +0,0 @@ -# -# Copyright (c) 2021, 2022 Oracle and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# 1st stage, build the app -FROM maven:3.6.3-openjdk-17-slim as build - -WORKDIR /helidon - -# Create a first layer to cache the "Maven World" in the local repository. -# Incremental docker builds will always resume after that, unless you update -# the pom -ADD pom.xml . -RUN mvn package -Dmaven.test.skip -Declipselink.weave.skip - -# Do the Maven build to create the custom Java Runtime Image -# Incremental docker builds will resume here when you change sources -ADD src src -RUN mvn package -Pjlink-image -DskipTests -RUN echo "done!" - -# 2nd stage, build the final image with the JRI built in the 1st stage - -FROM debian:stretch-slim -WORKDIR /helidon -COPY --from=build /helidon/target/helidon-examples-integration-neo4j-mp-jri ./ -ENTRYPOINT ["/bin/bash", "/helidon/bin/start"] -EXPOSE 8080 diff --git a/examples/integrations/neo4j/neo4j-mp/Dockerfile.native b/examples/integrations/neo4j/neo4j-mp/Dockerfile.native deleted file mode 100644 index 403c9eba53e..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/Dockerfile.native +++ /dev/null @@ -1,57 +0,0 @@ -# -# Copyright (c) 2021, 2023 Oracle and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# 1st stage, build the app -FROM ghcr.io/graalvm/graalvm-ce:ol9-java20-22.3.1 as build - -# Install native-image -RUN gu install native-image - -WORKDIR /usr/share - -# Install maven -RUN set -x && \ - curl -O https://archive.apache.org/dist/maven/maven-3/3.8.4/binaries/apache-maven-3.8.4-bin.tar.gz && \ - tar -xvf apache-maven-*-bin.tar.gz && \ - rm apache-maven-*-bin.tar.gz && \ - mv apache-maven-* maven && \ - ln -s /usr/share/maven/bin/mvn /bin/ - -WORKDIR /helidon - -# Create a first layer to cache the "Maven World" in the local repository. -# Incremental docker builds will always resume after that, unless you update -# the pom -ADD pom.xml . -RUN mvn package -Pnative-image -Dnative.image.skip -Dmaven.test.skip -Declipselink.weave.skip - -# Do the Maven build! -# Incremental docker builds will resume here when you change sources -ADD src src -RUN mvn package -Pnative-image -Dnative.image.buildStatic -DskipTests - -RUN echo "done!" - -# 2nd stage, build the runtime image -FROM scratch -WORKDIR /helidon - -# Copy the binary built in the 1st stage -COPY --from=build /helidon/target/helidon-examples-integration-neo4j-mp . - -ENTRYPOINT ["./helidon-examples-integration-neo4j-mp"] - -EXPOSE 8080 diff --git a/examples/integrations/neo4j/neo4j-mp/README.md b/examples/integrations/neo4j/neo4j-mp/README.md deleted file mode 100644 index d925c4b2d94..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/README.md +++ /dev/null @@ -1,167 +0,0 @@ -# Helidon Quickstart MP Example - -This example implements a simple Neo4j REST service using MicroProfile. - -## Build and run - -Bring up a Neo4j instance via Docker - -```bash -docker run --publish=7474:7474 --publish=7687:7687 -e 'NEO4J_AUTH=neo4j/secret' neo4j:4.0 -``` - -Goto the Neo4j browser and play the first step of the movies graph: [`:play movies`](http://localhost:7474/browser/?cmd=play&arg=movies). - - -Then build with JDK11+ -```bash -mvn package -java -jar target/helidon-examples-integration-neo4j-mp.jar -``` - -## Exercise the application - -``` -curl -X GET http://localhost:8080/movies - -``` - -## Try health and metrics - -``` -curl -s -X GET http://localhost:8080/health -{"outcome":"UP",... -. . . - -# Prometheus Format -curl -s -X GET http://localhost:8080/metrics -# TYPE base:gc_g1_young_generation_count gauge -. . . - -# JSON Format -curl -H 'Accept: application/json' -X GET http://localhost:8080/metrics -{"base":... -. . . - -``` - -## Build the Docker Image - -``` -docker build -t helidon-integrations-neo4j-mp . -``` - -## Start the application with Docker - -``` -docker run --rm -p 8080:8080 helidon-integrations-neo4j-mp:latest -``` - -Exercise the application as described above - -## Deploy the application to Kubernetes - -``` -kubectl cluster-info # Verify which cluster -kubectl get pods # Verify connectivity to cluster -kubectl create -f app.yaml # Deploy application -kubectl get service helidon-integrations-neo4j-mp # Verify deployed service -``` - -## Build a native image with GraalVM - -GraalVM allows you to compile your programs ahead-of-time into a native - executable. See https://www.graalvm.org/docs/reference-manual/aot-compilation/ - for more information. - -You can build a native executable in 2 different ways: -* With a local installation of GraalVM -* Using Docker - -### Local build - -Download Graal VM at https://www.graalvm.org/downloads. We recommend -version `20.1.0` or later. - -``` -# Setup the environment -export GRAALVM_HOME=/path -# build the native executable -mvn package -Pnative-image -``` - -You can also put the Graal VM `bin` directory in your PATH, or pass - `-DgraalVMHome=/path` to the Maven command. - -See https://github.com/oracle/helidon-build-tools/tree/master/helidon-maven-plugin#goal-native-image - for more information. - -Start the application: - -``` -./target/helidon-quickstart-mp -``` - -### Multi-stage Docker build - -Build the "native" Docker Image - -``` -docker build -t helidon-integrations-neo4j-mp-native -f Dockerfile.native . -``` - -Start the application: - -``` -docker run --rm -p 8080:8080 helidon-integrations-neo4j-mp-native:latest -``` - - -## Build a Java Runtime Image using jlink - -You can build a custom Java Runtime Image (JRI) containing the application jars and the JDK modules -on which they depend. This image also: - -* Enables Class Data Sharing by default to reduce startup time. -* Contains a customized `start` script to simplify CDS usage and support debug and test modes. - -You can build a custom JRI in two different ways: -* Local -* Using Docker - - -### Local build - -``` -# build the JRI -mvn package -Pjlink-image -``` - -See https://github.com/oracle/helidon-build-tools/tree/master/helidon-maven-plugin#goal-jlink-image - for more information. - -Start the application: - -``` -./target/helidon-integrations-neo4j-mp-jri/bin/start -``` - -### Multi-stage Docker build - -Build the JRI as a Docker Image - -``` -docker build -t helidon-integrations-neo4j-mp-jri -f Dockerfile.jlink . -``` - -Start the application: - -``` -docker run --rm -p 8080:8080 helidon-integrations-neo4j-mp-jri:latest -``` - -See the start script help: - -``` -docker run --rm helidon-integrations-neo4j-mp-jri:latest --help -``` diff --git a/examples/integrations/neo4j/neo4j-mp/app.yaml b/examples/integrations/neo4j/neo4j-mp/app.yaml deleted file mode 100644 index 3359b178eef..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/app.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# -# Copyright (c) 2021 Oracle and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -kind: Service -apiVersion: v1 -metadata: - name: helidon-examples-integration-neo4j-mp - labels: - app: helidon-examples-integration-neo4j-mp -spec: - type: NodePort - selector: - app: helidon-examples-integration-neo4j-mp - ports: - - port: 8080 - targetPort: 8080 - name: http ---- -kind: Deployment -apiVersion: extensions/v1beta1 -metadata: - name: helidon-examples-integration-neo4j-mp -spec: - replicas: 1 - template: - metadata: - labels: - app: helidon-examples-integration-neo4j-mp - version: v1 - spec: - containers: - - name: helidon-examples-integration-neo4j-mp - image: helidon-examples-integration-neo4j-mp - imagePullPolicy: IfNotPresent - ports: - - containerPort: 8080 ---- diff --git a/examples/integrations/neo4j/neo4j-mp/pom.xml b/examples/integrations/neo4j/neo4j-mp/pom.xml deleted file mode 100644 index 4916901140f..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/pom.xml +++ /dev/null @@ -1,141 +0,0 @@ - - - - - 4.0.0 - - io.helidon.applications - helidon-mp - 4.0.0-SNAPSHOT - ../../../../applications/mp/pom.xml - - io.helidon.examples.integrations.neo4j - helidon-examples-integration-neo4j-mp - Helidon Neo4j MP integration Example - - - 4.4.3 - - - - - io.helidon.microprofile.bundles - helidon-microprofile - - - io.helidon.integrations.neo4j - helidon-integrations-neo4j - - - io.helidon.integrations.neo4j - helidon-integrations-neo4j-metrics - - - io.helidon.integrations.neo4j - helidon-integrations-neo4j-health - - - - - org.jboss - jandex - runtime - true - - - - - - org.neo4j.test - neo4j-harness - ${neo4j-harness.version} - test - - - org.slf4j - slf4j-nop - - - org.junit.vintage - junit-vintage-engine - - - org.neo4j.app - neo4j-server - - - - - org.neo4j.app - neo4j-server - ${neo4j-harness.version} - test - - - * - * - - - - - org.junit.jupiter - junit-jupiter-api - test - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-libs - - - - - org.jboss.jandex - jandex-maven-plugin - - - make-index - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - - --add-opens=java.base/java.lang=ALL-UNNAMED - - - - - - diff --git a/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/Neo4jResource.java b/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/Neo4jResource.java deleted file mode 100644 index c35b4d33d93..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/Neo4jResource.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2021 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.examples.integrations.neo4j.mp; - -import java.util.List; - -import io.helidon.examples.integrations.neo4j.mp.domain.Movie; -import io.helidon.examples.integrations.neo4j.mp.domain.MovieRepository; - -import jakarta.enterprise.context.RequestScoped; -import jakarta.inject.Inject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; - -/** - * REST endpoint for movies. - */ -@Path("/movies") -@RequestScoped -public class Neo4jResource { - /** - * The greeting message provider. - */ - private final MovieRepository movieRepository; - - /** - * Constructor. - * - * @param movieRepository - */ - @Inject - public Neo4jResource(MovieRepository movieRepository) { - this.movieRepository = movieRepository; - } - - /** - * All movies. - * - * @return json String with all movies - */ - @GET - @Produces(MediaType.APPLICATION_JSON) - public List getAllMovies() { - return movieRepository.findAll(); - } - -} - diff --git a/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/Actor.java b/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/Actor.java deleted file mode 100644 index 4b821668180..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/Actor.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2002-2020 "Neo4j," - * Neo4j Sweden AB [http://neo4j.com] - * This file is part of Neo4j. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.examples.integrations.neo4j.mp.domain; - -import java.util.ArrayList; -import java.util.List; - -/* - * Helidon changes are under the copyright of: - * - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -/** - * The Actor class. - * - * @author Michael Simons - */ -public class Actor { - - private final String name; - - private final List roles; - - /** - * Constructor. - * - * @param name - * @param roles - */ - public Actor(String name, final List roles) { - this.name = name; - this.roles = new ArrayList<>(roles); - } -} diff --git a/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/Movie.java b/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/Movie.java deleted file mode 100644 index 7d1abcd1455..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/Movie.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2002-2020 "Neo4j," - * Neo4j Sweden AB [http://neo4j.com] - * This file is part of Neo4j. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.examples.integrations.neo4j.mp.domain; - -import java.util.ArrayList; -import java.util.List; - -/* - * Helidon changes are under the copyright of: - * - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -/** - * The Movie class. - * - * @author Michael Simons - */ -public class Movie { - - private final String title; - - private final String description; - - private List actors = new ArrayList<>(); - - private List directors = new ArrayList<>(); - - private Integer released; - - /** - * Constructor. - * - * @param title - * @param description - */ - public Movie(String title, String description) { - this.title = title; - this.description = description; - } - - public String getTitle() { - return title; - } - - public List getActors() { - return actors; - } - - public void setActors(List actors) { - this.actors = actors; - } - - public String getDescription() { - return description; - } - - public List getDirectors() { - return directors; - } - - public void setDirectorss(List directors) { - this.directors = directors; - } - - public Integer getReleased() { - return released; - } - - public void setReleased(Integer released) { - this.released = released; - } -} diff --git a/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/MovieRepository.java b/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/MovieRepository.java deleted file mode 100644 index c9eb52cef70..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/MovieRepository.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2002-2020 "Neo4j," - * Neo4j Sweden AB [http://neo4j.com] - * This file is part of Neo4j. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.examples.integrations.neo4j.mp.domain; - -import java.util.List; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import org.neo4j.driver.Driver; -import org.neo4j.driver.Value; - -/* - * Helidon changes are under the copyright of: - * - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -/** - * The Movies repository. - * - * @author Michael Simons - */ -@ApplicationScoped -public class MovieRepository { - - private final Driver driver; - - /** - * Constructor. - * @param driver - */ - @Inject - public MovieRepository(Driver driver) { - this.driver = driver; - } - - /** - * Return al Movies. - * @return list with movies - */ - public List findAll() { - - try (var session = driver.session()) { - - var query = "" - + "match (m:Movie) " - + "match (m) <- [:DIRECTED] - (d:Person) " - + "match (m) <- [r:ACTED_IN] - (a:Person) " - + "return m, collect(d) as directors, collect({name:a.name, roles: r.roles}) as actors"; - - return session.readTransaction(tx -> tx.run(query).list(r -> { - var movieNode = r.get("m").asNode(); - - var directors = r.get("directors").asList(v -> { - var personNode = v.asNode(); - return new Person(personNode.get("born").asInt(), personNode.get("name").asString()); - }); - - var actors = r.get("actors").asList(v -> { - return new Actor(v.get("name").asString(), v.get("roles").asList(Value::asString)); - }); - - var m = new Movie(movieNode.get("title").asString(), movieNode.get("tagline").asString()); - m.setReleased(movieNode.get("released").asInt()); - m.setDirectorss(directors); - m.setActors(actors); - return m; - })); - } - } -} diff --git a/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/Person.java b/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/Person.java deleted file mode 100644 index 692a58e7e93..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/Person.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2002-2020 "Neo4j," - * Neo4j Sweden AB [http://neo4j.com] - * This file is part of Neo4j. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.examples.integrations.neo4j.mp.domain; - -/* - * Helidon changes are under the copyright of: - * - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -/** - * The Person class. - * - * @author Michael Simons - */ -public class Person { - - private final String name; - - private Integer born; - - /** - * Person constructor. - * - * @param born - * @param name - */ - public Person(Integer born, String name) { - this.born = born; - this.name = name; - } - - public String getName() { - return name; - } - - public Integer getBorn() { - return born; - } - - public void setBorn(Integer born) { - this.born = born; - } - - @SuppressWarnings("checkstyle:OperatorWrap") - @Override - public String toString() { - return "Person{" + - "name='" + name + '\'' + - ", born=" + born + - '}'; - } -} diff --git a/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/package-info.java b/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/package-info.java deleted file mode 100644 index 0883266ea9e..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/domain/package-info.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (c) 2021 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Domain objects for movie DB. - */ -package io.helidon.examples.integrations.neo4j.mp.domain; diff --git a/examples/integrations/neo4j/neo4j-mp/src/main/resources/META-INF/beans.xml b/examples/integrations/neo4j/neo4j-mp/src/main/resources/META-INF/beans.xml deleted file mode 100644 index dbf3e648c1e..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/src/main/resources/META-INF/beans.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - diff --git a/examples/integrations/neo4j/neo4j-mp/src/main/resources/META-INF/microprofile-config.properties b/examples/integrations/neo4j/neo4j-mp/src/main/resources/META-INF/microprofile-config.properties deleted file mode 100644 index 70f63d64091..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/src/main/resources/META-INF/microprofile-config.properties +++ /dev/null @@ -1,31 +0,0 @@ -# -# Copyright (c) 2021 Oracle and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# Application properties. This is the default greeting -app.greeting=Hello - -# Microprofile server properties -server.port=8080 -server.host=0.0.0.0 - -# Enable the optional MicroProfile Metrics REST.request metrics -metrics.rest-request.enabled=true - -# Neo4j settings -neo4j.uri=bolt://localhost:7687 -neo4j.authentication.username=neo4j -neo4j.authentication.password: secret -neo4j.pool.metricsEnabled: true diff --git a/examples/integrations/neo4j/neo4j-mp/src/main/resources/logging.properties b/examples/integrations/neo4j/neo4j-mp/src/main/resources/logging.properties deleted file mode 100644 index b3e1f27ca75..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/src/main/resources/logging.properties +++ /dev/null @@ -1,27 +0,0 @@ -# -# Copyright (c) 2021, 2022 Oracle and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# Example Logging Configuration File -# For more information see $JAVA_HOME/jre/lib/logging.properties - -# Send messages to the console -handlers=io.helidon.logging.jul.HelidonConsoleHandler - -# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread -java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n - -# Global logging level. Can be overridden by specific loggers -.level=INFO diff --git a/examples/integrations/neo4j/neo4j-mp/src/test/java/io/helidon/examples/integrations/neo4j/mp/MainTest.java b/examples/integrations/neo4j/neo4j-mp/src/test/java/io/helidon/examples/integrations/neo4j/mp/MainTest.java deleted file mode 100644 index 60f39e02d4c..00000000000 --- a/examples/integrations/neo4j/neo4j-mp/src/test/java/io/helidon/examples/integrations/neo4j/mp/MainTest.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.examples.integrations.neo4j.mp; - -import io.helidon.microprofile.server.Server; - -import jakarta.enterprise.inject.se.SeContainer; -import jakarta.enterprise.inject.spi.CDI; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.neo4j.harness.Neo4j; -import org.neo4j.harness.Neo4jBuilders; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -/** - * Main tests of the application done here. - */ -@Disabled("3.0.0-JAKARTA") // OpenAPI -// Caused by: java.lang.NoSuchMethodError: 'java.util.List org.jboss.jandex.ClassInfo.unsortedFields()' -class MainTest { - private static Server server; - private static Neo4j embeddedDatabaseServer; - - @BeforeAll - public static void startTheServer() throws Exception { - - embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder() - .withDisabledServer() - .withFixture(FIXTURE) - .build(); - - System.setProperty("neo4j.uri", embeddedDatabaseServer.boltURI().toString()); - - server = Server.create().start(); - - } - - @AfterAll - static void destroyClass() { - CDI current = CDI.current(); - ((SeContainer) current).close(); - embeddedDatabaseServer.close(); - } - - - @Test - void testMovies() { - - Client client = ClientBuilder.newClient(); - - JsonArray jsorArray = client - .target(getConnectionString("/movies")) - .request() - .get(JsonArray.class); - JsonObject first = jsorArray.getJsonObject(0); - assertThat(first.getString("title"), is("The Matrix")); - - } - - private String getConnectionString(String path) { - return "http://localhost:" + server.port() + path; - } - - static final String FIXTURE = "" - + "CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'})\n" - + "CREATE (Keanu:Person {name:'Keanu Reeves', born:1964})\n" - + "CREATE (Carrie:Person {name:'Carrie-Anne Moss', born:1967})\n" - + "CREATE (Laurence:Person {name:'Laurence Fishburne', born:1961})\n" - + "CREATE (Hugo:Person {name:'Hugo Weaving', born:1960})\n" - + "CREATE (LillyW:Person {name:'Lilly Wachowski', born:1967})\n" - + "CREATE (LanaW:Person {name:'Lana Wachowski', born:1965})\n" - + "CREATE (JoelS:Person {name:'Joel Silver', born:1952})\n" - + "CREATE (KevinB:Person {name:'Kevin Bacon', born:1958})\n" - + "CREATE\n" - + "(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix),\n" - + "(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrix),\n" - + "(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrix),\n" - + "(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrix),\n" - + "(LillyW)-[:DIRECTED]->(TheMatrix),\n" - + "(LanaW)-[:DIRECTED]->(TheMatrix),\n" - + "(JoelS)-[:PRODUCED]->(TheMatrix)\n" - + "\n" - + "CREATE (Emil:Person {name:\"Emil Eifrem\", born:1978})\n" - + "CREATE (Emil)-[:ACTED_IN {roles:[\"Emil\"]}]->(TheMatrix)\n" - + "\n" - + "CREATE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded', released:2003, tagline:'Free your mind'})\n" - + "CREATE\n" - + "(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixReloaded),\n" - + "(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixReloaded),\n" - + "(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixReloaded),\n" - + "(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixReloaded),\n" - + "(LillyW)-[:DIRECTED]->(TheMatrixReloaded),\n" - + "(LanaW)-[:DIRECTED]->(TheMatrixReloaded),\n" - + "(JoelS)-[:PRODUCED]->(TheMatrixReloaded)\n" - + "\n" - + "CREATE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions', released:2003, tagline:'Everything that has a beginning has an end'})\n" - + "CREATE\n" - + "(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixRevolutions),\n" - + "(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixRevolutions),\n" - + "(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixRevolutions),\n" - + "(KevinB)-[:ACTED_IN {roles:['Unknown']}]->(TheMatrixRevolutions),\n" - + "(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixRevolutions),\n" - + "(LillyW)-[:DIRECTED]->(TheMatrixRevolutions),\n" - + "(LanaW)-[:DIRECTED]->(TheMatrixRevolutions),\n" - + "(JoelS)-[:PRODUCED]->(TheMatrixRevolutions)\n"; -} diff --git a/examples/integrations/neo4j/neo4j-se/.dockerignore b/examples/integrations/neo4j/neo4j-se/.dockerignore deleted file mode 100644 index c8b241f2215..00000000000 --- a/examples/integrations/neo4j/neo4j-se/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -target/* \ No newline at end of file diff --git a/examples/integrations/neo4j/neo4j-se/Dockerfile b/examples/integrations/neo4j/neo4j-se/Dockerfile deleted file mode 100644 index bfe5c682281..00000000000 --- a/examples/integrations/neo4j/neo4j-se/Dockerfile +++ /dev/null @@ -1,45 +0,0 @@ -# -# Copyright (c) 2021 Oracle and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# 1st stage, build the app -FROM maven:3.6.3-openjdk-17-slim as build - -WORKDIR /helidon - -# Create a first layer to cache the "Maven World" in the local repository. -# Incremental docker builds will always resume after that, unless you update -# the pom -ADD pom.xml . -RUN mvn package -Dmaven.test.skip -Declipselink.weave.skip - -# Do the Maven build! -# Incremental docker builds will resume here when you change sources -ADD src src -RUN mvn package -DskipTests - -RUN echo "done!" - -# 2nd stage, build the runtime image -FROM openjdk:17-jdk-slim -WORKDIR /helidon - -# Copy the binary built in the 1st stage -COPY --from=build /helidon/target/helidon-examples-integration-neo4j-se.jar ./ -COPY --from=build /helidon/target/libs ./libs - -CMD ["java", "-jar", "helidon-examples-integration-neo4j-se.jar"] - -EXPOSE 8080 diff --git a/examples/integrations/neo4j/neo4j-se/Dockerfile.jlink b/examples/integrations/neo4j/neo4j-se/Dockerfile.jlink deleted file mode 100644 index f0f92e3ac3b..00000000000 --- a/examples/integrations/neo4j/neo4j-se/Dockerfile.jlink +++ /dev/null @@ -1,40 +0,0 @@ -# -# Copyright (c) 2021, 2022 Oracle and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# 1st stage, build the app -FROM maven:3.6.3-openjdk-17-slim as build - -WORKDIR /helidon - -# Create a first layer to cache the "Maven World" in the local repository. -# Incremental docker builds will always resume after that, unless you update -# the pom -ADD pom.xml . -RUN mvn package -Dmaven.test.skip -Declipselink.weave.skip - -# Do the Maven build to create the custom Java Runtime Image -# Incremental docker builds will resume here when you change sources -ADD src src -RUN mvn package -Pjlink-image -DskipTests -RUN echo "done!" - -# 2nd stage, build the final image with the JRI built in the 1st stage - -FROM debian:stretch-slim -WORKDIR /helidon -COPY --from=build /helidon/target/helidon-examples-integration-neo4j-se-jri ./ -ENTRYPOINT ["/bin/bash", "/helidon/bin/start"] -EXPOSE 8080 diff --git a/examples/integrations/neo4j/neo4j-se/Dockerfile.native b/examples/integrations/neo4j/neo4j-se/Dockerfile.native deleted file mode 100644 index bc844d3d556..00000000000 --- a/examples/integrations/neo4j/neo4j-se/Dockerfile.native +++ /dev/null @@ -1,57 +0,0 @@ -# -# Copyright (c) 2021, 2023 Oracle and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# 1st stage, build the app -FROM ghcr.io/graalvm/graalvm-ce:ol9-java20-22.3.1 as build - -# Install native-image -RUN gu install native-image - -WORKDIR /usr/share - -# Install maven -RUN set -x && \ - curl -O https://archive.apache.org/dist/maven/maven-3/3.8.4/binaries/apache-maven-3.8.4-bin.tar.gz && \ - tar -xvf apache-maven-*-bin.tar.gz && \ - rm apache-maven-*-bin.tar.gz && \ - mv apache-maven-* maven && \ - ln -s /usr/share/maven/bin/mvn /bin/ - -WORKDIR /helidon - -# Create a first layer to cache the "Maven World" in the local repository. -# Incremental docker builds will always resume after that, unless you update -# the pom -ADD pom.xml . -RUN mvn package -Pnative-image -Dnative.image.skip -Dmaven.test.skip -Declipselink.weave.skip - -# Do the Maven build! -# Incremental docker builds will resume here when you change sources -ADD src src -RUN mvn package -Pnative-image -Dnative.image.buildStatic -DskipTests - -RUN echo "done!" - -# 2nd stage, build the runtime image -FROM scratch -WORKDIR /helidon - -# Copy the binary built in the 1st stage -COPY --from=build /helidon/target/helidon-examples-integration-neo4j-se . - -ENTRYPOINT ["./helidon-examples-integration-neo4j-se"] - -EXPOSE 8080 diff --git a/examples/integrations/neo4j/neo4j-se/README.md b/examples/integrations/neo4j/neo4j-se/README.md deleted file mode 100644 index ad4d09abd42..00000000000 --- a/examples/integrations/neo4j/neo4j-se/README.md +++ /dev/null @@ -1,183 +0,0 @@ -# Helidon SE integration with Neo4J example - -## Build and run - -Bring up a Neo4j instance via Docker - -```bash -docker run --publish=7474:7474 --publish=7687:7687 -e 'NEO4J_AUTH=neo4j/secret' neo4j:4.0 -``` - -Goto the Neo4j browser and play the first step of the movies graph: [`:play movies`](http://localhost:7474/browser/?cmd=play&arg=movies). - -Build and run with With JDK11+ -```bash -mvn package -java -jar target/helidon-examples-integration-neo4j-se.jar -``` - -Then access the rest API like this: - -```` -curl localhost:8080/api/movies -```` - -#Health and metrics - -Heo4jSupport provides health checks and metrics reading from Neo4j. - -To enable them add to routing: -```java -// metrics -Neo4jMetricsSupport.builder() - .driver(neo4j.driver()) - .build() - .initialize(); -// health checks -HealthSupport health = HealthSupport.builder() - .add(HealthChecks.healthChecks()) // Adds a convenient set of checks - .addReadiness(Neo4jHealthCheck.create(neo4j.driver())) - .build(); - -return Routing.builder() - .register(health) // Health at "/health" - .register(metrics) // Metrics at "/metrics" - .register(movieService) - .build(); -``` -and enable them in the driver: -```yaml - pool: - metricsEnabled: true -``` - - -```` -curl localhost:8080/health -```` - -```` -curl localhost:8080/metrics -```` - - - -## Build the Docker Image - -``` -docker build -t helidon-integrations-heo4j-se . -``` - -## Start the application with Docker - -``` -docker run --rm -p 8080:8080 helidon-integrations-heo4j-se:latest -``` - -Exercise the application as described above - -## Deploy the application to Kubernetes - -``` -kubectl cluster-info # Verify which cluster -kubectl get pods # Verify connectivity to cluster -kubectl create -f app.yaml # Deply application -kubectl get service helidon-integrations-heo4j-se # Get service info -``` - -## Build a native image with GraalVM - -GraalVM allows you to compile your programs ahead-of-time into a native - executable. See https://www.graalvm.org/docs/reference-manual/aot-compilation/ - for more information. - -You can build a native executable in 2 different ways: -* With a local installation of GraalVM -* Using Docker - -### Local build - -Download Graal VM at https://www.graalvm.org/downloads. We recommend -version `20.1.0` or later. - -``` -# Setup the environment -export GRAALVM_HOME=/path -# build the native executable -mvn package -Pnative-image -``` - -You can also put the Graal VM `bin` directory in your PATH, or pass - `-DgraalVMHome=/path` to the Maven command. - -See https://github.com/oracle/helidon-build-tools/tree/master/helidon-maven-plugin#goal-native-image - for more information. - -Start the application: - -``` -./target/helidon-integrations-heo4j-se -``` - -### Multi-stage Docker build - -Build the "native" Docker Image - -``` -docker build -t helidon-integrations-heo4j-se-native -f Dockerfile.native . -``` - -Start the application: - -``` -docker run --rm -p 8080:8080 helidon-integrations-heo4j-se-native:latest -``` - -## Build a Java Runtime Image using jlink - -You can build a custom Java Runtime Image (JRI) containing the application jars and the JDK modules -on which they depend. This image also: - -* Enables Class Data Sharing by default to reduce startup time. -* Contains a customized `start` script to simplify CDS usage and support debug and test modes. - -You can build a custom JRI in two different ways: -* Local -* Using Docker - - -### Local build - -``` -# build the JRI -mvn package -Pjlink-image -``` - -See https://github.com/oracle/helidon-build-tools/tree/master/helidon-maven-plugin#goal-jlink-image - for more information. - -Start the application: - -``` -./target/helidon-integrations-heo4j-se-jri/bin/start -``` - -### Multi-stage Docker build - -Build the JRI as a Docker Image - -``` -docker build -t helidon-integrations-heo4j-se-jri -f Dockerfile.jlink . -``` - -Start the application: - -``` -docker run --rm -p 8080:8080 helidon-integrations-heo4j-se-jri:latest -``` - -See the start script help: - -``` -docker run --rm helidon-integrations-heo4j-se-jri:latest --help -``` diff --git a/examples/integrations/neo4j/neo4j-se/app.yaml b/examples/integrations/neo4j/neo4j-se/app.yaml deleted file mode 100644 index 7028ee6a53a..00000000000 --- a/examples/integrations/neo4j/neo4j-se/app.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# -# Copyright (c) 2021 Oracle and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -kind: Service -apiVersion: v1 -metadata: - name: helidon-examples-integration-neo4j-se - labels: - app: helidon-examples-integration-neo4j-se -spec: - type: NodePort - selector: - app: helidon-examples-integration-neo4j-se - ports: - - port: 8080 - targetPort: 8080 - name: http ---- -kind: Deployment -apiVersion: extensions/v1beta1 -metadata: - name: helidon-examples-integration-neo4j-se -spec: - replicas: 1 - template: - metadata: - labels: - app: helidon-examples-integration-neo4j-se - version: v1 - spec: - containers: - - name: helidon-examples-integration-neo4j-se - image: helidon-examples-integration-neo4j-se - imagePullPolicy: IfNotPresent - ports: - - containerPort: 8080 ---- diff --git a/examples/integrations/neo4j/neo4j-se/pom.xml b/examples/integrations/neo4j/neo4j-se/pom.xml deleted file mode 100644 index bcd45f1ac73..00000000000 --- a/examples/integrations/neo4j/neo4j-se/pom.xml +++ /dev/null @@ -1,143 +0,0 @@ - - - - - 4.0.0 - - io.helidon.applications - helidon-se - 4.0.0-SNAPSHOT - ../../../../applications/se/pom.xml - - io.helidon.examples.integrations.neo4j - helidon-examples-integration-neo4j-se - Helidon Integrations Neo4j SE Example - - - io.helidon.examples.integrations.neo4j.se.Main - 4.4.3 - - - - - io.helidon.reactive.webserver - helidon-reactive-webserver - - - io.helidon.reactive.media - helidon-reactive-media-jsonp - - - io.helidon.reactive.media - helidon-reactive-media-jsonb - - - io.helidon.config - helidon-config-yaml - - - io.helidon.reactive.health - helidon-reactive-health - - - io.helidon.health - helidon-health-checks - - - io.helidon.reactive.metrics - helidon-reactive-metrics - - - io.helidon.metrics - helidon-metrics - runtime - - - io.helidon.integrations.neo4j - helidon-integrations-neo4j - - - io.helidon.integrations.neo4j - helidon-integrations-neo4j-metrics - - - io.helidon.integrations.neo4j - helidon-integrations-neo4j-health - - - org.junit.jupiter - junit-jupiter-api - test - - - io.helidon.reactive.webclient - helidon-reactive-webclient - test - - - - org.neo4j.test - neo4j-harness - ${neo4j-harness.version} - test - - - org.junit.vintage - junit-vintage-engine - - - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-libs - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - - --enable-preview - --add-exports=java.base/sun.nio.ch=ALL-UNNAMED - --add-opens=java.base/java.lang=ALL-UNNAMED - --add-opens=java.base/java.lang.reflect=ALL-UNNAMED - --add-opens=java.base/java.io=ALL-UNNAMED - --add-opens=java.base/java.nio=ALL-UNNAMED - --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED - - - - - - diff --git a/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/Main.java b/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/Main.java deleted file mode 100644 index 0417e25a14e..00000000000 --- a/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/Main.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.examples.integrations.neo4j.se; - -import java.io.IOException; -import java.io.InputStream; -import java.util.logging.LogManager; - -import io.helidon.common.reactive.Single; -import io.helidon.config.Config; -import io.helidon.examples.integrations.neo4j.se.domain.MovieRepository; -import io.helidon.health.checks.HealthChecks; -import io.helidon.integrations.neo4j.Neo4j; -import io.helidon.integrations.neo4j.health.Neo4jHealthCheck; -import io.helidon.integrations.neo4j.metrics.Neo4jMetricsSupport; -import io.helidon.logging.common.LogConfig; -import io.helidon.reactive.health.HealthSupport; -import io.helidon.reactive.media.jsonb.JsonbSupport; -import io.helidon.reactive.media.jsonp.JsonpSupport; -import io.helidon.reactive.metrics.MetricsSupport; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.WebServer; - -import org.neo4j.driver.Driver; - -/** - * The application main class. - */ -public final class Main { - - /** - * Cannot be instantiated. - */ - private Main() { - } - - /** - * Application main entry point. - * @param args command line arguments. - * @throws IOException if there are problems reading logging properties - */ - public static void main(final String[] args) throws IOException { - startServer(); - } - - /** - * Start the server. - * @return the created WebServer instance - */ - public static Single startServer() { - // load logging configuration - LogConfig.configureRuntime(); - - // By default this will pick up application.yaml from the classpath - Config config = Config.create(); - - Single server = WebServer.builder(createRouting(config)) - .config(config.get("server")) - .addMediaSupport(JsonpSupport.create()) - .addMediaSupport(JsonbSupport.create()) - .build() - .start(); - - server.thenAccept(ws -> { - System.out.println( - "WEB server is up! http://localhost:" + ws.port() + "/api/movies"); - ws.whenShutdown().thenRun(() - -> System.out.println("WEB server is DOWN. Good bye!")); - }) - .exceptionally(t -> { - System.err.println("Startup failed: " + t.getMessage()); - t.printStackTrace(System.err); - return null; - }); - - return server; - } - - /** - * Creates new Routing. - * - * @return routing configured with JSON support, a health check, and a service - * @param config configuration of this server - */ - private static Routing createRouting(Config config) { - - MetricsSupport metrics = MetricsSupport.create(); - - Neo4j neo4j = Neo4j.create(config.get("neo4j")); - - // registers all metrics - Neo4jMetricsSupport.builder() - .driver(neo4j.driver()) - .build() - .initialize(); - - Neo4jHealthCheck healthCheck = Neo4jHealthCheck.create(neo4j.driver()); - - Driver neo4jDriver = neo4j.driver(); - - MovieService movieService = new MovieService(new MovieRepository(neo4jDriver)); - - HealthSupport health = HealthSupport.builder() - .add(HealthChecks.healthChecks()) // Adds a convenient set of checks - .addReadiness(healthCheck) - .build(); - - return Routing.builder() - .register(health) // Health at "/health" - .register(metrics) // Metrics at "/metrics" - .register(movieService) - .build(); - } - - /** - * Configure logging from logging.properties file. - */ - private static void setupLogging() throws IOException { - try (InputStream is = Main.class.getResourceAsStream("/logging.properties")) { - LogManager.getLogManager().readConfiguration(is); - } - } - -} diff --git a/examples/integrations/neo4j/neo4j-se/src/test/java/io/helidon/examples/quickstart/se/MainTest.java b/examples/integrations/neo4j/neo4j-se/src/test/java/io/helidon/examples/quickstart/se/MainTest.java deleted file mode 100644 index 455cb018177..00000000000 --- a/examples/integrations/neo4j/neo4j-se/src/test/java/io/helidon/examples/quickstart/se/MainTest.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.examples.quickstart.se; - -import java.util.concurrent.TimeUnit; - -import io.helidon.common.http.Http; -import io.helidon.examples.integrations.neo4j.se.Main; -import io.helidon.reactive.media.jsonp.JsonpSupport; -import io.helidon.reactive.webclient.WebClient; -import io.helidon.reactive.webclient.WebClientResponse; -import io.helidon.reactive.webserver.WebServer; - -import jakarta.json.JsonArray; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.neo4j.harness.Neo4j; -import org.neo4j.harness.Neo4jBuilders; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -/** - * Main test class for Neo4j Helidon SE quickstarter. - */ -public class MainTest { - - private static WebServer webServer; - private static WebClient webClient; - - private static Neo4j embeddedDatabaseServer; - - @BeforeAll - static void startTheServer() { - - embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder() - .withDisabledServer() - .withFixture(FIXTURE) - .build(); - - System.setProperty("neo4j.uri", embeddedDatabaseServer.boltURI().toString()); - - webServer = Main.startServer().await(); - - webClient = WebClient.builder() - .baseUri("http://localhost:" + webServer.port()) - .addMediaSupport(JsonpSupport.create()) - .build(); - } - - @AfterAll - static void stopServer() { - if (webServer != null) { - webServer.shutdown() - .await(10, TimeUnit.SECONDS); - } - if (embeddedDatabaseServer != null) { - embeddedDatabaseServer.close(); - } - } - - @Test - void testMovies() { - - JsonArray result = webClient.get() - .path("api/movies") - .request(JsonArray.class) - .await(); - - assertThat(result.getJsonObject(0).getString("title"), is("The Matrix Reloaded")); - } - - @Test - public void testHealth() { - - WebClientResponse response = webClient.get() - .path("/health") - .request() - .await(); - - assertThat(response.status(), is(Http.Status.OK_200)); - } - - @Test - public void testMetrics() { - WebClientResponse response = webClient.get() - .path("/metrics") - .request() - .await(); - - assertThat(response.status(), is(Http.Status.OK_200)); - } - - static final String FIXTURE = "" - + "CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'})\n" - + "CREATE (Keanu:Person {name:'Keanu Reeves', born:1964})\n" - + "CREATE (Carrie:Person {name:'Carrie-Anne Moss', born:1967})\n" - + "CREATE (Laurence:Person {name:'Laurence Fishburne', born:1961})\n" - + "CREATE (Hugo:Person {name:'Hugo Weaving', born:1960})\n" - + "CREATE (LillyW:Person {name:'Lilly Wachowski', born:1967})\n" - + "CREATE (LanaW:Person {name:'Lana Wachowski', born:1965})\n" - + "CREATE (JoelS:Person {name:'Joel Silver', born:1952})\n" - + "CREATE (KevinB:Person {name:'Kevin Bacon', born:1958})\n" - + "CREATE\n" - + "(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix),\n" - + "(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrix),\n" - + "(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrix),\n" - + "(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrix),\n" - + "(LillyW)-[:DIRECTED]->(TheMatrix),\n" - + "(LanaW)-[:DIRECTED]->(TheMatrix),\n" - + "(JoelS)-[:PRODUCED]->(TheMatrix)\n" - + "\n" - + "CREATE (Emil:Person {name:\"Emil Eifrem\", born:1978})\n" - + "CREATE (Emil)-[:ACTED_IN {roles:[\"Emil\"]}]->(TheMatrix)\n" - + "\n" - + "CREATE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded', released:2003, tagline:'Free your mind'})\n" - + "CREATE\n" - + "(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixReloaded),\n" - + "(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixReloaded),\n" - + "(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixReloaded),\n" - + "(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixReloaded),\n" - + "(LillyW)-[:DIRECTED]->(TheMatrixReloaded),\n" - + "(LanaW)-[:DIRECTED]->(TheMatrixReloaded),\n" - + "(JoelS)-[:PRODUCED]->(TheMatrixReloaded)\n" - + "\n" - + "CREATE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions', released:2003, tagline:'Everything that has a beginning has an end'})\n" - + "CREATE\n" - + "(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixRevolutions),\n" - + "(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixRevolutions),\n" - + "(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixRevolutions),\n" - + "(KevinB)-[:ACTED_IN {roles:['Unknown']}]->(TheMatrixRevolutions),\n" - + "(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixRevolutions),\n" - + "(LillyW)-[:DIRECTED]->(TheMatrixRevolutions),\n" - + "(LanaW)-[:DIRECTED]->(TheMatrixRevolutions),\n" - + "(JoelS)-[:PRODUCED]->(TheMatrixRevolutions)\n"; -} \ No newline at end of file diff --git a/examples/integrations/neo4j/pom.xml b/examples/integrations/neo4j/pom.xml index 2310f680841..3a3c60fef69 100644 --- a/examples/integrations/neo4j/pom.xml +++ b/examples/integrations/neo4j/pom.xml @@ -17,22 +17,112 @@ --> - + 4.0.0 - io.helidon.examples.integrations - helidon-examples-integrations-project + io.helidon.applications + helidon-nima 4.0.0-SNAPSHOT + ../../../applications/nima/pom.xml - io.helidon.examples.integrations.neo4j - helidon-examples-integrations-neo4j-project - Helidon Neo4j Integrations Examples - pom + helidon-examples-integration-neo4j + Helidon Integrations Neo4j Example - - neo4j-mp - neo4j-se - + + io.helidon.examples.integrations.neo4j.Main + 5.8.0 + + + + io.helidon.nima.webserver + helidon-nima-webserver + + + io.helidon.health + helidon-health-checks + + + io.helidon.nima.http.media + helidon-nima-http-media-jsonp + + + io.helidon.nima.http.media + helidon-nima-http-media-jsonb + + + io.helidon.config + helidon-config-yaml + + + io.helidon.nima.observe + helidon-nima-observe + + + io.helidon.nima.observe + helidon-nima-observe-health + + + io.helidon.nima.observe + helidon-nima-observe-metrics + + + io.helidon.metrics + helidon-metrics + + + io.helidon.integrations.neo4j + helidon-integrations-neo4j + + + io.helidon.integrations.neo4j + helidon-integrations-neo4j-health + + + io.helidon.integrations.neo4j + helidon-integrations-neo4j-metrics + + + io.helidon.nima.testing.junit5 + helidon-nima-testing-junit5-webserver + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + org.neo4j.test + neo4j-harness + ${neo4j-harness.version} + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + diff --git a/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/Main.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/Main.java new file mode 100644 index 00000000000..8ad950fb79d --- /dev/null +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/Main.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.neo4j; + +import io.helidon.config.Config; +import io.helidon.examples.integrations.neo4j.domain.MovieRepository; +import io.helidon.health.checks.DeadlockHealthCheck; +import io.helidon.health.checks.DiskSpaceHealthCheck; +import io.helidon.health.checks.HeapMemoryHealthCheck; +import io.helidon.integrations.neo4j.Neo4j; +import io.helidon.integrations.neo4j.health.Neo4jHealthCheck; +import io.helidon.integrations.neo4j.metrics.Neo4jMetricsSupport; +import io.helidon.logging.common.LogConfig; +import io.helidon.nima.http.media.jsonp.JsonpSupport; +import io.helidon.nima.observe.ObserveFeature; +import io.helidon.nima.observe.health.HealthFeature; +import io.helidon.nima.observe.health.HealthObserveProvider; +import io.helidon.nima.webserver.WebServer; + +import org.neo4j.driver.Driver; + +import static io.helidon.nima.webserver.http.HttpRouting.Builder; + +/** + * The application main class. + */ +public class Main { + /** + * Cannot be instantiated. + */ + private Main() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(String[] args) { + // load logging configuration + LogConfig.configureRuntime(); + + startServer(); + } + + static void startServer() { + + Config config = Config.create(); + WebServer server = WebServer.builder() + .addMediaSupport(JsonpSupport.create(config)) + .routing(Main::routing) + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port() + "/api/movies"); + } + + /** + * Updates HTTP Routing. + */ + static void routing(Builder routing) { + Neo4j neo4j = Neo4j.create(Config.create().get("neo4j")); + Driver neo4jDriver = neo4j.driver(); + + Neo4jMetricsSupport.builder() + .driver(neo4jDriver) + .build() + .initialize(); + + Neo4jHealthCheck healthCheck = Neo4jHealthCheck.create(neo4jDriver); + + MovieService movieService = new MovieService(new MovieRepository(neo4jDriver)); + + ObserveFeature observe = ObserveFeature.builder() + .addProvider(HealthObserveProvider.create(HealthFeature.builder() + .useSystemServices(false) + .addCheck(HeapMemoryHealthCheck.create()) + .addCheck(DiskSpaceHealthCheck.create()) + .addCheck(DeadlockHealthCheck.create()) + .addCheck(healthCheck) + .build())) + .build(); + + routing.register(movieService) + .addFeature(observe); + } +} + diff --git a/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/MovieService.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/MovieService.java similarity index 64% rename from examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/MovieService.java rename to examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/MovieService.java index ec2f41a8f58..5c8a87611dd 100644 --- a/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/MovieService.java +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/MovieService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,37 +14,32 @@ * limitations under the License. */ -package io.helidon.examples.integrations.neo4j.se; +package io.helidon.examples.integrations.neo4j; -import io.helidon.examples.integrations.neo4j.se.domain.MovieRepository; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.ServerRequest; -import io.helidon.reactive.webserver.ServerResponse; -import io.helidon.reactive.webserver.Service; +import io.helidon.examples.integrations.neo4j.domain.MovieRepository; +import io.helidon.nima.webserver.http.HttpRules; +import io.helidon.nima.webserver.http.HttpService; +import io.helidon.nima.webserver.http.ServerRequest; +import io.helidon.nima.webserver.http.ServerResponse; /** * The Movie service. - * */ -public class MovieService implements Service { +public class MovieService implements HttpService { private final MovieRepository movieRepository; /** * The movies service. - * @param movieRepository + * + * @param movieRepository a movie repository. */ public MovieService(MovieRepository movieRepository) { this.movieRepository = movieRepository; } - /** - * Main routing done here. - * - * @param rules - */ @Override - public void update(Routing.Rules rules) { + public void routing(HttpRules rules) { rules.get("/api/movies", this::findMoviesHandler); } diff --git a/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/domain/Actor.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Actor.java similarity index 94% rename from examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/domain/Actor.java rename to examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Actor.java index 7b0bdb2320e..ee9d5d8e3e1 100644 --- a/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/domain/Actor.java +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Actor.java @@ -13,9 +13,10 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ -package io.helidon.examples.integrations.neo4j.se.domain; +package io.helidon.examples.integrations.neo4j.domain; import java.util.ArrayList; import java.util.List; @@ -23,7 +24,7 @@ /* * Helidon changes are under the copyright of: * - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/domain/Movie.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Movie.java similarity index 95% rename from examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/domain/Movie.java rename to examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Movie.java index 42552be4d6e..17c9ca519b8 100644 --- a/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/domain/Movie.java +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Movie.java @@ -13,9 +13,10 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ -package io.helidon.examples.integrations.neo4j.se.domain; +package io.helidon.examples.integrations.neo4j.domain; import java.util.ArrayList; import java.util.List; @@ -23,7 +24,7 @@ /* * Helidon changes are under the copyright of: * - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/domain/MovieRepository.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/MovieRepository.java similarity index 96% rename from examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/domain/MovieRepository.java rename to examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/MovieRepository.java index 0e5d9bf79b1..40a2c4833f2 100644 --- a/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/domain/MovieRepository.java +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/MovieRepository.java @@ -13,9 +13,10 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ -package io.helidon.examples.integrations.neo4j.se.domain; +package io.helidon.examples.integrations.neo4j.domain; import java.util.List; @@ -25,7 +26,7 @@ /* * Helidon changes are under the copyright of: * - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/domain/Person.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Person.java similarity index 94% rename from examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/domain/Person.java rename to examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Person.java index 7a057c6617a..5fb2a1abe45 100644 --- a/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/domain/Person.java +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/Person.java @@ -13,13 +13,15 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ -package io.helidon.examples.integrations.neo4j.se.domain; + +package io.helidon.examples.integrations.neo4j.domain; /* * Helidon changes are under the copyright of: * - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/domain/package-info.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/package-info.java similarity index 84% rename from examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/domain/package-info.java rename to examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/package-info.java index efb4e863e8b..215caf712f1 100644 --- a/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/domain/package-info.java +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/domain/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,4 +17,4 @@ /** * Domain objects for movies. */ -package io.helidon.examples.integrations.neo4j.se.domain; +package io.helidon.examples.integrations.neo4j.domain; diff --git a/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/package-info.java b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/package-info.java similarity index 74% rename from examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/package-info.java rename to examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/package-info.java index 62a2ff870ee..6d132dbc7eb 100644 --- a/examples/integrations/neo4j/neo4j-se/src/main/java/io/helidon/examples/integrations/neo4j/se/package-info.java +++ b/examples/integrations/neo4j/src/main/java/io/helidon/examples/integrations/neo4j/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,9 @@ */ /** - * SE Neo4j demo application. + * Helidon Integrations Neo4j Example. *

* - * @see io.helidon.examples.integrations.neo4j.se.Main + * @see io.helidon.examples.integrations.neo4j.Main */ -package io.helidon.examples.integrations.neo4j.se; +package io.helidon.examples.integrations.neo4j; diff --git a/examples/integrations/neo4j/neo4j-se/src/main/resources/application.yaml b/examples/integrations/neo4j/src/main/resources/application.yaml similarity index 92% rename from examples/integrations/neo4j/neo4j-se/src/main/resources/application.yaml rename to examples/integrations/neo4j/src/main/resources/application.yaml index 9103d052e4f..63f95335305 100644 --- a/examples/integrations/neo4j/neo4j-se/src/main/resources/application.yaml +++ b/examples/integrations/neo4j/src/main/resources/application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2021 Oracle and/or its affiliates. +# Copyright (c) 2021, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/integrations/neo4j/neo4j-se/src/main/resources/logging.properties b/examples/integrations/neo4j/src/main/resources/logging.properties similarity index 90% rename from examples/integrations/neo4j/neo4j-se/src/main/resources/logging.properties rename to examples/integrations/neo4j/src/main/resources/logging.properties index 903826ad47f..164c69e8d0b 100644 --- a/examples/integrations/neo4j/neo4j-se/src/main/resources/logging.properties +++ b/examples/integrations/neo4j/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2021, 2022 Oracle and/or its affiliates. +# Copyright (c) 2021, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,8 +27,7 @@ java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$ .level=INFO # Component specific log levels -#io.helidon.reactive.webserver.level=INFO +#io.helidon.nima.webserver.level=INFO #io.helidon.config.level=INFO #io.helidon.security.level=INFO #io.helidon.common.level=INFO -#io.netty.level=INFO diff --git a/examples/integrations/neo4j/src/test/java/io/helidon/examples/integrations/neo4j/MainTest.java b/examples/integrations/neo4j/src/test/java/io/helidon/examples/integrations/neo4j/MainTest.java new file mode 100644 index 00000000000..68078f49857 --- /dev/null +++ b/examples/integrations/neo4j/src/test/java/io/helidon/examples/integrations/neo4j/MainTest.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.neo4j; + +import io.helidon.common.http.Http; +import io.helidon.nima.testing.junit5.webserver.ServerTest; +import io.helidon.nima.testing.junit5.webserver.SetUpRoute; +import io.helidon.nima.webclient.http1.Http1Client; +import io.helidon.nima.webclient.http1.Http1ClientResponse; +import io.helidon.nima.webserver.http.HttpRouting; + +import jakarta.json.JsonArray; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.neo4j.harness.Neo4j; +import org.neo4j.harness.Neo4jBuilders; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Main test class for Neo4j Helidon Nima app. + */ +@ServerTest +public class MainTest { + + private final Http1Client webClient; + + private static Neo4j embeddedDatabaseServer; + + public MainTest(Http1Client webClient) { + this.webClient = webClient; + } + + @SetUpRoute + static void routing(HttpRouting.Builder builder) { + //Setup embedded Neo4j Server and inject in routing + embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder() + .withDisabledServer() + .withFixture(FIXTURE) + .build(); + + System.setProperty("neo4j.uri", embeddedDatabaseServer.boltURI().toString()); + + Main.routing(builder); + } + + @BeforeAll + static void startServer() { + //Setup embedded Neo4j Server and inject in routing + embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder() + .withDisabledServer() + .withFixture(FIXTURE) + .build(); + + System.setProperty("neo4j.uri", embeddedDatabaseServer.boltURI().toString()); + } + + @AfterAll + static void stopServer() { + if (embeddedDatabaseServer != null) { + embeddedDatabaseServer.close(); + } + } + + @Test + void testMovies() { + JsonArray result = webClient.get("/api/movies").request(JsonArray.class); + assertThat(result.getJsonObject(0).getString("title"), containsString("The Matrix")); + } + + @Test + public void testHealth() { + try (Http1ClientResponse response = webClient.get("/observe/health").request()) { + assertThat(response.status(), is(Http.Status.NO_CONTENT_204)); + } + } + + static final String FIXTURE = """ + CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'}) + CREATE (Keanu:Person {name:'Keanu Reeves', born:1964}) + CREATE (Carrie:Person {name:'Carrie-Anne Moss', born:1967}) + CREATE (Laurence:Person {name:'Laurence Fishburne', born:1961}) + CREATE (Hugo:Person {name:'Hugo Weaving', born:1960}) + CREATE (LillyW:Person {name:'Lilly Wachowski', born:1967}) + CREATE (LanaW:Person {name:'Lana Wachowski', born:1965}) + CREATE (JoelS:Person {name:'Joel Silver', born:1952}) + CREATE (KevinB:Person {name:'Kevin Bacon', born:1958}) + CREATE + (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix), + (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrix), + (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrix), + (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrix), + (LillyW)-[:DIRECTED]->(TheMatrix), + (LanaW)-[:DIRECTED]->(TheMatrix), + (JoelS)-[:PRODUCED]->(TheMatrix) + + CREATE (Emil:Person {name:"Emil Eifrem", born:1978}) + CREATE (Emil)-[:ACTED_IN {roles:["Emil"]}]->(TheMatrix) + + CREATE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded', released:2003, tagline:'Free your mind'}) + CREATE + (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixReloaded), + (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixReloaded), + (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixReloaded), + (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixReloaded), + (LillyW)-[:DIRECTED]->(TheMatrixReloaded), + (LanaW)-[:DIRECTED]->(TheMatrixReloaded), + (JoelS)-[:PRODUCED]->(TheMatrixReloaded) + + CREATE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions', released:2003, + tagline:'Everything that has a beginning has an end'}) + CREATE + (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixRevolutions), + (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixRevolutions), + (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixRevolutions), + (KevinB)-[:ACTED_IN {roles:['Unknown']}]->(TheMatrixRevolutions), + (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixRevolutions), + (LillyW)-[:DIRECTED]->(TheMatrixRevolutions), + (LanaW)-[:DIRECTED]->(TheMatrixRevolutions), + (JoelS)-[:PRODUCED]->(TheMatrixRevolutions)"""; +} \ No newline at end of file diff --git a/examples/integrations/neo4j/src/test/java/io/helidon/examples/integrations/neo4j/package-info.java b/examples/integrations/neo4j/src/test/java/io/helidon/examples/integrations/neo4j/package-info.java new file mode 100644 index 00000000000..e9d169770e5 --- /dev/null +++ b/examples/integrations/neo4j/src/test/java/io/helidon/examples/integrations/neo4j/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests for Helidon Integrations Neo4j Example. + */ +package io.helidon.examples.integrations.neo4j; diff --git a/examples/integrations/oci/atp-cdi/pom.xml b/examples/integrations/oci/atp-cdi/pom.xml index 55c21d3b7de..3287dac3951 100644 --- a/examples/integrations/oci/atp-cdi/pom.xml +++ b/examples/integrations/oci/atp-cdi/pom.xml @@ -61,7 +61,7 @@ helidon-config-yaml-mp - org.jboss + io.smallrye jandex runtime @@ -79,7 +79,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/integrations/oci/atp-reactive/src/main/java/io/helidon/examples/integrations/oci/atp/reactive/OciAtpMain.java b/examples/integrations/oci/atp-reactive/src/main/java/io/helidon/examples/integrations/oci/atp/reactive/OciAtpMain.java deleted file mode 100644 index 91057ba8ed0..00000000000 --- a/examples/integrations/oci/atp-reactive/src/main/java/io/helidon/examples/integrations/oci/atp/reactive/OciAtpMain.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.examples.integrations.oci.atp.reactive; - -import java.io.IOException; - -import io.helidon.logging.common.LogConfig; -import io.helidon.config.Config; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.WebServer; - -import com.oracle.bmc.ConfigFileReader; -import com.oracle.bmc.auth.AuthenticationDetailsProvider; -import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; -import com.oracle.bmc.database.DatabaseAsync; -import com.oracle.bmc.database.DatabaseAsyncClient; -import com.oracle.bmc.model.BmcException; - -/** - * Main class of the example. - * This example sets up a web server to serve REST API to retrieve ATP wallet. - */ -public final class OciAtpMain { - /** - * Cannot be instantiated. - */ - private OciAtpMain() { - } - - /** - * Application main entry point. - * - * @param args command line arguments. - */ - public static void main(String[] args) throws IOException { - // load logging configuration - LogConfig.configureRuntime(); - - // By default this will pick up application.yaml from the classpath - Config config = Config.create(); - - // this requires OCI configuration in the usual place - // ~/.oci/config - AuthenticationDetailsProvider authProvider = new ConfigFileAuthenticationDetailsProvider(ConfigFileReader.parseDefault()); - DatabaseAsync databaseAsyncClient = DatabaseAsyncClient.builder().build(authProvider); - - // Prepare routing for the server - WebServer server = WebServer.builder() - .config(config.get("server")) - .routing(Routing.builder() - .register("/atp", new AtpService(databaseAsyncClient, config)) - // OCI SDK error handling - .error(BmcException.class, (req, res, ex) -> res.status(ex.getStatusCode()) - .send(ex.getMessage()))) - .build(); - - // Start the server and print some info. - server.start().thenAccept(ws -> { - System.out.println( - "WEB server is up! http://localhost:" + ws.port() + "/"); - }); - - // Server threads are not daemon. NO need to block. Just react. - server.whenShutdown().thenRun(() -> System.out.println("WEB server is DOWN. Good bye!")); - } -} diff --git a/examples/integrations/oci/atp-reactive/src/main/java/io/helidon/examples/integrations/oci/atp/reactive/OciResponseHandler.java b/examples/integrations/oci/atp-reactive/src/main/java/io/helidon/examples/integrations/oci/atp/reactive/OciResponseHandler.java deleted file mode 100644 index 0eac2a44fa5..00000000000 --- a/examples/integrations/oci/atp-reactive/src/main/java/io/helidon/examples/integrations/oci/atp/reactive/OciResponseHandler.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.examples.integrations.oci.atp.reactive; - -import java.util.concurrent.CountDownLatch; - -import com.oracle.bmc.responses.AsyncHandler; - -final class OciResponseHandler implements AsyncHandler { - private OUT item; - private Throwable failed = null; - private CountDownLatch latch = new CountDownLatch(1); - - protected OUT waitForCompletion() throws Exception { - latch.await(); - if (failed != null) { - if (failed instanceof Exception) { - throw (Exception) failed; - } - throw (Error) failed; - } - return item; - } - - @Override - public void onSuccess(IN request, OUT response) { - item = response; - latch.countDown(); - } - - @Override - public void onError(IN request, Throwable error) { - failed = error; - latch.countDown(); - } -} diff --git a/examples/integrations/oci/atp-reactive/src/main/java/io/helidon/examples/integrations/oci/atp/reactive/package-info.java b/examples/integrations/oci/atp-reactive/src/main/java/io/helidon/examples/integrations/oci/atp/reactive/package-info.java deleted file mode 100644 index ee7800316ed..00000000000 --- a/examples/integrations/oci/atp-reactive/src/main/java/io/helidon/examples/integrations/oci/atp/reactive/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2021 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Example of integration with OCI ATP in reactive application. - */ -package io.helidon.examples.integrations.oci.atp.reactive; diff --git a/examples/integrations/oci/atp-reactive/README.md b/examples/integrations/oci/atp/README.md similarity index 91% rename from examples/integrations/oci/atp-reactive/README.md rename to examples/integrations/oci/atp/README.md index 20b402558c1..bcba8b88a77 100644 --- a/examples/integrations/oci/atp-reactive/README.md +++ b/examples/integrations/oci/atp/README.md @@ -1,4 +1,4 @@ -# Helidon ATP Reactive Examples +# Helidon ATP Nima Examples This example demonstrates how user can easily retrieve wallet from their ATP instance running in OCI and use information from that wallet to setup DataSource to do Database operations. @@ -16,7 +16,7 @@ Once you have updated required properties, you can run the example: ```shell script mvn clean install -java -jar ./target/helidon-examples-integrations-oci-atp-reactive.jar +java -jar ./target/helidon-examples-integrations-oci-atp.jar ``` To verify that, you can retrieve wallet and do database operation: diff --git a/examples/integrations/oci/atp-reactive/pom.xml b/examples/integrations/oci/atp/pom.xml similarity index 86% rename from examples/integrations/oci/atp-reactive/pom.xml rename to examples/integrations/oci/atp/pom.xml index 90956f5db61..03856861c55 100644 --- a/examples/integrations/oci/atp-reactive/pom.xml +++ b/examples/integrations/oci/atp/pom.xml @@ -29,18 +29,18 @@ io.helidon.examples.integrations.oci - helidon-examples-integrations-oci-atp-reactive - Helidon Examples Integration OCI ATP Reactive - Reactive integration with OCI ATP. + helidon-examples-integrations-oci-atp + Helidon Examples Integration OCI ATP Nima + Nima integration with OCI ATP. - io.helidon.examples.integrations.oci.atp.reactive.OciAtpMain + io.helidon.examples.integrations.oci.atp.OciAtpMain - io.helidon.reactive.webserver - helidon-reactive-webserver + io.helidon.nima.webserver + helidon-nima-webserver io.helidon.reactive.dbclient diff --git a/examples/integrations/oci/atp-reactive/src/main/java/io/helidon/examples/integrations/oci/atp/reactive/AtpService.java b/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/AtpService.java similarity index 65% rename from examples/integrations/oci/atp-reactive/src/main/java/io/helidon/examples/integrations/oci/atp/reactive/AtpService.java rename to examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/AtpService.java index 9e0243c0cdc..fc2ce73e623 100644 --- a/examples/integrations/oci/atp-reactive/src/main/java/io/helidon/examples/integrations/oci/atp/reactive/AtpService.java +++ b/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/AtpService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.examples.integrations.oci.atp.reactive; +package io.helidon.examples.integrations.oci.atp; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; @@ -22,6 +22,7 @@ import java.nio.charset.StandardCharsets; import java.security.KeyStore; import java.sql.SQLException; +import java.time.Duration; import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.ZipEntry; @@ -32,16 +33,19 @@ import javax.net.ssl.TrustManagerFactory; import io.helidon.common.http.Http; -import io.helidon.common.reactive.Single; import io.helidon.config.Config; +import io.helidon.config.ConfigException; +import io.helidon.nima.webserver.http.HttpRules; +import io.helidon.nima.webserver.http.HttpService; +import io.helidon.nima.webserver.http.ServerRequest; +import io.helidon.nima.webserver.http.ServerResponse; import io.helidon.reactive.dbclient.DbClient; import io.helidon.reactive.dbclient.jdbc.JdbcDbClientProvider; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.ServerRequest; -import io.helidon.reactive.webserver.ServerResponse; -import io.helidon.reactive.webserver.Service; -import com.oracle.bmc.database.DatabaseAsync; +import com.oracle.bmc.ConfigFileReader; +import com.oracle.bmc.auth.AuthenticationDetailsProvider; +import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; +import com.oracle.bmc.database.DatabaseClient; import com.oracle.bmc.database.model.GenerateAutonomousDatabaseWalletDetails; import com.oracle.bmc.database.requests.GenerateAutonomousDatabaseWalletRequest; import com.oracle.bmc.database.responses.GenerateAutonomousDatabaseWalletResponse; @@ -50,45 +54,48 @@ import oracle.ucp.jdbc.PoolDataSource; import oracle.ucp.jdbc.PoolDataSourceFactory; -class AtpService implements Service { +class AtpService implements HttpService { private static final Logger LOGGER = Logger.getLogger(AtpService.class.getName()); - private final DatabaseAsync databaseAsyncClient; + private final DatabaseClient databaseClient; private final Config config; - AtpService(DatabaseAsync databaseAsyncClient, Config config) { - this.databaseAsyncClient = databaseAsyncClient; - this.config = config; + AtpService(Config config) { + try { + // this requires OCI configuration in the usual place + // ~/.oci/config + AuthenticationDetailsProvider authProvider = new ConfigFileAuthenticationDetailsProvider(ConfigFileReader.parseDefault()); + databaseClient = DatabaseClient.builder().build(authProvider); + this.config = config; + } catch (IOException e) { + throw new ConfigException("Failed to read configuration properties", e); + } } - @Override - public void update(Routing.Rules rules) { + /** + * A service registers itself by updating the routine rules. + * + * @param rules the routing rules. + */ + public void routing(HttpRules rules) { rules.get("/wallet", this::generateWallet); } /** * Generate wallet file for the configured ATP. + * + * @param req request + * @param res response */ private void generateWallet(ServerRequest req, ServerResponse res) { - OciResponseHandler walletHandler = - new OciResponseHandler<>(); - GenerateAutonomousDatabaseWalletResponse walletResponse = null; - try { - databaseAsyncClient.generateAutonomousDatabaseWallet( - GenerateAutonomousDatabaseWalletRequest.builder() - .autonomousDatabaseId(config.get("oci.atp.ocid").asString().get()) - .generateAutonomousDatabaseWalletDetails( - GenerateAutonomousDatabaseWalletDetails.builder() - .password(config.get("oci.atp.walletPassword").asString().get()) - .build()) - .build(), walletHandler); - walletResponse = walletHandler.waitForCompletion(); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Error waiting for GenerateAutonomousDatabaseWalletResponse", e); - res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send(); - return; - } + GenerateAutonomousDatabaseWalletResponse walletResponse = databaseClient.generateAutonomousDatabaseWallet( + GenerateAutonomousDatabaseWalletRequest.builder() + .autonomousDatabaseId(config.get("oci.atp.ocid").asString().get()) + .generateAutonomousDatabaseWalletDetails( + GenerateAutonomousDatabaseWalletDetails.builder() + .password(config.get("oci.atp.walletPassword").asString().get()) + .build()) + .build()); if (walletResponse.getContentLength() == 0) { LOGGER.log(Level.SEVERE, "GenerateAutonomousDatabaseWalletResponse is empty"); @@ -101,20 +108,27 @@ private void generateWallet(ServerRequest req, ServerResponse res) { walletContent = walletResponse.getInputStream().readAllBytes(); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Error processing GenerateAutonomousDatabaseWalletResponse", e); - res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send(); + res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send(e.getMessage()); return; } - createDbClient(walletContent) - .flatMap(dbClient -> dbClient.execute(exec -> exec.query("SELECT 'Hello world!!' FROM DUAL"))) - .first() - .map(dbRow -> dbRow.column(1).as(String.class)) - .ifEmpty(() -> res.status(404).send()) - .onError(res::send) - .forSingle(res::send); + try { + String result = createDbClient(walletContent) + .execute(exec -> exec.query("SELECT 'Hello world!!' FROM DUAL")) + .first() + .map(dbRow -> dbRow.column(1).as(String.class)) + .await(Duration.ofSeconds(60)); + if (result == null || result.isEmpty()) { + res.status(Http.Status.NOT_FOUND_404).send(); + } else { + res.send(result); + } + } catch (Exception e) { + res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send(e.getMessage()); + } } - Single createDbClient(byte[] walletContent) { + DbClient createDbClient(byte[] walletContent) { PoolDataSource pds = PoolDataSourceFactory.getPoolDataSource(); try { pds.setSSLContext(getSSLContext(walletContent)); @@ -123,22 +137,23 @@ Single createDbClient(byte[] walletContent) { .orElseThrow(() -> new IllegalStateException("Missing tnsNetServiceName!!")))); pds.setUser(config.get("db.userName").as(String.class).orElse("ADMIN")); pds.setPassword(config.get("db.password") - .as(String.class) - .orElseThrow(() -> new IllegalStateException("Missing password!!"))); + .as(String.class) + .orElseThrow(() -> new IllegalStateException("Missing password!!"))); pds.setConnectionFactoryClassName(OracleDataSource.class.getName()); } catch (SQLException e) { LOGGER.log(Level.SEVERE, "Error setting up PoolDataSource", e); - return Single.error(e); + throw new RuntimeException(e); } - return Single.just(new JdbcDbClientProvider().builder() - .connectionPool(() -> { - try { - return pds.getConnection(); - } catch (SQLException e) { - throw new IllegalStateException("Error while setting up new connection", e); - } - }) - .build()); + + return new JdbcDbClientProvider().builder() + .connectionPool(() -> { + try { + return pds.getConnection(); + } catch (SQLException e) { + throw new IllegalStateException("Error while setting up new connection", e); + } + }) + .build(); } /** @@ -174,8 +189,8 @@ private static SSLContext getSSLContext(byte[] walletContent) throws IllegalStat /** * Returns JDBC URL with connection description for the given service based on tnsnames.ora in wallet. * - * @param walletContent - * @param tnsNetServiceName + * @param walletContent walletContent + * @param tnsNetServiceName tnsNetServiceName * @return String */ private static String getJdbcUrl(byte[] walletContent, String tnsNetServiceName) throws IllegalStateException { diff --git a/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/OciAtpMain.java b/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/OciAtpMain.java new file mode 100644 index 00000000000..e63672babe9 --- /dev/null +++ b/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/OciAtpMain.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.atp; + +import java.io.IOException; + +import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http.HttpRouting; + +/** + * Main class of the example. + * This example sets up a web server to serve REST API to retrieve ATP wallet. + */ +public final class OciAtpMain { + + private static Config config; + + /** + * Cannot be instantiated. + */ + private OciAtpMain() { + } + + /** + * Application main entry point. + * + * @param args command line arguments. + */ + public static void main(String[] args) throws IOException { + // load logging configuration + LogConfig.configureRuntime(); + + // By default this will pick up application.yaml from the classpath + config = Config.create(); + + WebServer server = WebServer.builder() + .routing(OciAtpMain::routing) + .config(config.get("server")) + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port()); + } + + /** + * Updates HTTP Routing. + */ + static void routing(HttpRouting.Builder routing) { + AtpService atpService = new AtpService(config); + routing.register("/atp", atpService); + } +} diff --git a/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/package-info.java b/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/package-info.java new file mode 100644 index 00000000000..35e3363181b --- /dev/null +++ b/examples/integrations/oci/atp/src/main/java/io/helidon/examples/integrations/oci/atp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of integration with OCI ATP in Nima application. + */ +package io.helidon.examples.integrations.oci.atp; diff --git a/examples/integrations/oci/atp-reactive/src/main/resources/application.yaml b/examples/integrations/oci/atp/src/main/resources/application.yaml similarity index 94% rename from examples/integrations/oci/atp-reactive/src/main/resources/application.yaml rename to examples/integrations/oci/atp/src/main/resources/application.yaml index 156f26ed159..b65d90eccd2 100644 --- a/examples/integrations/oci/atp-reactive/src/main/resources/application.yaml +++ b/examples/integrations/oci/atp/src/main/resources/application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2021, 2022 Oracle and/or its affiliates. +# Copyright (c) 2021, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/integrations/oci/objectstorage-reactive/src/main/resources/logging.properties b/examples/integrations/oci/atp/src/main/resources/logging.properties similarity index 94% rename from examples/integrations/oci/objectstorage-reactive/src/main/resources/logging.properties rename to examples/integrations/oci/atp/src/main/resources/logging.properties index 43cd0f29d7d..c09e76d612c 100644 --- a/examples/integrations/oci/objectstorage-reactive/src/main/resources/logging.properties +++ b/examples/integrations/oci/atp/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2021, 2022 Oracle and/or its affiliates. +# Copyright (c) 2021, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/integrations/oci/metrics-reactive/pom.xml b/examples/integrations/oci/metrics/pom.xml similarity index 84% rename from examples/integrations/oci/metrics-reactive/pom.xml rename to examples/integrations/oci/metrics/pom.xml index 84d18bb3c70..0acc6710b1a 100644 --- a/examples/integrations/oci/metrics-reactive/pom.xml +++ b/examples/integrations/oci/metrics/pom.xml @@ -29,19 +29,15 @@ - io.helidon.examples.integrations.oci.telemetry.reactive.OciMetricsMain + io.helidon.examples.integrations.oci.telemetry.OciMetricsMain io.helidon.examples.integrations.oci - helidon-examples-integrations-oci-metrics-reactive - Helidon Examples Integration OCI Metrics Reactive - Reactive integration with OCI Metrics. + helidon-examples-integrations-oci-metrics + Helidon Examples Integration OCI Metrics + Integration with OCI Metrics. - - io.helidon.reactive.webserver - helidon-reactive-webserver - io.helidon.config helidon-config-yaml diff --git a/examples/integrations/oci/metrics-reactive/src/main/java/io/helidon/examples/integrations/oci/telemetry/reactive/OciMetricsMain.java b/examples/integrations/oci/metrics/src/main/java/io/helidon/examples/integrations/oci/telemetry/OciMetricsMain.java similarity index 61% rename from examples/integrations/oci/metrics-reactive/src/main/java/io/helidon/examples/integrations/oci/telemetry/reactive/OciMetricsMain.java rename to examples/integrations/oci/metrics/src/main/java/io/helidon/examples/integrations/oci/telemetry/OciMetricsMain.java index a7073137da8..77e0ee9282c 100644 --- a/examples/integrations/oci/metrics-reactive/src/main/java/io/helidon/examples/integrations/oci/telemetry/reactive/OciMetricsMain.java +++ b/examples/integrations/oci/metrics/src/main/java/io/helidon/examples/integrations/oci/telemetry/OciMetricsMain.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.examples.integrations.oci.telemetry.reactive; +package io.helidon.examples.integrations.oci.telemetry; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -22,24 +22,18 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.CountDownLatch; -import io.helidon.logging.common.LogConfig; import io.helidon.config.Config; +import io.helidon.logging.common.LogConfig; import com.oracle.bmc.ConfigFileReader; import com.oracle.bmc.auth.AuthenticationDetailsProvider; import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; -import com.oracle.bmc.monitoring.MonitoringAsync; -import com.oracle.bmc.monitoring.MonitoringAsyncClient; -import com.oracle.bmc.monitoring.model.Datapoint; -import com.oracle.bmc.monitoring.model.FailedMetricRecord; -import com.oracle.bmc.monitoring.model.MetricDataDetails; -import com.oracle.bmc.monitoring.model.PostMetricDataDetails; -import com.oracle.bmc.monitoring.model.PostMetricDataResponseDetails; +import com.oracle.bmc.monitoring.Monitoring; +import com.oracle.bmc.monitoring.MonitoringClient; +import com.oracle.bmc.monitoring.model.*; import com.oracle.bmc.monitoring.requests.PostMetricDataRequest; import com.oracle.bmc.monitoring.responses.PostMetricDataResponse; -import com.oracle.bmc.responses.AsyncHandler; import static io.helidon.config.ConfigSources.classpath; import static io.helidon.config.ConfigSources.file; @@ -54,6 +48,7 @@ private OciMetricsMain() { /** * Main method. + * * @param args ignored */ public static void main(String[] args) throws Exception { @@ -67,29 +62,23 @@ public static void main(String[] args) throws Exception { // this requires OCI configuration in the usual place // ~/.oci/config AuthenticationDetailsProvider authProvider = new ConfigFileAuthenticationDetailsProvider(ConfigFileReader.parseDefault()); - MonitoringAsync monitoringAsyncClient = new MonitoringAsyncClient(authProvider); - monitoringAsyncClient.setEndpoint(monitoringAsyncClient.getEndpoint().replace("telemetry.", "telemetry-ingestion.")); - - PostMetricDataRequest postMetricDataRequest = PostMetricDataRequest.builder() - .postMetricDataDetails(getPostMetricDataDetails(config)) - .build(); - /* - * Invoke the API call. I use .await() to block the call, as otherwise our - * main method would finish without waiting for the response. - * In a real reactive application, this should not be done (as you would write the response - * to a server response or use other reactive/non-blocking APIs). - */ - ResponseHandler monitoringHandler = - new ResponseHandler<>(); - monitoringAsyncClient.postMetricData(postMetricDataRequest, monitoringHandler); - PostMetricDataResponse postMetricDataResponse = monitoringHandler.waitForCompletion(); - PostMetricDataResponseDetails postMetricDataResponseDetails = postMetricDataResponse.getPostMetricDataResponseDetails(); - int count = postMetricDataResponseDetails.getFailedMetricsCount(); - System.out.println("Failed count: " + count); - if (count > 0) { - System.out.println("Failed metrics:"); - for (FailedMetricRecord failedMetric : postMetricDataResponseDetails.getFailedMetrics()) { - System.out.println("\t" + failedMetric.getMessage() + ": " + failedMetric.getMetricData()); + try (Monitoring monitoringClient = MonitoringClient.builder().build(authProvider)) { + monitoringClient.setEndpoint(monitoringClient.getEndpoint().replace("telemetry.", "telemetry-ingestion.")); + + PostMetricDataRequest postMetricDataRequest = PostMetricDataRequest.builder() + .postMetricDataDetails(getPostMetricDataDetails(config)) + .build(); + + // Invoke the API call. + PostMetricDataResponse postMetricDataResponse = monitoringClient.postMetricData(postMetricDataRequest); + PostMetricDataResponseDetails postMetricDataResponseDetails = postMetricDataResponse.getPostMetricDataResponseDetails(); + int count = postMetricDataResponseDetails.getFailedMetricsCount(); + System.out.println("Failed count: " + count); + if (count > 0) { + System.out.println("Failed metrics:"); + for (FailedMetricRecord failedMetric : postMetricDataResponseDetails.getFailedMetrics()) { + System.out.println("\t" + failedMetric.getMessage() + ": " + failedMetric.getMetricData()); + } } } } @@ -143,33 +132,4 @@ private static Map makeMap(String... data) { } return map; } - - private static class ResponseHandler implements AsyncHandler { - private OUT item; - private Throwable failed = null; - private CountDownLatch latch = new CountDownLatch(1); - - private OUT waitForCompletion() throws Exception { - latch.await(); - if (failed != null) { - if (failed instanceof Exception) { - throw (Exception) failed; - } - throw (Error) failed; - } - return item; - } - - @Override - public void onSuccess(IN request, OUT response) { - item = response; - latch.countDown(); - } - - @Override - public void onError(IN request, Throwable error) { - failed = error; - latch.countDown(); - } - } } diff --git a/examples/integrations/oci/metrics/src/main/java/io/helidon/examples/integrations/oci/telemetry/package-info.java b/examples/integrations/oci/metrics/src/main/java/io/helidon/examples/integrations/oci/telemetry/package-info.java new file mode 100644 index 00000000000..3fc15dd7d8a --- /dev/null +++ b/examples/integrations/oci/metrics/src/main/java/io/helidon/examples/integrations/oci/telemetry/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example using OCI metrics blocking API. + */ +package io.helidon.examples.integrations.oci.telemetry; diff --git a/examples/integrations/oci/metrics-reactive/src/main/resources/application.yaml b/examples/integrations/oci/metrics/src/main/resources/application.yaml similarity index 92% rename from examples/integrations/oci/metrics-reactive/src/main/resources/application.yaml rename to examples/integrations/oci/metrics/src/main/resources/application.yaml index d9304c7641e..c2c669e26ca 100644 --- a/examples/integrations/oci/metrics-reactive/src/main/resources/application.yaml +++ b/examples/integrations/oci/metrics/src/main/resources/application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2021 Oracle and/or its affiliates. +# Copyright (c) 2021, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/integrations/oci/vault-reactive/src/main/resources/logging.properties b/examples/integrations/oci/metrics/src/main/resources/logging.properties similarity index 94% rename from examples/integrations/oci/vault-reactive/src/main/resources/logging.properties rename to examples/integrations/oci/metrics/src/main/resources/logging.properties index f8802f90626..341cef8ce9e 100644 --- a/examples/integrations/oci/vault-reactive/src/main/resources/logging.properties +++ b/examples/integrations/oci/metrics/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2021, 2022 Oracle and/or its affiliates. +# Copyright (c) 2021, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/integrations/oci/objectstorage-cdi/pom.xml b/examples/integrations/oci/objectstorage-cdi/pom.xml index fbe0121e50c..cd80c3664ac 100644 --- a/examples/integrations/oci/objectstorage-cdi/pom.xml +++ b/examples/integrations/oci/objectstorage-cdi/pom.xml @@ -55,7 +55,7 @@ helidon-config-yaml-mp - org.jboss + io.smallrye jandex runtime @@ -73,7 +73,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/integrations/oci/objectstorage-reactive/src/main/java/io/helidon/examples/integrations/oci/objecstorage/reactive/ObjectStorageService.java b/examples/integrations/oci/objectstorage-reactive/src/main/java/io/helidon/examples/integrations/oci/objecstorage/reactive/ObjectStorageService.java deleted file mode 100644 index c4cbbe25694..00000000000 --- a/examples/integrations/oci/objectstorage-reactive/src/main/java/io/helidon/examples/integrations/oci/objecstorage/reactive/ObjectStorageService.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.examples.integrations.oci.objecstorage.reactive; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.concurrent.CountDownLatch; -import java.util.logging.Level; -import java.util.logging.Logger; - -import io.helidon.common.http.Http; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.ServerRequest; -import io.helidon.reactive.webserver.ServerResponse; -import io.helidon.reactive.webserver.Service; - -import com.oracle.bmc.objectstorage.ObjectStorageAsync; -import com.oracle.bmc.objectstorage.model.RenameObjectDetails; -import com.oracle.bmc.objectstorage.requests.DeleteObjectRequest; -import com.oracle.bmc.objectstorage.requests.GetNamespaceRequest; -import com.oracle.bmc.objectstorage.requests.GetObjectRequest; -import com.oracle.bmc.objectstorage.requests.PutObjectRequest; -import com.oracle.bmc.objectstorage.requests.RenameObjectRequest; -import com.oracle.bmc.objectstorage.responses.DeleteObjectResponse; -import com.oracle.bmc.objectstorage.responses.GetNamespaceResponse; -import com.oracle.bmc.objectstorage.responses.GetObjectResponse; -import com.oracle.bmc.objectstorage.responses.PutObjectResponse; -import com.oracle.bmc.objectstorage.responses.RenameObjectResponse; -import com.oracle.bmc.responses.AsyncHandler; - -class ObjectStorageService implements Service { - private static final Logger LOGGER = Logger.getLogger(ObjectStorageService.class.getName()); - private final ObjectStorageAsync objectStorageAsyncClient; - private final String bucketName; - private final String namespaceName; - - ObjectStorageService(ObjectStorageAsync objectStorageAsyncClient, String bucketName) throws Exception { - this.objectStorageAsyncClient = objectStorageAsyncClient; - this.bucketName = bucketName; - ResponseHandler namespaceHandler = - new ResponseHandler<>(); - this.objectStorageAsyncClient.getNamespace(GetNamespaceRequest.builder().build(), namespaceHandler); - GetNamespaceResponse namespaceResponse = namespaceHandler.waitForCompletion(); - this.namespaceName = namespaceResponse.getValue(); - } - - @Override - public void update(Routing.Rules rules) { - rules.get("/file/{file-name}", this::download) - .post("/file/{file-name}", this::upload) - .delete("/file/{file-name}", this::delete) - .get("/rename/{old-name}/{new-name}", this::rename); - } - - private void delete(ServerRequest req, ServerResponse res) { - String objectName = req.path().param("file-name"); - - ResponseHandler deleteObjectHandler = - new ResponseHandler<>(); - - objectStorageAsyncClient.deleteObject(DeleteObjectRequest.builder() - .namespaceName(namespaceName) - .bucketName(bucketName) - .objectName(objectName).build(), deleteObjectHandler); - try { - DeleteObjectResponse deleteObjectResponse = deleteObjectHandler.waitForCompletion(); - res.status(Http.Status.OK_200) - .send(); - return; - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Error deleting object", e); - res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send(); - return; - } - } - - private void rename(ServerRequest req, ServerResponse res) { - String oldName = req.path().param("old-name"); - String newName = req.path().param("new-name"); - - RenameObjectRequest renameObjectRequest = RenameObjectRequest.builder() - .namespaceName(namespaceName) - .bucketName(bucketName) - .renameObjectDetails(RenameObjectDetails.builder() - .newName(newName) - .sourceName(oldName) - .build()) - .build(); - - ResponseHandler renameObjectHandler = - new ResponseHandler<>(); - - try { - objectStorageAsyncClient.renameObject(renameObjectRequest, renameObjectHandler); - RenameObjectResponse renameObjectResponse = renameObjectHandler.waitForCompletion(); - res.status(Http.Status.OK_200) - .send(); - return; - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Error renaming object", e); - res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send(); - return; - } - } - - private void upload(ServerRequest req, ServerResponse res) { - String objectName = req.path().param("file-name"); - PutObjectRequest putObjectRequest = null; - try (InputStream stream = new FileInputStream(System.getProperty("user.dir") + File.separator + objectName)) { - byte[] contents = stream.readAllBytes(); - putObjectRequest = - PutObjectRequest.builder() - .namespaceName(namespaceName) - .bucketName(bucketName) - .objectName(objectName) - .putObjectBody(new ByteArrayInputStream(contents)) - .contentLength(Long.valueOf(contents.length)) - .build(); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Error creating PutObjectRequest", e); - res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send(); - return; - } - - ResponseHandler putObjectHandler = - new ResponseHandler<>(); - - try { - objectStorageAsyncClient.putObject(putObjectRequest, putObjectHandler); - PutObjectResponse putObjectResponse = putObjectHandler.waitForCompletion(); - res.status(Http.Status.OK_200).send(); - return; - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Error uploading object", e); - res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send(); - return; - } - } - - private void download(ServerRequest req, ServerResponse res) { - String objectName = req.path().param("file-name"); - ResponseHandler objectHandler = - new ResponseHandler<>(); - GetObjectRequest getObjectRequest = - GetObjectRequest.builder() - .namespaceName(namespaceName) - .bucketName(bucketName) - .objectName(objectName) - .build(); - GetObjectResponse getObjectResponse = null; - try { - objectStorageAsyncClient.getObject(getObjectRequest, objectHandler); - getObjectResponse = objectHandler.waitForCompletion(); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Error getting object", e); - res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send(); - return; - } - - if (getObjectResponse.getContentLength() == 0) { - LOGGER.log(Level.SEVERE, "GetObjectResponse is empty"); - res.status(Http.Status.NOT_FOUND_404).send(); - return; - } - - try (InputStream fileStream = getObjectResponse.getInputStream()) { - byte[] objectContent = fileStream.readAllBytes(); - res.addHeader(Http.Header.CONTENT_DISPOSITION, "attachment; filename=\"" + objectName + "\"") - .status(Http.Status.OK_200).send(objectContent); - return; - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "Error processing GetObjectResponse", e); - res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send(); - return; - } - } - - private static class ResponseHandler implements AsyncHandler { - private OUT item; - private Throwable failed = null; - private CountDownLatch latch = new CountDownLatch(1); - - private OUT waitForCompletion() throws Exception { - latch.await(); - if (failed != null) { - if (failed instanceof Exception) { - throw (Exception) failed; - } - throw (Error) failed; - } - return item; - } - - @Override - public void onSuccess(IN request, OUT response) { - item = response; - latch.countDown(); - } - - @Override - public void onError(IN request, Throwable error) { - failed = error; - latch.countDown(); - } - } -} diff --git a/examples/integrations/oci/objectstorage-reactive/src/main/java/io/helidon/examples/integrations/oci/objecstorage/reactive/OciObjectStorageMain.java b/examples/integrations/oci/objectstorage-reactive/src/main/java/io/helidon/examples/integrations/oci/objecstorage/reactive/OciObjectStorageMain.java deleted file mode 100644 index 4abb8893af3..00000000000 --- a/examples/integrations/oci/objectstorage-reactive/src/main/java/io/helidon/examples/integrations/oci/objecstorage/reactive/OciObjectStorageMain.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.examples.integrations.oci.objecstorage.reactive; - -import io.helidon.logging.common.LogConfig; -import io.helidon.config.Config; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.WebServer; - -import com.oracle.bmc.ConfigFileReader; -import com.oracle.bmc.auth.AuthenticationDetailsProvider; -import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; -import com.oracle.bmc.model.BmcException; -import com.oracle.bmc.objectstorage.ObjectStorageAsync; -import com.oracle.bmc.objectstorage.ObjectStorageAsyncClient; - -import static io.helidon.config.ConfigSources.classpath; -import static io.helidon.config.ConfigSources.file; - -/** - * Main class of the example. - * This example sets up a web server to serve REST API to upload/download/delete objects. - */ -public final class OciObjectStorageMain { - private OciObjectStorageMain() { - } - - /** - * Main method. - * - * @param args ignored - */ - public static void main(String[] args) throws Exception { - LogConfig.configureRuntime(); - // as I cannot share my configuration of OCI, let's combine the configuration - // from my home directory with the one compiled into the jar - // when running this example, you can either update the application.yaml in resources directory - // or use the same approach - Config config = buildConfig(); - - Config ociConfig = config.get("oci"); - - // this requires OCI configuration in the usual place - // ~/.oci/config - AuthenticationDetailsProvider authProvider = new ConfigFileAuthenticationDetailsProvider(ConfigFileReader.parseDefault()); - ObjectStorageAsync objectStorageAsyncClient = new ObjectStorageAsyncClient(authProvider); - - // the following parameters are required - String bucketName = ociConfig.get("objectstorage").get("bucketName").asString().get(); - - WebServer.builder() - .config(config.get("server")) - .routing(Routing.builder() - .register("/files", new ObjectStorageService(objectStorageAsyncClient, bucketName)) - // OCI SDK error handling - .error(BmcException.class, (req, res, ex) -> res.status(ex.getStatusCode()) - .send(ex.getMessage()))) - .build() - .start() - .await(); - } - - private static Config buildConfig() { - return Config.builder() - .sources( - // you can use this file to override the defaults that are built-in - file(System.getProperty("user.home") + "/helidon/conf/examples.yaml").optional(), - // in jar file (see src/main/resources/application.yaml) - classpath("application.yaml")) - .build(); - } -} diff --git a/examples/integrations/oci/objectstorage-reactive/src/main/java/io/helidon/examples/integrations/oci/objecstorage/reactive/package-info.java b/examples/integrations/oci/objectstorage-reactive/src/main/java/io/helidon/examples/integrations/oci/objecstorage/reactive/package-info.java deleted file mode 100644 index 3bda523337e..00000000000 --- a/examples/integrations/oci/objectstorage-reactive/src/main/java/io/helidon/examples/integrations/oci/objecstorage/reactive/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2021 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Example of integration with OCI object storage in reactive application. - */ -package io.helidon.examples.integrations.oci.objecstorage.reactive; diff --git a/examples/integrations/oci/objectstorage-reactive/pom.xml b/examples/integrations/oci/objectstorage/pom.xml similarity index 85% rename from examples/integrations/oci/objectstorage-reactive/pom.xml rename to examples/integrations/oci/objectstorage/pom.xml index 62cdac5b41c..f7df51ccec8 100644 --- a/examples/integrations/oci/objectstorage-reactive/pom.xml +++ b/examples/integrations/oci/objectstorage/pom.xml @@ -29,18 +29,18 @@ io.helidon.examples.integrations.oci - helidon-examples-integrations-oci-objectstorage-reactive - Helidon Examples Integration OCI Object Storage Reactive - Reactive integration with OCI Object Storage. + helidon-examples-integrations-oci-objectstorage + Helidon Examples Integration OCI Object Storage + Nima integration with OCI Object Storage. - io.helidon.examples.integrations.oci.objecstorage.reactive.OciObjectStorageMain + io.helidon.examples.integrations.oci.objecstorage.OciObjectStorageMain - io.helidon.reactive.webserver - helidon-reactive-webserver + io.helidon.nima.webserver + helidon-nima-webserver io.helidon.config diff --git a/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/ObjectStorageService.java b/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/ObjectStorageService.java new file mode 100644 index 00000000000..da42cb4e428 --- /dev/null +++ b/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/ObjectStorageService.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.objecstorage; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.common.http.Http; +import io.helidon.config.Config; +import io.helidon.config.ConfigException; +import io.helidon.nima.webserver.http.HttpRules; +import io.helidon.nima.webserver.http.HttpService; +import io.helidon.nima.webserver.http.ServerRequest; +import io.helidon.nima.webserver.http.ServerResponse; + +import com.oracle.bmc.ConfigFileReader; +import com.oracle.bmc.auth.AuthenticationDetailsProvider; +import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; +import com.oracle.bmc.objectstorage.ObjectStorage; +import com.oracle.bmc.objectstorage.ObjectStorageClient; +import com.oracle.bmc.objectstorage.requests.DeleteObjectRequest; +import com.oracle.bmc.objectstorage.requests.GetNamespaceRequest; +import com.oracle.bmc.objectstorage.requests.GetObjectRequest; +import com.oracle.bmc.objectstorage.requests.PutObjectRequest; +import com.oracle.bmc.objectstorage.responses.DeleteObjectResponse; +import com.oracle.bmc.objectstorage.responses.GetNamespaceResponse; +import com.oracle.bmc.objectstorage.responses.GetObjectResponse; +import com.oracle.bmc.objectstorage.responses.PutObjectResponse; + +/** + * REST API for the objecstorage example. + */ +public class ObjectStorageService implements HttpService { + private static final Logger LOGGER = Logger.getLogger(ObjectStorageService.class.getName()); + private final ObjectStorage objectStorageClient; + private final String namespaceName; + private final String bucketName; + + + ObjectStorageService(Config config) { + try { + AuthenticationDetailsProvider authProvider = new ConfigFileAuthenticationDetailsProvider(ConfigFileReader.parseDefault()); + this.objectStorageClient = ObjectStorageClient.builder().build(authProvider); + this.bucketName = config.get("oci.objectstorage.bucketName") + .asString() + .orElseThrow(() -> new IllegalStateException("Missing bucket name!!")); + GetNamespaceResponse namespaceResponse = + this.objectStorageClient.getNamespace(GetNamespaceRequest.builder().build()); + this.namespaceName = namespaceResponse.getValue(); + } catch (IOException e) { + throw new ConfigException("Failed to read configuration properties", e); + } + } + + /** + * A service registers itself by updating the routine rules. + * + * @param rules the routing rules. + */ + public void routing(HttpRules rules) { + rules.get("/file/{file-name}", this::download); + rules.post("/file/{fileName}", this::upload); + rules.delete("/file/{file-name}", this::delete); + } + + /** + * Download a file from object storage. + * + * @param request request + * @param response response + */ + public void download(ServerRequest request, ServerResponse response) { + String fileName = request.path().pathParameters().value("file-name"); + GetObjectResponse getObjectResponse = + objectStorageClient.getObject( + GetObjectRequest.builder() + .namespaceName(namespaceName) + .bucketName(bucketName) + .objectName(fileName) + .build()); + + if (getObjectResponse.getContentLength() == 0) { + LOGGER.log(Level.SEVERE, "GetObjectResponse is empty"); + response.status(Http.Status.NOT_FOUND_404).send(); + return; + } + + try (InputStream fileStream = getObjectResponse.getInputStream()) { + byte[] objectContent = fileStream.readAllBytes(); + response + .status(Http.Status.OK_200) + .header(Http.Header.CONTENT_DISPOSITION.defaultCase(), "attachment; filename=\"" + fileName + "\"") + .header("opc-request-id", getObjectResponse.getOpcRequestId()) + .header(Http.Header.CONTENT_LENGTH.defaultCase(), getObjectResponse.getContentLength().toString()); + + response.send(objectContent); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error processing GetObjectResponse", e); + response.status(Http.Status.INTERNAL_SERVER_ERROR_500).send(); + } + } + + /** + * Upload a file to object storage. + * + * @param request request + * @param response response + */ + public void upload(ServerRequest request, ServerResponse response) { + String fileName = request.path().pathParameters().value("fileName"); + PutObjectRequest putObjectRequest = null; + try (InputStream stream = new FileInputStream(System.getProperty("user.dir") + File.separator + fileName)) { + byte[] contents = stream.readAllBytes(); + putObjectRequest = + PutObjectRequest.builder() + .namespaceName(namespaceName) + .bucketName(bucketName) + .objectName(fileName) + .putObjectBody(new ByteArrayInputStream(contents)) + .contentLength(Long.valueOf(contents.length)) + .build(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error creating PutObjectRequest", e); + response.status(Http.Status.INTERNAL_SERVER_ERROR_500).send(); + return; + } + PutObjectResponse putObjectResponse = objectStorageClient.putObject(putObjectRequest); + + response.status(Http.Status.OK_200).header("opc-request-id", putObjectResponse.getOpcRequestId()); + + response.send(); + } + + /** + * Delete a file from object storage. + * + * @param request request + * @param response response + */ + public void delete(ServerRequest request, ServerResponse response) { + String fileName = request.path().pathParameters().value("file-name"); + DeleteObjectResponse deleteObjectResponse = objectStorageClient.deleteObject(DeleteObjectRequest.builder() + .namespaceName(namespaceName) + .bucketName(bucketName) + .objectName(fileName) + .build()); + response.status(Http.Status.OK_200).header("opc-request-id", deleteObjectResponse.getOpcRequestId()); + + response.send(); + } +} diff --git a/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/OciObjectStorageMain.java b/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/OciObjectStorageMain.java new file mode 100644 index 00000000000..16a882fa870 --- /dev/null +++ b/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/OciObjectStorageMain.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.objecstorage; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import io.helidon.config.Config; +import io.helidon.config.spi.ConfigSource; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http.HttpRouting; + +import static io.helidon.config.ConfigSources.classpath; +import static io.helidon.config.ConfigSources.file; + +/** + * Main class of the example. + * This example sets up a web server to serve REST API to upload/download/delete objects. + */ +public final class OciObjectStorageMain { + + private static Config config; + + private OciObjectStorageMain() { + } + + /** + * Main method. + * + * @param args ignored + */ + public static void main(String[] args) { + + config = Config + .builder() + .sources(examplesConfig()) + .build(); + + WebServer server = WebServer.builder() + .routing(OciObjectStorageMain::routing) + .config(config.get("server")) + .start(); + } + + /** + * Updates HTTP Routing. + */ + static void routing(HttpRouting.Builder routing) { + ObjectStorageService objectStorageService = new ObjectStorageService(config); + routing.register("/files", objectStorageService); + } + + private static List> examplesConfig() { + List> suppliers = new ArrayList<>(); + Path path = Paths.get(System.getProperty("user.home") + "/helidon/conf/examples.yaml"); + if (Files.exists(path)) { + suppliers.add(file(path).build()); + } + suppliers.add(classpath("application.yaml").build()); + return suppliers; + } +} diff --git a/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/package-info.java b/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/package-info.java new file mode 100644 index 00000000000..1a91fa72fe8 --- /dev/null +++ b/examples/integrations/oci/objectstorage/src/main/java/io/helidon/examples/integrations/oci/objecstorage/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of integration with OCI object storage in Nima application. + */ +package io.helidon.examples.integrations.oci.objecstorage; diff --git a/examples/integrations/oci/objectstorage-reactive/src/main/resources/application.yaml b/examples/integrations/oci/objectstorage/src/main/resources/application.yaml similarity index 92% rename from examples/integrations/oci/objectstorage-reactive/src/main/resources/application.yaml rename to examples/integrations/oci/objectstorage/src/main/resources/application.yaml index 4c07c154bc6..75e202bd486 100644 --- a/examples/integrations/oci/objectstorage-reactive/src/main/resources/application.yaml +++ b/examples/integrations/oci/objectstorage/src/main/resources/application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2021, 2022 Oracle and/or its affiliates. +# Copyright (c) 2021, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/integrations/oci/atp-reactive/src/main/resources/logging.properties b/examples/integrations/oci/objectstorage/src/main/resources/logging.properties similarity index 94% rename from examples/integrations/oci/atp-reactive/src/main/resources/logging.properties rename to examples/integrations/oci/objectstorage/src/main/resources/logging.properties index 43cd0f29d7d..c09e76d612c 100644 --- a/examples/integrations/oci/atp-reactive/src/main/resources/logging.properties +++ b/examples/integrations/oci/objectstorage/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2021, 2022 Oracle and/or its affiliates. +# Copyright (c) 2021, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/integrations/oci/pom.xml b/examples/integrations/oci/pom.xml index 586da771869..e896103ba23 100644 --- a/examples/integrations/oci/pom.xml +++ b/examples/integrations/oci/pom.xml @@ -33,12 +33,12 @@ Examples of integration with OCI (Oracle Cloud). - atp-reactive + atp atp-cdi - metrics-reactive - objectstorage-reactive + metrics + objectstorage objectstorage-cdi - vault-reactive + vault vault-cdi diff --git a/examples/integrations/oci/vault-cdi/pom.xml b/examples/integrations/oci/vault-cdi/pom.xml index e419c413b6e..43823bd348c 100644 --- a/examples/integrations/oci/vault-cdi/pom.xml +++ b/examples/integrations/oci/vault-cdi/pom.xml @@ -87,7 +87,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/integrations/oci/vault-reactive/src/main/java/io/helidon/examples/integrations/oci/vault/reactive/OciHandler.java b/examples/integrations/oci/vault-reactive/src/main/java/io/helidon/examples/integrations/oci/vault/reactive/OciHandler.java deleted file mode 100644 index 1eee9e8b5f2..00000000000 --- a/examples/integrations/oci/vault-reactive/src/main/java/io/helidon/examples/integrations/oci/vault/reactive/OciHandler.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.examples.integrations.oci.vault.reactive; - -import java.util.function.Consumer; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.oracle.bmc.responses.AsyncHandler; - -final class OciHandler { - private static final Logger LOGGER = Logger.getLogger(OciHandler.class.getName()); - - private OciHandler() { - } - - static AsyncHandler ociHandler(Consumer handler) { - return new AsyncHandler<>() { - @Override - public void onSuccess(REQ req, RES res) { - handler.accept(res); - } - - @Override - public void onError(REQ req, Throwable error) { - LOGGER.log(Level.WARNING, "OCI Exception", error); - if (error instanceof Error) { - throw (Error) error; - } - if (error instanceof RuntimeException) { - throw (RuntimeException) error; - } - throw new RuntimeException(error); - } - }; - } -} diff --git a/examples/integrations/oci/vault-reactive/src/main/java/io/helidon/examples/integrations/oci/vault/reactive/VaultService.java b/examples/integrations/oci/vault-reactive/src/main/java/io/helidon/examples/integrations/oci/vault/reactive/VaultService.java deleted file mode 100644 index 492b8ff29f2..00000000000 --- a/examples/integrations/oci/vault-reactive/src/main/java/io/helidon/examples/integrations/oci/vault/reactive/VaultService.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.examples.integrations.oci.vault.reactive; - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Date; - -import io.helidon.common.Base64Value; -import io.helidon.reactive.webserver.Handler; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.ServerRequest; -import io.helidon.reactive.webserver.ServerResponse; -import io.helidon.reactive.webserver.Service; - -import com.oracle.bmc.keymanagement.KmsCryptoAsync; -import com.oracle.bmc.keymanagement.model.DecryptDataDetails; -import com.oracle.bmc.keymanagement.model.EncryptDataDetails; -import com.oracle.bmc.keymanagement.model.SignDataDetails; -import com.oracle.bmc.keymanagement.model.VerifyDataDetails; -import com.oracle.bmc.keymanagement.requests.DecryptRequest; -import com.oracle.bmc.keymanagement.requests.EncryptRequest; -import com.oracle.bmc.keymanagement.requests.SignRequest; -import com.oracle.bmc.keymanagement.requests.VerifyRequest; -import com.oracle.bmc.secrets.SecretsAsync; -import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails; -import com.oracle.bmc.secrets.model.SecretBundleContentDetails; -import com.oracle.bmc.secrets.requests.GetSecretBundleRequest; -import com.oracle.bmc.vault.VaultsAsync; -import com.oracle.bmc.vault.model.Base64SecretContentDetails; -import com.oracle.bmc.vault.model.CreateSecretDetails; -import com.oracle.bmc.vault.model.ScheduleSecretDeletionDetails; -import com.oracle.bmc.vault.model.SecretContentDetails; -import com.oracle.bmc.vault.requests.CreateSecretRequest; -import com.oracle.bmc.vault.requests.ScheduleSecretDeletionRequest; - -import static io.helidon.examples.integrations.oci.vault.reactive.OciHandler.ociHandler; - -class VaultService implements Service { - private final SecretsAsync secrets; - private final VaultsAsync vaults; - private final KmsCryptoAsync crypto; - private final String vaultOcid; - private final String compartmentOcid; - private final String encryptionKeyOcid; - private final String signatureKeyOcid; - - VaultService(SecretsAsync secrets, - VaultsAsync vaults, - KmsCryptoAsync crypto, - String vaultOcid, - String compartmentOcid, - String encryptionKeyOcid, - String signatureKeyOcid) { - this.secrets = secrets; - this.vaults = vaults; - this.crypto = crypto; - this.vaultOcid = vaultOcid; - this.compartmentOcid = compartmentOcid; - this.encryptionKeyOcid = encryptionKeyOcid; - this.signatureKeyOcid = signatureKeyOcid; - } - - @Override - public void update(Routing.Rules rules) { - rules.get("/encrypt/{text:.*}", this::encrypt) - .get("/decrypt/{text:.*}", this::decrypt) - .get("/sign/{text}", this::sign) - .post("/verify/{text}", Handler.create(String.class, this::verify)) - .get("/secret/{id}", this::getSecret) - .post("/secret/{name}", Handler.create(String.class, this::createSecret)) - .delete("/secret/{id}", this::deleteSecret); - } - - private void getSecret(ServerRequest req, ServerResponse res) { - secrets.getSecretBundle(GetSecretBundleRequest.builder() - .secretId(req.path().param("id")) - .build(), ociHandler(ociRes -> { - SecretBundleContentDetails content = ociRes.getSecretBundle().getSecretBundleContent(); - if (content instanceof Base64SecretBundleContentDetails) { - // the only supported type - res.send(Base64Value.createFromEncoded(((Base64SecretBundleContentDetails) content).getContent()) - .toDecodedString()); - } else { - req.next(new Exception("Invalid secret content type")); - } - })); - } - - private void deleteSecret(ServerRequest req, ServerResponse res) { - // has to be for quite a long period of time - did not work with less than 30 days - Date deleteTime = Date.from(Instant.now().plus(30, ChronoUnit.DAYS)); - - String secretOcid = req.path().param("id"); - - vaults.scheduleSecretDeletion(ScheduleSecretDeletionRequest.builder() - .secretId(secretOcid) - .scheduleSecretDeletionDetails(ScheduleSecretDeletionDetails.builder() - .timeOfDeletion(deleteTime) - .build()) - .build(), ociHandler(ociRes -> res.send("Secret " + secretOcid - + " was marked for deletion"))); - } - - private void createSecret(ServerRequest req, ServerResponse res, String secretText) { - SecretContentDetails content = Base64SecretContentDetails.builder() - .content(Base64Value.create(secretText).toBase64()) - .build(); - - vaults.createSecret(CreateSecretRequest.builder() - .createSecretDetails(CreateSecretDetails.builder() - .secretName(req.path().param("name")) - .vaultId(vaultOcid) - .compartmentId(compartmentOcid) - .keyId(encryptionKeyOcid) - .secretContent(content) - .build()) - .build(), ociHandler(ociRes -> res.send(ociRes.getSecret().getId()))); - } - - private void verify(ServerRequest req, ServerResponse res, String signature) { - String text = req.path().param("text"); - VerifyDataDetails.SigningAlgorithm algorithm = VerifyDataDetails.SigningAlgorithm.Sha224RsaPkcsPss; - - crypto.verify(VerifyRequest.builder() - .verifyDataDetails(VerifyDataDetails.builder() - .keyId(signatureKeyOcid) - .signingAlgorithm(algorithm) - .message(Base64Value.create(text).toBase64()) - .signature(signature) - .build()) - .build(), - ociHandler(ociRes -> { - boolean valid = ociRes.getVerifiedData() - .getIsSignatureValid(); - res.send(valid ? "Signature valid" : "Signature not valid"); - })); - } - - private void sign(ServerRequest req, ServerResponse res) { - crypto.sign(SignRequest.builder() - .signDataDetails(SignDataDetails.builder() - .keyId(signatureKeyOcid) - .signingAlgorithm(SignDataDetails.SigningAlgorithm.Sha224RsaPkcsPss) - .message(Base64Value.create(req.path().param("text")).toBase64()) - .build()) - .build(), ociHandler(ociRes -> res.send(ociRes.getSignedData() - .getSignature()))); - } - - private void encrypt(ServerRequest req, ServerResponse res) { - crypto.encrypt(EncryptRequest.builder() - .encryptDataDetails(EncryptDataDetails.builder() - .keyId(encryptionKeyOcid) - .plaintext(Base64Value.create(req.path().param("text")).toBase64()) - .build()) - .build(), ociHandler(ociRes -> res.send(ociRes.getEncryptedData().getCiphertext()))); - } - - private void decrypt(ServerRequest req, ServerResponse res) { - crypto.decrypt(DecryptRequest.builder() - .decryptDataDetails(DecryptDataDetails.builder() - .keyId(encryptionKeyOcid) - .ciphertext(req.path().param("text")) - .build()) - .build(), ociHandler(ociRes -> res.send(Base64Value.createFromEncoded(ociRes.getDecryptedData() - .getPlaintext()) - .toDecodedString()))); - } -} diff --git a/examples/integrations/oci/vault-reactive/src/main/java/io/helidon/examples/integrations/oci/vault/reactive/package-info.java b/examples/integrations/oci/vault-reactive/src/main/java/io/helidon/examples/integrations/oci/vault/reactive/package-info.java deleted file mode 100644 index 23e07e67fd2..00000000000 --- a/examples/integrations/oci/vault-reactive/src/main/java/io/helidon/examples/integrations/oci/vault/reactive/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2021 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Example of OCI Vault integration in a reactive application. - */ -package io.helidon.examples.integrations.oci.vault.reactive; diff --git a/examples/integrations/oci/vault-reactive/pom.xml b/examples/integrations/oci/vault/pom.xml similarity index 85% rename from examples/integrations/oci/vault-reactive/pom.xml rename to examples/integrations/oci/vault/pom.xml index f3ef3e400ed..588d6642305 100644 --- a/examples/integrations/oci/vault-reactive/pom.xml +++ b/examples/integrations/oci/vault/pom.xml @@ -29,18 +29,18 @@ io.helidon.examples.integrations.oci - helidon-examples-integrations-oci-vault-reactive - Helidon Examples Integration OCI Vault Reactive - Reactive integration with OCI Vault. + helidon-examples-integrations-oci-vault + Helidon Examples Integration OCI Vault + Nima integration with OCI Vault. - io.helidon.examples.integrations.oci.vault.reactive.OciVaultMain + io.helidon.examples.integrations.oci.vault.OciVaultMain - io.helidon.reactive.webserver - helidon-reactive-webserver + io.helidon.nima.webserver + helidon-nima-webserver io.helidon.config diff --git a/examples/integrations/oci/vault-reactive/src/main/java/io/helidon/examples/integrations/oci/vault/reactive/OciVaultMain.java b/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/OciVaultMain.java similarity index 64% rename from examples/integrations/oci/vault-reactive/src/main/java/io/helidon/examples/integrations/oci/vault/reactive/OciVaultMain.java rename to examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/OciVaultMain.java index 51581058603..0d3cc69b323 100644 --- a/examples/integrations/oci/vault-reactive/src/main/java/io/helidon/examples/integrations/oci/vault/reactive/OciVaultMain.java +++ b/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/OciVaultMain.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,25 +14,24 @@ * limitations under the License. */ -package io.helidon.examples.integrations.oci.vault.reactive; +package io.helidon.examples.integrations.oci.vault; import java.io.IOException; -import io.helidon.logging.common.LogConfig; import io.helidon.config.Config; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.WebServer; +import io.helidon.logging.common.LogConfig; +import io.helidon.nima.webserver.WebServer; import com.oracle.bmc.ConfigFileReader; import com.oracle.bmc.auth.AuthenticationDetailsProvider; import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; -import com.oracle.bmc.keymanagement.KmsCryptoAsync; -import com.oracle.bmc.keymanagement.KmsCryptoAsyncClient; +import com.oracle.bmc.keymanagement.KmsCrypto; +import com.oracle.bmc.keymanagement.KmsCryptoClient; import com.oracle.bmc.model.BmcException; -import com.oracle.bmc.secrets.SecretsAsync; -import com.oracle.bmc.secrets.SecretsAsyncClient; -import com.oracle.bmc.vault.VaultsAsync; -import com.oracle.bmc.vault.VaultsAsyncClient; +import com.oracle.bmc.secrets.Secrets; +import com.oracle.bmc.secrets.SecretsClient; +import com.oracle.bmc.vault.Vaults; +import com.oracle.bmc.vault.VaultsClient; import static io.helidon.config.ConfigSources.classpath; import static io.helidon.config.ConfigSources.file; @@ -70,28 +69,28 @@ public static void main(String[] args) throws IOException { // ~/.oci/config AuthenticationDetailsProvider authProvider = new ConfigFileAuthenticationDetailsProvider(ConfigFileReader.parseDefault()); - SecretsAsync secrets = SecretsAsyncClient.builder().build(authProvider); - KmsCryptoAsync crypto = KmsCryptoAsyncClient.builder() + Secrets secrets = SecretsClient.builder().build(authProvider); + KmsCrypto crypto = KmsCryptoClient.builder() .endpoint(cryptoEndpoint) .build(authProvider); - VaultsAsync vaults = VaultsAsyncClient.builder().build(authProvider); + Vaults vaults = VaultsClient.builder().build(authProvider); - WebServer.builder() + WebServer server = WebServer.builder() + .routing(routing -> routing + .register("/vault", new VaultService(secrets, + vaults, + crypto, + vaultOcid, + compartmentOcid, + encryptionKey, + signatureKey)) + .error(BmcException.class, (req, res, ex) -> res.status( + ex.getStatusCode()).send(ex.getMessage()))) .config(config.get("server")) - .routing(Routing.builder() - .register("/vault", new VaultService(secrets, - vaults, - crypto, - vaultOcid, - compartmentOcid, - encryptionKey, - signatureKey)) - // OCI SDK error handling - .error(BmcException.class, (req, res, ex) -> res.status(ex.getStatusCode()) - .send(ex.getMessage()))) - .build() - .start() - .await(); + .start(); + + System.out.println("WEB server is up! http://localhost:" + server.port()); + } private static Config buildConfig() { diff --git a/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/VaultService.java b/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/VaultService.java new file mode 100644 index 00000000000..3b7bf256858 --- /dev/null +++ b/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/VaultService.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.integrations.oci.vault; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.common.Base64Value; +import io.helidon.common.http.Http; +import io.helidon.nima.webserver.http.HttpRules; +import io.helidon.nima.webserver.http.HttpService; +import io.helidon.nima.webserver.http.ServerRequest; +import io.helidon.nima.webserver.http.ServerResponse; + +import com.oracle.bmc.keymanagement.KmsCrypto; +import com.oracle.bmc.keymanagement.model.DecryptDataDetails; +import com.oracle.bmc.keymanagement.model.EncryptDataDetails; +import com.oracle.bmc.keymanagement.model.SignDataDetails; +import com.oracle.bmc.keymanagement.model.VerifyDataDetails; +import com.oracle.bmc.keymanagement.requests.DecryptRequest; +import com.oracle.bmc.keymanagement.requests.EncryptRequest; +import com.oracle.bmc.keymanagement.requests.SignRequest; +import com.oracle.bmc.keymanagement.requests.VerifyRequest; +import com.oracle.bmc.keymanagement.responses.DecryptResponse; +import com.oracle.bmc.keymanagement.responses.EncryptResponse; +import com.oracle.bmc.keymanagement.responses.SignResponse; +import com.oracle.bmc.keymanagement.responses.VerifyResponse; +import com.oracle.bmc.secrets.Secrets; +import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails; +import com.oracle.bmc.secrets.model.SecretBundleContentDetails; +import com.oracle.bmc.secrets.requests.GetSecretBundleRequest; +import com.oracle.bmc.secrets.responses.GetSecretBundleResponse; +import com.oracle.bmc.vault.Vaults; +import com.oracle.bmc.vault.model.Base64SecretContentDetails; +import com.oracle.bmc.vault.model.CreateSecretDetails; +import com.oracle.bmc.vault.model.ScheduleSecretDeletionDetails; +import com.oracle.bmc.vault.model.SecretContentDetails; +import com.oracle.bmc.vault.requests.CreateSecretRequest; +import com.oracle.bmc.vault.requests.ScheduleSecretDeletionRequest; +import com.oracle.bmc.vault.responses.CreateSecretResponse; + +class VaultService implements HttpService { + + private static final Logger LOGGER = Logger.getLogger(VaultService.class.getName()); + private final Secrets secrets; + private final Vaults vaults; + private final KmsCrypto crypto; + private final String vaultOcid; + private final String compartmentOcid; + private final String encryptionKeyOcid; + private final String signatureKeyOcid; + + VaultService(Secrets secrets, + Vaults vaults, + KmsCrypto crypto, + String vaultOcid, + String compartmentOcid, + String encryptionKeyOcid, + String signatureKeyOcid) { + this.secrets = secrets; + this.vaults = vaults; + this.crypto = crypto; + this.vaultOcid = vaultOcid; + this.compartmentOcid = compartmentOcid; + this.encryptionKeyOcid = encryptionKeyOcid; + this.signatureKeyOcid = signatureKeyOcid; + } + + /** + * A service registers itself by updating the routine rules. + * + * @param rules the routing rules. + */ + @Override + public void routing(HttpRules rules) { + rules.get("/encrypt/{text:.*}", this::encrypt) + .get("/decrypt/{text:.*}", this::decrypt) + .get("/sign/{text}", this::sign) + .post("/verify/{text}", this::verify) + .get("/secret/{id}", this::getSecret) + .post("/secret/{name}", this::createSecret) + .delete("/secret/{id}", this::deleteSecret); + } + + private void getSecret(ServerRequest req, ServerResponse res) { + ociHandler(response -> { + GetSecretBundleResponse id = secrets.getSecretBundle(GetSecretBundleRequest.builder() + .secretId(req.path().pathParameters().value("id")) + .build()); + SecretBundleContentDetails content = id.getSecretBundle().getSecretBundleContent(); + if (content instanceof Base64SecretBundleContentDetails) { + // the only supported type + res.send(Base64Value.createFromEncoded(((Base64SecretBundleContentDetails) content).getContent()) + .toDecodedString()); + } else { + res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send("Invalid secret content type"); + } + }, res); + + } + + private void deleteSecret(ServerRequest req, ServerResponse res) { + ociHandler(response -> { + // has to be for quite a long period of time - did not work with less than 30 days + Date deleteTime = Date.from(Instant.now().plus(30, ChronoUnit.DAYS)); + String secretOcid = req.path().pathParameters().value("id"); + vaults.scheduleSecretDeletion(ScheduleSecretDeletionRequest.builder() + .secretId(secretOcid) + .scheduleSecretDeletionDetails(ScheduleSecretDeletionDetails.builder() + .timeOfDeletion(deleteTime) + .build()) + .build() + ); + response.send(String.format("Secret %s was marked for deletion", secretOcid)); + }, res); + } + + private void createSecret(ServerRequest req, ServerResponse res) { + ociHandler(response -> { + String secretText = req.content().as(String.class); + SecretContentDetails content = Base64SecretContentDetails.builder() + .content(Base64Value.create(secretText).toBase64()) + .build(); + CreateSecretResponse vaultsSecret = vaults.createSecret(CreateSecretRequest.builder() + .createSecretDetails(CreateSecretDetails.builder() + .secretName(req.path().pathParameters().value("name")) + .vaultId(vaultOcid) + .compartmentId(compartmentOcid) + .keyId(encryptionKeyOcid) + .secretContent(content) + .build()) + .build()); + response.send(vaultsSecret.getSecret().getId()); + }, res); + } + + private void verify(ServerRequest req, ServerResponse res) { + + + ociHandler(response -> { + String text = req.path().pathParameters().value("text"); + String signature = req.content().as(String.class); + VerifyDataDetails.SigningAlgorithm algorithm = VerifyDataDetails.SigningAlgorithm.Sha224RsaPkcsPss; + VerifyResponse verifyResponse = crypto.verify(VerifyRequest.builder() + .verifyDataDetails(VerifyDataDetails.builder() + .keyId(signatureKeyOcid) + .signingAlgorithm(algorithm) + .message(Base64Value.create(text).toBase64()) + .signature(signature) + .build()) + .build()); + boolean valid = verifyResponse.getVerifiedData().getIsSignatureValid(); + response.send(valid ? "Signature valid" : "Signature not valid"); + }, res); + } + + private void sign(ServerRequest req, ServerResponse res) { + ociHandler(response -> { + SignResponse signResponse = crypto.sign(SignRequest.builder() + .signDataDetails(SignDataDetails.builder() + .keyId(signatureKeyOcid) + .signingAlgorithm(SignDataDetails.SigningAlgorithm.Sha224RsaPkcsPss) + .message(Base64Value.create(req.path() + .pathParameters().value("text")).toBase64()) + .build()) + .build()); + response.send(signResponse.getSignedData().getSignature()); + }, res); + } + + private void encrypt(ServerRequest req, ServerResponse res) { + ociHandler(response -> { + EncryptResponse encryptResponse = crypto.encrypt(EncryptRequest.builder() + .encryptDataDetails(EncryptDataDetails.builder() + .keyId(encryptionKeyOcid) + .plaintext(Base64Value.create(req.path() + .pathParameters().value("text")).toBase64()) + .build()) + .build()); + response.send(encryptResponse.getEncryptedData().getCiphertext()); + }, res); + } + + private void decrypt(ServerRequest req, ServerResponse res) { + ociHandler(response -> { + DecryptResponse decryptResponse = crypto.decrypt(DecryptRequest.builder() + .decryptDataDetails(DecryptDataDetails.builder() + .keyId(encryptionKeyOcid) + .ciphertext(req.path() + .pathParameters().value("text")) + .build()) + .build()); + response.send(Base64Value.createFromEncoded(decryptResponse.getDecryptedData().getPlaintext()) + .toDecodedString()); + }, res); + } + + private void ociHandler(Consumer consumer, ServerResponse response) { + try { + consumer.accept(response); + } catch (Throwable error) { + LOGGER.log(Level.WARNING, "OCI Exception", error); + response.status(Http.Status.INTERNAL_SERVER_ERROR_500).send(error.getMessage()); + } + } +} diff --git a/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/package-info.java b/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/package-info.java new file mode 100644 index 00000000000..deea60af35b --- /dev/null +++ b/examples/integrations/oci/vault/src/main/java/io/helidon/examples/integrations/oci/vault/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Example of OCI Vault integration in a Nima application. + */ +package io.helidon.examples.integrations.oci.vault; diff --git a/examples/integrations/oci/vault-reactive/src/main/resources/application.yaml b/examples/integrations/oci/vault/src/main/resources/application.yaml similarity index 95% rename from examples/integrations/oci/vault-reactive/src/main/resources/application.yaml rename to examples/integrations/oci/vault/src/main/resources/application.yaml index 982ef550c45..a61582e97b5 100644 --- a/examples/integrations/oci/vault-reactive/src/main/resources/application.yaml +++ b/examples/integrations/oci/vault/src/main/resources/application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2021, 2022 Oracle and/or its affiliates. +# Copyright (c) 2021, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/integrations/oci/metrics-reactive/src/main/resources/logging.properties b/examples/integrations/oci/vault/src/main/resources/logging.properties similarity index 94% rename from examples/integrations/oci/metrics-reactive/src/main/resources/logging.properties rename to examples/integrations/oci/vault/src/main/resources/logging.properties index f8802f90626..341cef8ce9e 100644 --- a/examples/integrations/oci/metrics-reactive/src/main/resources/logging.properties +++ b/examples/integrations/oci/vault/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2021, 2022 Oracle and/or its affiliates. +# Copyright (c) 2021, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/integrations/vault/hcp-cdi/pom.xml b/examples/integrations/vault/hcp-cdi/pom.xml index e4fb29b92b3..79346a8154d 100644 --- a/examples/integrations/vault/hcp-cdi/pom.xml +++ b/examples/integrations/vault/hcp-cdi/pom.xml @@ -87,7 +87,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/jbatch/pom.xml b/examples/jbatch/pom.xml index e1f7c9cc0f7..56bdea51824 100644 --- a/examples/jbatch/pom.xml +++ b/examples/jbatch/pom.xml @@ -68,7 +68,7 @@ - org.jboss + io.smallrye jandex runtime true @@ -124,7 +124,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/logging/jul/README.md b/examples/logging/jul/README.md index 07b3df804af..1d86b469683 100644 --- a/examples/logging/jul/README.md +++ b/examples/logging/jul/README.md @@ -20,11 +20,11 @@ java -jar target/helidon-examples-logging-jul.jar Expected output should be similar to the following: ```text -2020.11.19 15:37:28 INFO io.helidon.logging.common.LogConfig Thread[main,5,main]: Logging at initialization configured using classpath: /logging.properties "" +2020.11.19 15:37:28 INFO io.helidon.logging.jul.JulProvider Thread[#1,main,5,main]: Logging at initialization configured using classpath: /logging.properties "" 2020.11.19 15:37:28 INFO io.helidon.examples.logging.jul.Main Thread[main,5,main]: Starting up "startup" 2020.11.19 15:37:28 INFO io.helidon.examples.logging.jul.Main Thread[pool-1-thread-1,5,main]: Running on another thread "propagated" -2020.11.19 15:37:28 INFO io.helidon.common.features.HelidonFeatures Thread[features-thread,5,main]: Helidon SE 2.2.0 features: [Config, WebServer] "" -2020.11.19 15:37:28 INFO io.helidon.reactive.webserver.NettyWebServer Thread[nioEventLoopGroup-2-1,10,main]: Channel '@default' started: [id: 0x8a5f5634, L:/0:0:0:0:0:0:0:0:8080] "" +2020.11.19 15:37:28 INFO io.helidon.common.features.HelidonFeatures Thread[#23,features-thread,5,main]: Helidon NIMA 4.0.0-SNAPSHOT features: [Config, Encoding, Media, WebServer] "" +2020.11.19 15:37:28 INFO io.helidon.nima.webserver.LoomServer Thread[#1,main,5,main]: Started all channels in 46 milliseconds. 577 milliseconds since JVM startup. Java 20.0.1+9-29 "propagated" ``` # Running as native image diff --git a/examples/logging/jul/pom.xml b/examples/logging/jul/pom.xml index 85791619354..ca654ccdadd 100644 --- a/examples/logging/jul/pom.xml +++ b/examples/logging/jul/pom.xml @@ -41,8 +41,8 @@ - io.helidon.reactive.webserver - helidon-reactive-webserver + io.helidon.nima.webserver + helidon-nima-webserver diff --git a/examples/logging/jul/src/main/java/io/helidon/examples/logging/jul/Main.java b/examples/logging/jul/src/main/java/io/helidon/examples/logging/jul/Main.java index 5a9b065264f..7388ce88c2a 100644 --- a/examples/logging/jul/src/main/java/io/helidon/examples/logging/jul/Main.java +++ b/examples/logging/jul/src/main/java/io/helidon/examples/logging/jul/Main.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,15 +19,14 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import io.helidon.common.context.Context; import io.helidon.common.context.Contexts; import io.helidon.logging.common.HelidonMdc; import io.helidon.logging.common.LogConfig; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.WebServer; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http.HttpRouting; /** * Main class of the example, runnable from command line. @@ -51,18 +50,18 @@ public static void main(String[] args) { // done by the webserver Contexts.runInContext(Context.create(), Main::logging); - WebServer.builder() - .routing(Routing.builder() - .get("/", (req, res) -> { - HelidonMdc.set("name", String.valueOf(req.requestId())); - LOGGER.info("Running in webserver, id:"); - res.send("Hello"); - }) - .build()) + WebServer server = WebServer.builder() .port(8080) - .build() - .start() - .await(10, TimeUnit.SECONDS); + .routing(Main::routing) + .start(); + } + + private static void routing(HttpRouting.Builder routing) { + routing.get("/", (req, res) -> { + HelidonMdc.set("name", String.valueOf(req.id())); + LOGGER.info("Running in webserver, id:"); + res.send("Hello"); + }); } private static void logging() { diff --git a/examples/logging/log4j/README.md b/examples/logging/log4j/README.md index d99ea402692..af9d59a9765 100644 --- a/examples/logging/log4j/README.md +++ b/examples/logging/log4j/README.md @@ -22,12 +22,11 @@ java -jar target/helidon-examples-logging-log4j.jar Expected output should be similar to the following: ```text -2020-11-19 15:44:48,561 main INFO Registered Log4j as the java.util.logging.LogManager. 15:44:48.596 INFO [main] io.helidon.examples.logging.log4j.Main - Starting up "startup" -15:44:48.598 INFO [main] io.helidon.examples.logging.log4j.Main - Using JUL logger "startup" +15:44:48.598 INFO [main] io.helidon.examples.logging.log4j.Main - Using System logger "startup" 15:44:48.600 INFO [pool-2-thread-1] io.helidon.examples.logging.log4j.Main - Running on another thread "propagated" -15:44:48.704 INFO [features-thread] io.helidon.common.features.HelidonFeatures - Helidon SE 2.2.0 features: [Config, WebServer] "" -15:44:48.801 INFO [nioEventLoopGroup-2-1] io.helidon.reactive.webserver.NettyWebServer - Channel '@default' started: [id: 0xa215c23d, L:/0:0:0:0:0:0:0:0:8080] "" +15:44:48.704 INFO [features-thread] io.helidon.common.features.HelidonFeatures - Helidon NIMA 4.0.0-SNAPSHOT features: [Config, Encoding, Media, WebServer] "" +15:44:48.801 INFO [main] io.helidon.nima.webserver.LoomServer - Started all channels in 12 milliseconds. 746 milliseconds since JVM startup. Java 20.0.1+9-29 "propagated" ``` # Running as native image diff --git a/examples/logging/log4j/pom.xml b/examples/logging/log4j/pom.xml index bab864dff43..70644e0a199 100644 --- a/examples/logging/log4j/pom.xml +++ b/examples/logging/log4j/pom.xml @@ -41,8 +41,8 @@ - io.helidon.reactive.webserver - helidon-reactive-webserver + io.helidon.nima.webserver + helidon-nima-webserver io.helidon.logging diff --git a/examples/logging/log4j/src/main/java/io/helidon/examples/logging/log4j/Main.java b/examples/logging/log4j/src/main/java/io/helidon/examples/logging/log4j/Main.java index df0346c320a..767ece02f24 100644 --- a/examples/logging/log4j/src/main/java/io/helidon/examples/logging/log4j/Main.java +++ b/examples/logging/log4j/src/main/java/io/helidon/examples/logging/log4j/Main.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,14 +19,13 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; import io.helidon.common.context.Context; import io.helidon.common.context.Contexts; import io.helidon.logging.common.HelidonMdc; import io.helidon.logging.common.LogConfig; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.WebServer; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http.HttpRouting; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; @@ -68,18 +67,17 @@ public static void main(String[] args) { // done by the webserver Contexts.runInContext(Context.create(), Main::logging); - WebServer.builder() - .routing(Routing.builder() - .get("/", (req, res) -> { - HelidonMdc.set("name", String.valueOf(req.requestId())); - logger.info("Running in webserver, id:"); - res.send("Hello"); - }) - .build()) - .port(8080) - .build() - .start() - .await(10, TimeUnit.SECONDS); + WebServer server = WebServer.builder() + .routing(Main::routing) + .start(); + } + + private static void routing(HttpRouting.Builder routing) { + routing.get("/", (req, res) -> { + HelidonMdc.set("name", String.valueOf(req.id())); + logger.info("Running in webserver, id:"); + res.send("Hello"); + }); } private static void logging() { diff --git a/examples/logging/logback-aot/README.md b/examples/logging/logback-aot/README.md index 30c560c1e1b..c4a6be6559d 100644 --- a/examples/logging/logback-aot/README.md +++ b/examples/logging/logback-aot/README.md @@ -18,11 +18,11 @@ Within 30 seconds the configuration should be reloaded, and next request will ha Expected output should be similar to the following (for both hotspot and native): ```text -15:40:44.240 INFO [main] i.h.examples.logging.slf4j.Main - Starting up startup -15:40:44.241 INFO [main] i.h.examples.logging.slf4j.Main - Using JUL logger startup -15:40:44.245 INFO [pool-1-thread-1] i.h.examples.logging.slf4j.Main - Running on another thread propagated -15:40:44.395 INFO [features-thread] io.helidon.common.features.HelidonFeatures - Helidon SE 2.2.0 features: [Config, WebServer] -15:40:44.538 INFO [nioEventLoopGroup-2-1] io.helidon.reactive.webserver.NettyWebServer - Channel '@default' started: [id: 0x8e516487, L:/0:0:0:0:0:0:0:0:8080] +15:40:44.240 [INFO ] [io.helidon.examples.logging.logback.aot.Main.logging:128] Starting up startup +15:40:44.241 [INFO ] [o.slf4j.jdk.platform.logging.SLF4JPlatformLogger.performLog:151] Using System logger startup +15:40:44.245 [INFO ] [io.helidon.examples.logging.logback.aot.Main.log:146] Running on another thread propagated +15:40:44.395 [INFO ] [o.slf4j.jdk.platform.logging.SLF4JPlatformLogger.performLog:151] Helidon NIMA 4.0.0-SNAPSHOT features: [Config, Encoding, Media, WebServer] +15:40:44.538 [INFO ] [o.slf4j.jdk.platform.logging.SLF4JPlatformLogger.performLog:151] Started all channels in 15 milliseconds. 647 milliseconds since JVM startup. Java 20.0.1+9-29 propagated ``` The output is also logged into `helidon.log`. @@ -36,7 +36,7 @@ mvn clean package Run from command line: ```shell script -java -jar target/helidon-examples-logging-sfl4j.jar +java -jar target/helidon-examples-logging-slf4j-aot.jar ``` Execute endpoint: diff --git a/examples/logging/logback-aot/pom.xml b/examples/logging/logback-aot/pom.xml index add733a0905..b48865b4900 100644 --- a/examples/logging/logback-aot/pom.xml +++ b/examples/logging/logback-aot/pom.xml @@ -42,8 +42,8 @@ - io.helidon.reactive.webserver - helidon-reactive-webserver + io.helidon.nima.webserver + helidon-nima-webserver io.helidon.logging diff --git a/examples/logging/logback-aot/src/main/java/io/helidon/examples/logging/logback/aot/Main.java b/examples/logging/logback-aot/src/main/java/io/helidon/examples/logging/logback/aot/Main.java index 470b18c764b..e832f19cafc 100644 --- a/examples/logging/logback-aot/src/main/java/io/helidon/examples/logging/logback/aot/Main.java +++ b/examples/logging/logback-aot/src/main/java/io/helidon/examples/logging/logback/aot/Main.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,13 +19,12 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; import io.helidon.common.context.Context; import io.helidon.common.context.Contexts; import io.helidon.logging.common.HelidonMdc; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.WebServer; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http.HttpRouting; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; @@ -60,20 +59,20 @@ public static void main(String[] args) { // done by the webserver Contexts.runInContext(Context.create(), Main::logging); - WebServer.builder() - .routing(Routing.builder() - .get("/", (req, res) -> { - HelidonMdc.set("name", String.valueOf(req.requestId())); - LOGGER.debug("Debug message to show runtime reloading works"); - LOGGER.info("Running in webserver, id:"); - res.send("Hello") - .forSingle(ignored -> LOGGER.debug("Response sent")); - }) - .build()) + WebServer server = WebServer.builder() .port(8080) - .build() - .start() - .await(10, TimeUnit.SECONDS); + .routing(Main::routing) + .start(); + } + + private static void routing(HttpRouting.Builder routing) { + routing.get("/", (req, res) -> { + HelidonMdc.set("name", String.valueOf(req.id())); + LOGGER.debug("Debug message to show runtime reloading works"); + LOGGER.info("Running in webserver, id:"); + res.send("Hello"); + LOGGER.debug("Response sent"); + }); } private static void setupLogging() { diff --git a/examples/logging/slf4j/README.md b/examples/logging/slf4j/README.md index d9720bfea10..ec016da0c94 100644 --- a/examples/logging/slf4j/README.md +++ b/examples/logging/slf4j/README.md @@ -11,10 +11,10 @@ The example can be built using GraalVM native image as well. Expected output should be similar to the following (for both hotspot and native): ```text 15:40:44.240 INFO [main] i.h.examples.logging.slf4j.Main - Starting up startup -15:40:44.241 INFO [main] i.h.examples.logging.slf4j.Main - Using JUL logger startup +15:40:44.241 INFO [main] i.h.examples.logging.slf4j.Main - Using System logger startup 15:40:44.245 INFO [pool-1-thread-1] i.h.examples.logging.slf4j.Main - Running on another thread propagated -15:40:44.395 INFO [features-thread] io.helidon.common.features.HelidonFeatures - Helidon SE 2.2.0 features: [Config, WebServer] -15:40:44.538 INFO [nioEventLoopGroup-2-1] io.helidon.reactive.webserver.NettyWebServer - Channel '@default' started: [id: 0x8e516487, L:/0:0:0:0:0:0:0:0:8080] +15:40:44.395 INFO [features-thread] i.h.common.features.HelidonFeatures - Helidon NIMA 4.0.0-SNAPSHOT features: [Config, Encoding, Media, WebServer] +15:40:44.538 INFO [main] i.helidon.nima.webserver.LoomServer - Started all channels in 15 milliseconds. 561 milliseconds since JVM startup. Java 20.0.1+9-29 propagated ``` # Running as jar diff --git a/examples/logging/slf4j/pom.xml b/examples/logging/slf4j/pom.xml index 3a4505a43de..fb81df6a3f1 100644 --- a/examples/logging/slf4j/pom.xml +++ b/examples/logging/slf4j/pom.xml @@ -41,8 +41,8 @@ - io.helidon.reactive.webserver - helidon-reactive-webserver + io.helidon.nima.webserver + helidon-nima-webserver io.helidon.logging diff --git a/examples/logging/slf4j/src/main/java/io/helidon/examples/logging/slf4j/Main.java b/examples/logging/slf4j/src/main/java/io/helidon/examples/logging/slf4j/Main.java index c38d1ab793d..ab1bdb3c4b5 100644 --- a/examples/logging/slf4j/src/main/java/io/helidon/examples/logging/slf4j/Main.java +++ b/examples/logging/slf4j/src/main/java/io/helidon/examples/logging/slf4j/Main.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,14 +19,13 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; import io.helidon.common.context.Context; import io.helidon.common.context.Contexts; import io.helidon.logging.common.HelidonMdc; import io.helidon.logging.common.LogConfig; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.WebServer; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http.HttpRouting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,18 +54,17 @@ public static void main(String[] args) { // done by the webserver Contexts.runInContext(Context.create(), Main::logging); - WebServer.builder() - .routing(Routing.builder() - .get("/", (req, res) -> { - HelidonMdc.set("name", String.valueOf(req.requestId())); - LOGGER.info("Running in webserver, id:"); - res.send("Hello"); - }) - .build()) - .port(8080) - .build() - .start() - .await(10, TimeUnit.SECONDS); + WebServer server = WebServer.builder() + .routing(Main::routing) + .start(); + } + + private static void routing(HttpRouting.Builder routing) { + routing.get("/", (req, res) -> { + HelidonMdc.set("name", String.valueOf(req.id())); + LOGGER.info("Running in webserver, id:"); + res.send("Hello"); + }); } private static void logging() { diff --git a/examples/media/multipart/README.md b/examples/media/multipart/README.md index 206a630c045..13acde601fe 100644 --- a/examples/media/multipart/README.md +++ b/examples/media/multipart/README.md @@ -1,4 +1,4 @@ -# Helidon SE MultiPart Example +# Helidon Nima MultiPart Example This example demonstrates how to use `MultiPartSupport` with both the `WebServer` and `WebClient` APIs. diff --git a/examples/media/multipart/pom.xml b/examples/media/multipart/pom.xml index 2daf742224e..127f52bc325 100644 --- a/examples/media/multipart/pom.xml +++ b/examples/media/multipart/pom.xml @@ -23,9 +23,9 @@ 4.0.0 io.helidon.applications - helidon-se + helidon-nima 4.0.0-SNAPSHOT - ../../../applications/se/pom.xml + ../../../applications/nima/pom.xml io.helidon.examples.media helidon-examples-media-multipart @@ -41,24 +41,24 @@ - io.helidon.reactive.webserver - helidon-reactive-webserver + io.helidon.nima.webserver + helidon-nima-webserver - io.helidon.reactive.webserver - helidon-reactive-webserver-static-content + io.helidon.nima.http.media + helidon-nima-http-media-multipart - io.helidon.reactive.media - helidon-reactive-media-multipart + io.helidon.nima.http.media + helidon-nima-http-media-jsonp - io.helidon.reactive.media - helidon-reactive-media-jsonp + io.helidon.nima.webserver + helidon-nima-webserver-static-content - org.eclipse.parsson - parsson + jakarta.json + jakarta.json-api org.junit.jupiter @@ -71,8 +71,8 @@ test - io.helidon.reactive.webclient - helidon-reactive-webclient + io.helidon.nima.testing.junit5 + helidon-nima-testing-junit5-webserver test @@ -90,12 +90,15 @@ org.apache.maven.plugins - maven-compiler-plugin - - - --enable-preview - - + maven-failsafe-plugin + + + + integration-test + verify + + + diff --git a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileService.java b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileService.java index b757d8b0741..2b3b4d5cce9 100644 --- a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileService.java +++ b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileService.java @@ -15,92 +15,136 @@ */ package io.helidon.examples.media.multipart; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; +import java.nio.file.StandardOpenOption; import java.util.Map; -import java.util.concurrent.ExecutorService; +import java.util.stream.Stream; -import io.helidon.common.configurable.ThreadPoolSupplier; -import io.helidon.common.http.BadRequestException; import io.helidon.common.http.ContentDisposition; -import io.helidon.common.http.DataChunk; import io.helidon.common.http.Http; +import io.helidon.common.http.ServerResponseHeaders; import io.helidon.common.media.type.MediaTypes; -import io.helidon.common.reactive.IoMulti; -import io.helidon.reactive.media.multipart.ReadableBodyPart; -import io.helidon.reactive.webserver.ResponseHeaders; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.ServerRequest; -import io.helidon.reactive.webserver.ServerResponse; -import io.helidon.reactive.webserver.Service; +import io.helidon.nima.http.media.multipart.MultiPart; +import io.helidon.nima.http.media.multipart.ReadablePart; +import io.helidon.nima.webserver.http.HttpRules; +import io.helidon.nima.webserver.http.HttpService; +import io.helidon.nima.webserver.http.ServerRequest; +import io.helidon.nima.webserver.http.ServerResponse; import jakarta.json.Json; import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonBuilderFactory; +import static io.helidon.common.http.Http.Status.BAD_REQUEST_400; +import static io.helidon.common.http.Http.Status.MOVED_PERMANENTLY_301; +import static io.helidon.common.http.Http.Status.NOT_FOUND_404; + /** * File service. */ -public final class FileService implements Service { - - private static final JsonBuilderFactory JSON_FACTORY = Json.createBuilderFactory(Map.of()); - private final FileStorage storage; - private final ExecutorService executor = ThreadPoolSupplier.create("multipart-thread-pool").get(); - +public final class FileService implements HttpService { + private static final Http.HeaderValue UI_LOCATION = Http.Header.createCached(Http.Header.LOCATION, "/ui"); + private final JsonBuilderFactory jsonFactory; + private final Path storage; /** * Create a new file upload service instance. */ FileService() { - storage = new FileStorage(); + jsonFactory = Json.createBuilderFactory(Map.of()); + storage = createStorage(); + System.out.println("Storage: " + storage); } @Override - public void update(Routing.Rules rules) { + public void routing(HttpRules rules) { rules.get("/", this::list) - .get("/{fname}", this::download) - .post("/", this::upload); + .get("/{fname}", this::download) + .post("/", this::upload); + } + + private static Path createStorage() { + try { + return Files.createTempDirectory("fileupload"); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private static Stream listFiles(Path storage) { + + try (Stream walk = Files.walk(storage)) { + return walk.filter(Files::isRegularFile) + .map(storage::relativize) + .map(Path::toString) + .toList() + .stream(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private static OutputStream newOutputStream(Path storage, String fname) { + try { + return Files.newOutputStream(storage.resolve(fname), + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException ex) { + throw new RuntimeException(ex); + } } private void list(ServerRequest req, ServerResponse res) { - JsonArrayBuilder arrayBuilder = JSON_FACTORY.createArrayBuilder(); - storage.listFiles().forEach(arrayBuilder::add); - res.send(JSON_FACTORY.createObjectBuilder().add("files", arrayBuilder).build()); + JsonArrayBuilder arrayBuilder = jsonFactory.createArrayBuilder(); + listFiles(storage).forEach(arrayBuilder::add); + res.send(jsonFactory.createObjectBuilder().add("files", arrayBuilder).build()); } private void download(ServerRequest req, ServerResponse res) { - Path filePath = storage.lookup(req.path().param("fname")); - ResponseHeaders headers = res.headers(); + Path filePath = storage.resolve(req.path().pathParameters().value("fname")); + if (!filePath.getParent().equals(storage)) { + res.status(BAD_REQUEST_400).send("Invalid file name"); + return; + } + if (!Files.exists(filePath)) { + res.status(NOT_FOUND_404).send(); + return; + } + if (!Files.isRegularFile(filePath)) { + res.status(BAD_REQUEST_400).send("Not a file"); + return; + } + ServerResponseHeaders headers = res.headers(); headers.contentType(MediaTypes.APPLICATION_OCTET_STREAM); - headers.set(Http.Header.CONTENT_DISPOSITION, ContentDisposition.builder() - .filename(filePath.getFileName().toString()) - .build() - .toString()); + headers.set(ContentDisposition.builder() + .filename(filePath.getFileName().toString()) + .build()); res.send(filePath); } private void upload(ServerRequest req, ServerResponse res) { - req.content().asStream(ReadableBodyPart.class) - .forEach(part -> { - if (part.isNamed("file[]")) { - String filename = part.filename() - .orElseThrow(() -> new BadRequestException("Missing filename")); - part.content() - .map(DataChunk::data) - .flatMapIterable(Arrays::asList) - .to(IoMulti.writeToFile(storage.create(filename)) - .executor(executor) - .build()); - } else { - // when streaming unconsumed parts needs to be drained - part.drain(); - } - }) - .onError(res::send) - .onComplete(() -> { - res.status(Http.Status.MOVED_PERMANENTLY_301); - res.headers().set(Http.Header.LOCATION, "/ui"); - res.send(); - }).ignoreElement(); + MultiPart mp = req.content().as(MultiPart.class); + + while (mp.hasNext()) { + ReadablePart part = mp.next(); + if ("file[]".equals(URLDecoder.decode(part.name(), StandardCharsets.UTF_8))) { + try (InputStream in = part.inputStream(); OutputStream out = newOutputStream(storage, part.fileName().get())) { + in.transferTo(out); + } catch (IOException e) { + throw new RuntimeException("Failed to write content", e); + } + } + } + + res.status(MOVED_PERMANENTLY_301) + .header(UI_LOCATION) + .send(); } } diff --git a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileStorage.java b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileStorage.java deleted file mode 100644 index 8825395f665..00000000000 --- a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/FileStorage.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.examples.media.multipart; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.stream.Stream; - -import io.helidon.common.http.BadRequestException; -import io.helidon.common.http.NotFoundException; - -/** - * Simple bean to managed a directory based storage. - */ -public class FileStorage { - - private final Path storageDir; - - /** - * Create a new instance. - */ - public FileStorage() { - try { - storageDir = Files.createTempDirectory("fileupload"); - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } - } - - /** - * Get the storage directory. - * - * @return directory - */ - public Path storageDir() { - return storageDir; - } - - /** - * Get the names of the files in the storage directory. - * - * @return Stream of file names - */ - public Stream listFiles() { - try { - return Files.walk(storageDir) - .filter(Files::isRegularFile) - .map(storageDir::relativize) - .map(java.nio.file.Path::toString); - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } - } - - /** - * Create a new file in the storage. - * - * @param fname file name - * @return file - * @throws BadRequestException if the resolved file is not contained in the storage directory - */ - public Path create(String fname) { - Path file = storageDir.resolve(fname); - if (!file.getParent().equals(storageDir)) { - throw new BadRequestException("Invalid file name"); - } - try { - Files.createFile(file); - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } - return file; - } - - /** - * Lookup an existing file in the storage. - * - * @param fname file name - * @return file - * @throws NotFoundException If the resolved file does not exist - * @throws BadRequestException if the resolved file is not contained in the storage directory - */ - public Path lookup(String fname) { - Path file = storageDir.resolve(fname); - if (!file.getParent().equals(storageDir)) { - throw new BadRequestException("Invalid file name"); - } - if (!Files.exists(file)) { - throw new NotFoundException("file not found"); - } - if (!Files.isRegularFile(file)) { - throw new BadRequestException("Not a file"); - } - return file; - } -} diff --git a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/Main.java b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/Main.java index 63c82a582f9..6e9ecca0897 100644 --- a/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/Main.java +++ b/examples/media/multipart/src/main/java/io/helidon/examples/media/multipart/Main.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,75 +16,47 @@ package io.helidon.examples.media.multipart; import io.helidon.common.http.Http; -import io.helidon.common.reactive.Single; -import io.helidon.logging.common.LogConfig; -import io.helidon.reactive.media.jsonp.JsonpSupport; -import io.helidon.reactive.media.multipart.MultiPartSupport; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.WebServer; -import io.helidon.reactive.webserver.staticcontent.StaticContentSupport; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http.HttpRules; +import io.helidon.nima.webserver.staticcontent.StaticContentService; /** * This application provides a simple file upload service with a UI to exercise multipart. */ public final class Main { - private static final Http.HeaderValue REDIRECT_LOCATION = Http.Header.createCached(Http.Header.LOCATION, "/ui"); + private static final Http.HeaderValue UI_LOCATION = Http.Header.createCached(Http.Header.LOCATION, "/ui"); private Main() { } /** - * Creates new {@link Routing}. + * Executes the example. * - * @return the new instance + * @param args command line arguments, ignored */ - static Routing createRouting() { - return Routing.builder() - .any("/", (req, res) -> { - res.status(Http.Status.MOVED_PERMANENTLY_301); - res.headers().set(REDIRECT_LOCATION); - res.send(); - }) - .register("/ui", StaticContentSupport.builder("WEB") - .welcomeFileName("index.html") - .build()) - .register("/api", new FileService()) - .build(); - } + public static void main(String[] args) { + WebServer server = WebServer.builder() + .routing(Main::routing) + .port(8080) + .start(); - /** - * Application main entry point. - * @param args command line arguments. - */ - public static void main(final String[] args) { - startServer(8080); + System.out.println("WEB server is up! http://localhost:" + server.port()); } /** - * Start the server. - * @return the created {@link WebServer} instance + * Updates the routing rules. + * + * @param rules routing rules */ - static Single startServer(int port) { - LogConfig.configureRuntime(); - WebServer server = WebServer.builder(createRouting()) - .port(port) - .addMediaSupport(MultiPartSupport.create()) - .addMediaSupport(JsonpSupport.create()) - .build(); - - Single webserver = server.start(); - - // Start the server and print some info. - webserver.thenAccept(ws -> { - System.out.println("WEB server is up! http://localhost:" + ws.port()); - }); - - // Server threads are not demon. NO need to block. Just react. - server.whenShutdown() - .thenRun(() -> System.out.println("WEB server is DOWN. Good bye!")); - - return webserver; + static void routing(HttpRules rules) { + rules.any("/", (req, res) -> { + res.status(Http.Status.MOVED_PERMANENTLY_301); + res.header(UI_LOCATION); + res.send(); + }) + .register("/ui", StaticContentService.builder("WEB") + .welcomeFileName("index.html") + .build()) + .register("/api", new FileService()); } - - } diff --git a/examples/media/multipart/src/main/resources/WEB/index.html b/examples/media/multipart/src/main/resources/WEB/index.html index 020d42db3f8..cc055977e07 100644 --- a/examples/media/multipart/src/main/resources/WEB/index.html +++ b/examples/media/multipart/src/main/resources/WEB/index.html @@ -1,7 +1,7 @@ - - - Helidon Examples Media Multipart - - - + + - - -

Uploaded files

-
- -

Upload

-
- Select a file to upload: - - -
- - - + + +

Uploaded files

+
+ +

Upload (buffered)

+
+ Select a file to upload: + + +
+ +

Upload (stream)

+
+ Select a file to upload: + + +
+ + + diff --git a/examples/media/multipart/src/main/resources/logging.properties b/examples/media/multipart/src/main/resources/logging.properties index 22deeb40eb2..5b4a364a23d 100644 --- a/examples/media/multipart/src/main/resources/logging.properties +++ b/examples/media/multipart/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2022 Oracle and/or its affiliates. +# Copyright (c) 2018, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,22 +14,8 @@ # limitations under the License. # -# Example Logging Configuration File -# For more information see $JAVA_HOME/jre/lib/logging.properties - -# Send messages to the console -handlers=io.helidon.logging.jul.HelidonConsoleHandler - -# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread -java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n - +handlers=java.util.logging.ConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n # Global logging level. Can be overridden by specific loggers .level=INFO -io.helidon.level=FINEST - -# Component specific log levels -#io.helidon.reactive.webserver.level=INFO -#io.helidon.config.level=INFO -#io.helidon.security.level=INFO -#io.helidon.common.level=INFO -#io.netty.level=INFO +io.helidon.nima.level=INFO diff --git a/examples/media/multipart/src/test/java/io/helidon/examples/media/multipart/FileServiceTest.java b/examples/media/multipart/src/test/java/io/helidon/examples/media/multipart/FileServiceTest.java index 648403b69a0..07e04c5bc9f 100644 --- a/examples/media/multipart/src/test/java/io/helidon/examples/media/multipart/FileServiceTest.java +++ b/examples/media/multipart/src/test/java/io/helidon/examples/media/multipart/FileServiceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,23 +19,21 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.time.Duration; import java.util.List; import io.helidon.common.http.Http; import io.helidon.common.media.type.MediaTypes; -import io.helidon.reactive.media.jsonp.JsonpSupport; -import io.helidon.reactive.media.multipart.FileFormParams; -import io.helidon.reactive.media.multipart.MultiPartSupport; -import io.helidon.reactive.webclient.WebClient; -import io.helidon.reactive.webclient.WebClientResponse; -import io.helidon.reactive.webserver.WebServer; +import io.helidon.nima.http.media.multipart.WriteableMultiPart; +import io.helidon.nima.http.media.multipart.WriteablePart; +import io.helidon.nima.testing.junit5.webserver.ServerTest; +import io.helidon.nima.testing.junit5.webserver.SetUpRoute; +import io.helidon.nima.webclient.http1.Http1Client; +import io.helidon.nima.webclient.http1.Http1ClientResponse; +import io.helidon.nima.webserver.http.HttpRouting; import jakarta.json.JsonObject; import jakarta.json.JsonString; import org.hamcrest.Matchers; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -51,44 +49,30 @@ * Tests {@link FileService}. */ @TestMethodOrder(OrderAnnotation.class) +@ServerTest public class FileServiceTest { - private static final Duration TIMEOUT = Duration.ofSeconds(10); + private final Http1Client client; - private static WebServer webServer; - private static WebClient webClient; - - @BeforeAll - public static void startTheServer() { - webServer = Main.startServer(0) - .await(TIMEOUT); - - webClient = WebClient.builder() - .baseUri("http://localhost:" + webServer.port() + "/api") - .addMediaSupport(MultiPartSupport.create()) - .addMediaSupport(JsonpSupport.create()) - .build(); + FileServiceTest(Http1Client client) { + this.client = client; } - @AfterAll - public static void stopServer() { - if (webServer != null) { - webServer.shutdown() - .await(TIMEOUT); - } + @SetUpRoute + static void routing(HttpRouting.Builder builder) { + Main.routing(builder); } @Test @Order(1) public void testUpload() throws IOException { Path file = Files.writeString(Files.createTempFile(null, null), "bar\n"); - WebClientResponse response = webClient - .post() - .contentType(MediaTypes.MULTIPART_FORM_DATA) - .submit(FileFormParams.builder() - .addFile("file[]", "foo.txt", file) - .build()) - .await(TIMEOUT); - assertThat(response.status().code(), is(301)); + try (Http1ClientResponse response = client.post("/api") + .followRedirects(false) + .submit(WriteableMultiPart.builder() + .addPart(writeablePart("file[]", "foo.txt", file)) + .build())) { + assertThat(response.status(), is(Http.Status.MOVED_PERMANENTLY_301)); + } } @Test @@ -96,46 +80,47 @@ public void testUpload() throws IOException { public void testStreamUpload() throws IOException { Path file = Files.writeString(Files.createTempFile(null, null), "stream bar\n"); Path file2 = Files.writeString(Files.createTempFile(null, null), "stream foo\n"); - WebClientResponse response = webClient - .post() - .queryParam("stream", "true") - .contentType(MediaTypes.MULTIPART_FORM_DATA) - .submit(FileFormParams.builder() - .addFile("file[]", "streamed-foo.txt", file) - .addFile("otherPart", "streamed-foo2.txt", file2) - .build()) - .await(TIMEOUT); - assertThat(response.status().code(), is(301)); + try (Http1ClientResponse response = client.post("/api") + .queryParam("stream", "true") + .followRedirects(false) + .submit(WriteableMultiPart + .builder() + .addPart(writeablePart("file[]", "streamed-foo.txt", file)) + .addPart(writeablePart("otherPart", "streamed-foo2.txt", file2)) + .build())) { + assertThat(response.status(), is(Http.Status.MOVED_PERMANENTLY_301)); + } } @Test @Order(3) public void testList() { - WebClientResponse response = webClient - .get() - .contentType(MediaTypes.APPLICATION_JSON) - .request() - .await(TIMEOUT); - assertThat(response.status().code(), Matchers.is(200)); - JsonObject json = response.content().as(JsonObject.class).await(TIMEOUT); - assertThat(json, Matchers.is(notNullValue())); - List files = json.getJsonArray("files").getValuesAs(v -> ((JsonString) v).getString()); - assertThat(files, hasItem("foo.txt")); + try (Http1ClientResponse response = client.get("/api").request()) { + assertThat(response.status(), is(Http.Status.OK_200)); + JsonObject json = response.as(JsonObject.class); + assertThat(json, Matchers.is(notNullValue())); + List files = json.getJsonArray("files").getValuesAs(v -> ((JsonString) v).getString()); + assertThat(files, hasItem("foo.txt")); + } } @Test @Order(4) public void testDownload() { - WebClientResponse response = webClient - .get() - .path("foo.txt") - .accept(MediaTypes.APPLICATION_OCTET_STREAM) - .request() - .await(TIMEOUT); - assertThat(response.status().code(), is(200)); - assertThat(response.headers().first(Http.Header.CONTENT_DISPOSITION).orElse(null), - containsString("filename=\"foo.txt\"")); - byte[] bytes = response.content().as(byte[].class).await(TIMEOUT); - assertThat(new String(bytes, StandardCharsets.UTF_8), Matchers.is("bar\n")); + try (Http1ClientResponse response = client.get("/api").path("foo.txt").request()) { + assertThat(response.status(), is(Http.Status.OK_200)); + assertThat(response.headers().first(Http.Header.CONTENT_DISPOSITION).orElse(null), + containsString("filename=\"foo.txt\"")); + byte[] bytes = response.as(byte[].class); + assertThat(new String(bytes, StandardCharsets.UTF_8), Matchers.is("bar\n")); + } + } + + private WriteablePart writeablePart(String partName, String fileName, Path filePath) throws IOException { + return WriteablePart.builder(partName) + .fileName(fileName) + .content(Files.readAllBytes(filePath)) + .contentType(MediaTypes.MULTIPART_FORM_DATA) + .build(); } } diff --git a/examples/messaging/jms-websocket-mp/pom.xml b/examples/messaging/jms-websocket-mp/pom.xml index 8759c238326..844215011a3 100644 --- a/examples/messaging/jms-websocket-mp/pom.xml +++ b/examples/messaging/jms-websocket-mp/pom.xml @@ -49,7 +49,7 @@ helidon-microprofile-websocket - org.jboss + io.smallrye jandex runtime true diff --git a/examples/messaging/kafka-websocket-mp/pom.xml b/examples/messaging/kafka-websocket-mp/pom.xml index da2d37e95f5..a959d70824d 100644 --- a/examples/messaging/kafka-websocket-mp/pom.xml +++ b/examples/messaging/kafka-websocket-mp/pom.xml @@ -49,7 +49,7 @@ helidon-microprofile-websocket - org.jboss + io.smallrye jandex runtime true @@ -68,7 +68,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/messaging/oracle-aq-websocket-mp/pom.xml b/examples/messaging/oracle-aq-websocket-mp/pom.xml index 8a5cf3155dd..8b6534c53eb 100644 --- a/examples/messaging/oracle-aq-websocket-mp/pom.xml +++ b/examples/messaging/oracle-aq-websocket-mp/pom.xml @@ -54,7 +54,7 @@ helidon-microprofile-websocket - org.jboss + io.smallrye jandex runtime true diff --git a/examples/messaging/weblogic-jms-mp/pom.xml b/examples/messaging/weblogic-jms-mp/pom.xml index 9decd16cf7b..38ebd7443a2 100644 --- a/examples/messaging/weblogic-jms-mp/pom.xml +++ b/examples/messaging/weblogic-jms-mp/pom.xml @@ -51,7 +51,7 @@ - org.jboss + io.smallrye jandex runtime true @@ -70,7 +70,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/metrics/filtering/mp/src/test/java/io/helidon/examples/metrics/filtering/mp/MainTest.java b/examples/metrics/filtering/mp/src/test/java/io/helidon/examples/metrics/filtering/mp/MainTest.java index e439e784baf..e3963a9e0c4 100644 --- a/examples/metrics/filtering/mp/src/test/java/io/helidon/examples/metrics/filtering/mp/MainTest.java +++ b/examples/metrics/filtering/mp/src/test/java/io/helidon/examples/metrics/filtering/mp/MainTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,8 +30,6 @@ import static org.hamcrest.Matchers.is; @HelidonTest -@Disabled("3.0.0-JAKARTA") // OpenAPI -// Caused by: java.lang.NoSuchMethodError: 'java.util.List org.jboss.jandex.ClassInfo.unsortedFields()' public class MainTest { @Inject diff --git a/examples/microprofile/bean-validation/pom.xml b/examples/microprofile/bean-validation/pom.xml index b28f866c505..c86e0a0ec9c 100644 --- a/examples/microprofile/bean-validation/pom.xml +++ b/examples/microprofile/bean-validation/pom.xml @@ -42,7 +42,7 @@ - org.jboss + io.smallrye jandex runtime true @@ -77,7 +77,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/microprofile/cors/pom.xml b/examples/microprofile/cors/pom.xml index bcb633a867b..290f966a0dc 100644 --- a/examples/microprofile/cors/pom.xml +++ b/examples/microprofile/cors/pom.xml @@ -50,7 +50,7 @@ runtime - org.jboss + io.smallrye jandex runtime true @@ -70,6 +70,11 @@ helidon-reactive-webclient test + + io.helidon.reactive.media + helidon-reactive-media-jsonp + test + @@ -84,7 +89,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/microprofile/cors/src/test/java/io/helidon/microprofile/examples/cors/TestCORS.java b/examples/microprofile/cors/src/test/java/io/helidon/microprofile/examples/cors/TestCORS.java index e53e6c31ac0..9ba9d698687 100644 --- a/examples/microprofile/cors/src/test/java/io/helidon/microprofile/examples/cors/TestCORS.java +++ b/examples/microprofile/cors/src/test/java/io/helidon/microprofile/examples/cors/TestCORS.java @@ -53,7 +53,7 @@ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @Disabled("3.0.0-JAKARTA") // OpenAPI: Caused by: java.lang.NoSuchMethodError: -// 'java.util.List org.jboss.jandex.ClassInfo.unsortedFields()' +// 'java.util.List io.smallrye.jandex.ClassInfo.unsortedFields()' public class TestCORS { private static final String JSON_MESSAGE_RESPONSE_LABEL = "message"; diff --git a/examples/microprofile/graphql/pom.xml b/examples/microprofile/graphql/pom.xml index c966a1b09ff..2ac858618cf 100644 --- a/examples/microprofile/graphql/pom.xml +++ b/examples/microprofile/graphql/pom.xml @@ -55,7 +55,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/microprofile/hello-world-explicit/pom.xml b/examples/microprofile/hello-world-explicit/pom.xml index e4a8218a37e..2c831329564 100644 --- a/examples/microprofile/hello-world-explicit/pom.xml +++ b/examples/microprofile/hello-world-explicit/pom.xml @@ -44,7 +44,7 @@ helidon-microprofile - org.jboss + io.smallrye jandex runtime true @@ -63,7 +63,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/microprofile/hello-world-implicit/pom.xml b/examples/microprofile/hello-world-implicit/pom.xml index 90dc6ac321a..cdc5df5d3a5 100644 --- a/examples/microprofile/hello-world-implicit/pom.xml +++ b/examples/microprofile/hello-world-implicit/pom.xml @@ -41,7 +41,7 @@ helidon-microprofile - org.jboss + io.smallrye jandex runtime true @@ -75,7 +75,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/microprofile/hello-world-implicit/src/test/java/io/helidon/microprofile/example/helloworld/implicit/ImplicitHelloWorldTest.java b/examples/microprofile/hello-world-implicit/src/test/java/io/helidon/microprofile/example/helloworld/implicit/ImplicitHelloWorldTest.java index 2e5e7e3b072..3f58fbfeeb5 100644 --- a/examples/microprofile/hello-world-implicit/src/test/java/io/helidon/microprofile/example/helloworld/implicit/ImplicitHelloWorldTest.java +++ b/examples/microprofile/hello-world-implicit/src/test/java/io/helidon/microprofile/example/helloworld/implicit/ImplicitHelloWorldTest.java @@ -33,7 +33,7 @@ */ @HelidonTest @Disabled("3.0.0-JAKARTA") // OpenAPI: Caused by: java.lang.NoSuchMethodError: - // 'java.util.List org.jboss.jandex.ClassInfo.unsortedFields()' + // 'java.util.List io.smallrye.jandex.ClassInfo.unsortedFields()' class ImplicitHelloWorldTest { private final WebTarget target; diff --git a/examples/microprofile/http-status-count-mp/pom.xml b/examples/microprofile/http-status-count-mp/pom.xml index db1dcce4082..5171b1d2d4e 100644 --- a/examples/microprofile/http-status-count-mp/pom.xml +++ b/examples/microprofile/http-status-count-mp/pom.xml @@ -59,7 +59,7 @@ helidon-microprofile-health - org.jboss + io.smallrye jandex runtime @@ -105,7 +105,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/microprofile/idcs/pom.xml b/examples/microprofile/idcs/pom.xml index ec50df7dae0..17bb61eb486 100644 --- a/examples/microprofile/idcs/pom.xml +++ b/examples/microprofile/idcs/pom.xml @@ -53,7 +53,7 @@ helidon-security-providers-idcs-mapper - org.jboss + io.smallrye jandex runtime true @@ -72,7 +72,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/microprofile/lra/pom.xml b/examples/microprofile/lra/pom.xml index 5f68a2f2f17..6eb8bedbf25 100644 --- a/examples/microprofile/lra/pom.xml +++ b/examples/microprofile/lra/pom.xml @@ -53,7 +53,7 @@ helidon-lra-coordinator-narayana-client - org.jboss + io.smallrye jandex runtime true @@ -72,7 +72,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/microprofile/messaging-sse/pom.xml b/examples/microprofile/messaging-sse/pom.xml index f0c55af9d56..bf40012c526 100644 --- a/examples/microprofile/messaging-sse/pom.xml +++ b/examples/microprofile/messaging-sse/pom.xml @@ -49,7 +49,7 @@ jersey-media-sse - org.jboss + io.smallrye jandex runtime true @@ -91,7 +91,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/microprofile/multipart/pom.xml b/examples/microprofile/multipart/pom.xml index 85c414f54e8..caeffab5cbe 100644 --- a/examples/microprofile/multipart/pom.xml +++ b/examples/microprofile/multipart/pom.xml @@ -43,7 +43,7 @@ jersey-media-multipart - org.jboss + io.smallrye jandex runtime true @@ -76,7 +76,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/microprofile/multiport/pom.xml b/examples/microprofile/multiport/pom.xml index 19fb7d16043..d5548f2c1e5 100644 --- a/examples/microprofile/multiport/pom.xml +++ b/examples/microprofile/multiport/pom.xml @@ -79,7 +79,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/microprofile/oidc/pom.xml b/examples/microprofile/oidc/pom.xml index a64dbd5f279..559c68cb3f4 100644 --- a/examples/microprofile/oidc/pom.xml +++ b/examples/microprofile/oidc/pom.xml @@ -48,7 +48,7 @@ helidon-microprofile-oidc - org.jboss + io.smallrye jandex runtime true @@ -67,7 +67,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/microprofile/openapi-basic/pom.xml b/examples/microprofile/openapi-basic/pom.xml index 47ce4e2c72d..21422f0150c 100644 --- a/examples/microprofile/openapi-basic/pom.xml +++ b/examples/microprofile/openapi-basic/pom.xml @@ -50,7 +50,7 @@ runtime - org.jboss + io.smallrye jandex runtime true @@ -79,7 +79,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/microprofile/openapi-basic/src/test/java/io/helidon/microprofile/examples/openapi/basic/MainTest.java b/examples/microprofile/openapi-basic/src/test/java/io/helidon/microprofile/examples/openapi/basic/MainTest.java index fb023bf8f7f..ca3d7f4c48e 100644 --- a/examples/microprofile/openapi-basic/src/test/java/io/helidon/microprofile/examples/openapi/basic/MainTest.java +++ b/examples/microprofile/openapi-basic/src/test/java/io/helidon/microprofile/examples/openapi/basic/MainTest.java @@ -32,14 +32,11 @@ import jakarta.ws.rs.core.Response; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -@Disabled("3.0.0-JAKARTA") // OpenAPI: Caused by: java.lang.NoSuchMethodError: - // 'java.util.List org.jboss.jandex.ClassInfo.unsortedFields()' class MainTest { private static Server server; diff --git a/examples/microprofile/security/pom.xml b/examples/microprofile/security/pom.xml index 35e6d264491..c46f54477df 100644 --- a/examples/microprofile/security/pom.xml +++ b/examples/microprofile/security/pom.xml @@ -45,7 +45,7 @@ helidon-microprofile - org.jboss + io.smallrye jandex runtime true @@ -74,7 +74,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/microprofile/static-content/pom.xml b/examples/microprofile/static-content/pom.xml index 7089edbd1f5..27abb465b5a 100644 --- a/examples/microprofile/static-content/pom.xml +++ b/examples/microprofile/static-content/pom.xml @@ -45,7 +45,7 @@ helidon-microprofile - org.jboss + io.smallrye jandex runtime true @@ -74,7 +74,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/microprofile/tls/pom.xml b/examples/microprofile/tls/pom.xml index f01fd898f7a..6ef6674e395 100644 --- a/examples/microprofile/tls/pom.xml +++ b/examples/microprofile/tls/pom.xml @@ -39,7 +39,7 @@ helidon-microprofile - org.jboss + io.smallrye jandex runtime true @@ -69,7 +69,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/microprofile/websocket/pom.xml b/examples/microprofile/websocket/pom.xml index 94692e04a2d..8f86e5060bb 100644 --- a/examples/microprofile/websocket/pom.xml +++ b/examples/microprofile/websocket/pom.xml @@ -45,7 +45,7 @@ helidon-microprofile-websocket - org.jboss + io.smallrye jandex runtime true @@ -79,7 +79,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/pico/README.md b/examples/pico/README.md new file mode 100644 index 00000000000..ebec97c4343 --- /dev/null +++ b/examples/pico/README.md @@ -0,0 +1,12 @@ + +# Helidon Pico Examples + +Each subdirectory contains example code that highlights specific aspects of +Helidon Pico. + +Suggested path to follow: +1. [basics](./basics) +2. [providers](./providers) +3. [configdriven](./configdriven) +4. [interceptors](./interceptors) +5. [application](./application) diff --git a/examples/pico/application/README.md b/examples/pico/application/README.md new file mode 100644 index 00000000000..2d68bb49021 --- /dev/null +++ b/examples/pico/application/README.md @@ -0,0 +1,50 @@ +# Helidon Pico Application Example + +This example shows how a multi-module application can be created using Helidon Pico. The +[Main.java](./src/main/java/io/helidon/examples/pico/application/Main.java) class shows: + +* multi-module usage (i.e., this module amalgamates [basics](../basics), [providers](../providers), [configdriven](../configdriven), and [interceptors](../interceptors) ). +* compile-time generation of the DI model for the entire multi-module project using the _pico-maven-plugin_ (see [pom.xml](./pom.xml)). +* TestingSupport in [ApplicationTest](src/test/java/io/helidon/examples/pico/application/PicoApplicationTest.java) + +## Build and run + +```bash +mvn package +java -jar target/helidon-examples-pico-application.jar +``` + +Expected Output: +``` +Startup service providers (ranked according to weight, pre-activated): [ToolBox:INIT, CircularSaw:INIT, NailGun:INIT, TableSaw:INIT] +Highest weighted service provider: NailGun:INIT +----- +Nail Gun: (nail provider=NailProvider:INIT); initialized +Highest weighted service provider (after activation): io.helidon.examples.pico.providers.NailGun@7cbd9d24 +----- +Preferred Big Tool: Big Hammer +Optional Little Hammer: Optional[Little Hammer] +----- +ToolBox Contents: +Hammer:INIT +BigHammer:ACTIVE +LittleHammer:ACTIVE +Drill{root}:PENDING +AngleGrinderSaw:INIT +CircularSaw:INIT +HandSaw:INIT +NailGun:ACTIVE +TableSaw:INIT +----- +io.helidon.examples.pico.providers.CircularSaw:: will be injected with Optional.empty +Circular Saw: (blade=null); initialized +io.helidon.examples.pico.providers.TableSaw:: will be injected with Optional[LARGE Blade] +Table Saw: (blade=LARGE Blade); initialized +All service providers (after all activations): [ToolBox:ACTIVE, CircularSaw:ACTIVE, NailGun:ACTIVE, TableSaw:ACTIVE] +----- +Service lookup count: 2 +``` + +While the output of this example may look similar to the previous [providers](../providers) example, the implementation is different since this example builds (at compile time) [Application.java](target/generated-sources/annotations/io/helidon/examples/pico/application/Pico$$Application.java). This establishes direct bindings to each and every injection point in your application avoiding runtime resolution with the exception for truly dynamic runtime providers (i.e., anything that is config-driven services or _Provider_ type implementations). + +Note that the lookup count is 2 based upon the direct lookup calls used in the delegated [Main](../basics/src/main/java/io/helidon/examples/pico/basics/Main.java). diff --git a/examples/pico/application/pom.xml b/examples/pico/application/pom.xml new file mode 100644 index 00000000000..6d69509e3d1 --- /dev/null +++ b/examples/pico/application/pom.xml @@ -0,0 +1,117 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-nima + 4.0.0-SNAPSHOT + ../../../applications/nima/pom.xml + + io.helidon.examples.pico + helidon-examples-pico-application + Helidon Pico Examples Application + + + Example usages of a Pico Application. + + + + io.helidon.examples.pico.providers.Main + + + + + io.helidon.examples.pico + helidon-examples-pico-providers + ${helidon.version} + + + io.helidon.examples.pico + helidon-examples-pico-configdriven + ${helidon.version} + + + jakarta.annotation + jakarta.annotation-api + provided + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + io.helidon.pico + helidon-pico-testing + test + + + + + + + io.helidon.pico + helidon-pico-maven-plugin + ${helidon.version} + + + compile + compile + + application-create + + + + testCompile + test-compile + + test-application-create + + + + + io.helidon.examples.pico.application + + NAMED + + io.helidon.examples.pico.providers.BladeProvider + io.helidon.examples.pico.providers.NailProvider + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/pico/application/src/main/java/io/helidon/examples/pico/application/Main.java b/examples/pico/application/src/main/java/io/helidon/examples/pico/application/Main.java new file mode 100644 index 00000000000..e1226b1246b --- /dev/null +++ b/examples/pico/application/src/main/java/io/helidon/examples/pico/application/Main.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.application; + +import io.helidon.pico.api.Metrics; +import io.helidon.pico.api.PicoServices; + +/** + * Application example. Uses the same {@code main()} as {@link io.helidon.examples.pico.basics.Main}. + */ +public class Main extends io.helidon.examples.pico.basics.Main { + + /** + * Executes the example. + * + * @param args arguments + */ + public static void main(String... args) { + io.helidon.examples.pico.basics.Main.main(args); + + Metrics metrics = PicoServices.picoServices().orElseThrow().metrics().get(); + System.out.println("Service lookup count: " + metrics.lookupCount().get()); + } + +} diff --git a/examples/pico/application/src/main/java/io/helidon/examples/pico/application/package-info.java b/examples/pico/application/src/main/java/io/helidon/examples/pico/application/package-info.java new file mode 100644 index 00000000000..27ac09e0919 --- /dev/null +++ b/examples/pico/application/src/main/java/io/helidon/examples/pico/application/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Examples of multi-module Application generation in Pico. + */ +package io.helidon.examples.pico.application; diff --git a/examples/webserver/jersey/src/main/resources/logging.properties b/examples/pico/application/src/main/resources/logging.properties similarity index 58% rename from examples/webserver/jersey/src/main/resources/logging.properties rename to examples/pico/application/src/main/resources/logging.properties index 8c6d7322d28..bd06e0ed087 100644 --- a/examples/webserver/jersey/src/main/resources/logging.properties +++ b/examples/pico/application/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2017, 2022 Oracle and/or its affiliates. +# Copyright (c) 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,12 +15,12 @@ # -#All attributes details -handlers=io.helidon.logging.jul.HelidonConsoleHandler -java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n +handlers = java.util.logging.ConsoleHandler -#All log level details -.level=WARNING +java.util.logging.ConsoleHandler.level = FINEST +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = [%1$tc] %4$s: %2$s - %5$s %6$s%n -io.helidon.reactive.webserver.level=INFO -org.glassfish.jersey.internal.Errors.level=SEVERE +.level = INFO +io.helidon.config.level = WARNING +io.helidon.config.examples.level = FINEST diff --git a/examples/pico/application/src/test/java/io/helidon/examples/pico/application/PicoApplicationTest.java b/examples/pico/application/src/test/java/io/helidon/examples/pico/application/PicoApplicationTest.java new file mode 100644 index 00000000000..a3c45923dc0 --- /dev/null +++ b/examples/pico/application/src/test/java/io/helidon/examples/pico/application/PicoApplicationTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.application; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.pico.api.PicoServices; +import io.helidon.pico.api.Services; +import io.helidon.pico.testing.PicoTestingSupport; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.pico.testing.PicoTestingSupport.testableServices; + +class PicoApplicationTest { + + protected PicoServices picoServices; + protected Services services; + + @AfterAll + static void tearDown() { + PicoTestingSupport.resetAll(); + } + + protected void resetWith(Config config) { + PicoTestingSupport.resetAll(); + this.picoServices = testableServices(config); + this.services = picoServices.services(); + } + + @Test + void main() { + Config config = Config.builder() + .addSource(ConfigSources.classpath("application.yaml")) + .disableSystemPropertiesSource() + .disableEnvironmentVariablesSource() + .build(); + resetWith(config); + Main.main(); + } + +} diff --git a/examples/pico/basics/README.md b/examples/pico/basics/README.md new file mode 100644 index 00000000000..b91b7aae5ff --- /dev/null +++ b/examples/pico/basics/README.md @@ -0,0 +1,30 @@ +# Helidon Pico Basic Example + +This example shows the basics of using Helidon Pico. The +[Main.java](./src/main/java/io/helidon/examples/pico/basics/Main.java) class shows: + +* programmatic lookup of services in Pico's Services registry in [Main](./src/main/java/io/helidon/examples/pico/basics/Main.java). +* declarative injection in [ToolBox.java](./src/main/java/io/helidon/examples/pico/basics/ToolBox.java). +* lifecycle via PostConstruct and RunLevel in [Main](./src/main/java/io/helidon/examples/pico/basics/Main.java). +* annotation processing and source code generation (see [pom.xml](pom.xml) and [generated-sources](./target/generated-sources/annotations/io/helidon/examples/pico/basics)). + +## Build and run + +```bash +mvn package +java -jar target/helidon-examples-pico-basics.jar +``` + +Expected Output: +``` +Startup service providers (ranked according to weight, pre-activated): [ToolBox:INIT] +Highest weighted service provider: ToolBox:INIT +Preferred (highest weighted) 'Big' Tool: Big Hammer +Optional 'Little' Hammer: Optional[Little Hammer] +Tools in the virtual ToolBox: + tool: Hammer:INIT + tool: BigHammer:ACTIVE + tool: LittleHammer:ACTIVE +Highest weighted service provider (after activation): ToolBox +All service providers (after all activations): [ToolBox:ACTIVE] +``` diff --git a/examples/pico/basics/pom.xml b/examples/pico/basics/pom.xml new file mode 100644 index 00000000000..d8f944b91e6 --- /dev/null +++ b/examples/pico/basics/pom.xml @@ -0,0 +1,88 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-nima + 4.0.0-SNAPSHOT + ../../../applications/nima/pom.xml + + io.helidon.examples.pico + helidon-examples-pico-basics + Helidon Pico Examples Basics + + + Examples of programmatic and declarative usages of Pico. + + + + io.helidon.examples.pico.basics.Main + + + + + io.helidon.pico + helidon-pico-api + + + io.helidon.pico + helidon-pico-runtime + + + jakarta.annotation + jakarta.annotation-api + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + -Apico.autoAddNonContractInterfaces=false + -Apico.debug=false + + true + + + io.helidon.pico + helidon-pico-processor + ${helidon.version} + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/Big.java b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/Big.java new file mode 100644 index 00000000000..974cb24d366 --- /dev/null +++ b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/Big.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.basics; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import jakarta.inject.Qualifier; + +/** + * Custom annotation. + */ +@Qualifier +@Retention(RetentionPolicy.CLASS) +public @interface Big { + +} diff --git a/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/BigHammer.java b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/BigHammer.java new file mode 100644 index 00000000000..39b3411f8dd --- /dev/null +++ b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/BigHammer.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.basics; + +import jakarta.inject.Singleton; + +@Big +@Singleton +class BigHammer extends Hammer { + + @Override + public String name() { + return "Big " + super.name(); + } + +} diff --git a/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/Hammer.java b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/Hammer.java new file mode 100644 index 00000000000..74b48ad093c --- /dev/null +++ b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/Hammer.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.basics; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; + +import jakarta.inject.Singleton; + +/** + * By adding the {@link Singleton} annotation results in Hammer becoming a Pico service. Services can be looked up + * programmatically or declaratively injected via {@link jakarta.inject.Inject}. + *

+ * Here {@link Weight} is used that is higher than the default, making it more preferred in weighted rankings. + */ +@Singleton +@Weight(Weighted.DEFAULT_WEIGHT + 1) +class Hammer implements Tool { + + @Override + public String name() { + return "Hammer"; + } + + @Override + public String toString() { + return name(); + } + +} diff --git a/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/Little.java b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/Little.java new file mode 100644 index 00000000000..3d2b75b8da7 --- /dev/null +++ b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/Little.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.basics; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import jakarta.inject.Qualifier; + +/** + * Custom annotation. + */ +@Qualifier +@Retention(RetentionPolicy.CLASS) +public @interface Little { +} diff --git a/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/LittleHammer.java b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/LittleHammer.java new file mode 100644 index 00000000000..7facc0ac59e --- /dev/null +++ b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/LittleHammer.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.basics; + +import jakarta.inject.Singleton; + +@Little +@Singleton +class LittleHammer extends Hammer { + + @Override + public String name() { + return "Little " + super.name(); + } + +} diff --git a/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/Main.java b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/Main.java new file mode 100644 index 00000000000..62275493610 --- /dev/null +++ b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/Main.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.basics; + +import java.util.List; + +import io.helidon.pico.api.PicoServices; +import io.helidon.pico.api.RunLevel; +import io.helidon.pico.api.ServiceInfoCriteria; +import io.helidon.pico.api.ServiceInfoCriteriaDefault; +import io.helidon.pico.api.ServiceProvider; +import io.helidon.pico.api.Services; + +/** + * Basics example. + */ +public class Main { + + /** + * Executes the example. + * + * @param args arguments + */ + public static void main(String... args) { + Services services = PicoServices.realizedServices(); + + // 0. Demonstrates programmatic lookup from Pico's Services registry. + // 1. when a service is being managed by a DI provider (like Pico) it should be "looked up" or injected instead of new'ed + // 2. Notice we get a ServiceProvider - service providers allow for lazy initialization + ServiceInfoCriteria criteria = ServiceInfoCriteriaDefault.builder() + .runLevel(RunLevel.STARTUP) + .build(); + + List> startupServiceProviders = services.lookupAll(criteria); + System.out.println("Startup service providers (ranked according to weight, pre-activated): " + startupServiceProviders); + + ServiceProvider highestWeightedServiceProvider = services.lookupFirst(criteria); + System.out.println("Highest weighted service provider: " + highestWeightedServiceProvider); + + // trigger lazy activations for the highest weighted service provider + System.out.println("Highest weighted service provider (after activation): " + highestWeightedServiceProvider.get()); + + // trigger all activations for the (remaining unactivated) startup service providers + startupServiceProviders.forEach(ServiceProvider::get); + System.out.println("All service providers (after all activations): " + startupServiceProviders); + } + +} diff --git a/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/Tool.java b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/Tool.java new file mode 100644 index 00000000000..339ae70d629 --- /dev/null +++ b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/Tool.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.basics; + +import io.helidon.pico.api.Contract; + +/** + * An example Tool interface contract. + */ +@Contract +public interface Tool { + + /** + * The name of the tool. + * + * @return name of the tool + */ + String name(); + +} diff --git a/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/ToolBox.java b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/ToolBox.java new file mode 100644 index 00000000000..0d167aca57f --- /dev/null +++ b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/ToolBox.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.basics; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.pico.api.RunLevel; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +/** + * By adding the {@link Singleton} annotation results in ToolBox becoming a Pico service. Services can be looked up + * programmatically or declaratively injected via {@link jakarta.inject.Inject}. + *

+ * Here {@link Weight} is used that is higher than the default, making it more preferred in weighted rankings. + */ +@Singleton +@RunLevel(RunLevel.STARTUP) +@Weight(Weighted.DEFAULT_WEIGHT + 1) +public class ToolBox { + + private final List> allToolProviders; + private Tool preferredBigTool; + + // Pico field injection is supported for non-static, non-private methods (but not recommended) + // Here we are using it to also showcase for Optional usages. + @Inject Optional optionalLittleHammer; + + /** + * Here the constructor injects all {@link Tool} provider instances available. {@link Provider} is used to allow lazy + * activation of services until {@link Provider#get()} is called. + * + * @param allToolProviders all tool providers + */ + @Inject + ToolBox(List> allToolProviders) { + this.allToolProviders = Objects.requireNonNull(allToolProviders); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + /** + * Example of setter based injection. + * + * @param preferredBigTool the preferred big tool + */ + @Inject + @SuppressWarnings("unused") + void setPreferredBigTool(@Big Tool preferredBigTool) { + this.preferredBigTool = Objects.requireNonNull(preferredBigTool); + } + + /** + * This method will be called by Pico after this instance is lazily initialized (because this is the {@link PostConstruct} + * method). + */ + @PostConstruct + @SuppressWarnings("unused") + void init() { + System.out.println("Preferred (highest weighted) 'Big' Tool: " + preferredBigTool); + System.out.println("Optional 'Little' Hammer: " + optionalLittleHammer); + + printToolBoxContents(); + } + + public void printToolBoxContents() { + System.out.println("Tools in the virtual ToolBox:"); + for (Provider tool : allToolProviders) { + System.out.println(" tool: " + tool); + } + } + +} diff --git a/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/package-info.java b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/package-info.java new file mode 100644 index 00000000000..92acf790e40 --- /dev/null +++ b/examples/pico/basics/src/main/java/io/helidon/examples/pico/basics/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Examples of programmatic and declarative usages of Pico. + */ +package io.helidon.examples.pico.basics; diff --git a/tests/integration/native-image/se-1/src/main/resources/application.yaml b/examples/pico/basics/src/main/resources/logging.properties similarity index 58% rename from tests/integration/native-image/se-1/src/main/resources/application.yaml rename to examples/pico/basics/src/main/resources/logging.properties index 9346a8f9e12..bd06e0ed087 100644 --- a/tests/integration/native-image/se-1/src/main/resources/application.yaml +++ b/examples/pico/basics/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, 2023 Oracle and/or its affiliates. +# Copyright (c) 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,27 +14,13 @@ # limitations under the License. # -app: - greeting: "Hello" -color: - tint: RED +handlers = java.util.logging.ConsoleHandler -server: - port: 7076 - host: 0.0.0.0 +java.util.logging.ConsoleHandler.level = FINEST +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = [%1$tc] %4$s: %2$s - %5$s %6$s%n -tracing: - service: "helidon-se" - protocol: "http" - host: "localhost" - port: 7076 - api-version: 2 - # mocked zipkin - path: "/zipkin/api/v2/spans" - -client: - follow-redirects: true - max-redirects: 5 - services: - tracing: \ No newline at end of file +.level = INFO +io.helidon.config.level = WARNING +io.helidon.config.examples.level = FINEST diff --git a/examples/pico/configdriven/README.md b/examples/pico/configdriven/README.md new file mode 100644 index 00000000000..3d8dcd3f0a5 --- /dev/null +++ b/examples/pico/configdriven/README.md @@ -0,0 +1,28 @@ +# Helidon Pico Config-Driven Example + +This example shows the basics of using Helidon Pico's Config-Driven Services. The +[Main.java](./src/main/java/io/helidon/examples/pico/configdriven/Main.java) class shows: + +* setting up the bootstrap [configuration](./src/main/resources/application.yaml). +* [ConfigBean](src/main/java/io/helidon/examples/pico/configdriven/DrillConfig.java). +* [ConfiguredBy](src/main/java/io/helidon/examples/pico/configdriven/Drill.java) Services. +* annotation processing and source code generation (see [pom.xml](pom.xml) and [generated-sources](./target/generated-sources/annotations/io/helidon/examples/pico/configdriven)). + +## Build and run + +```bash +mvn package +java -jar target/helidon-examples-pico-configdriven.jar +``` + +Expected Output: +``` +Preferred (highest weighted) 'Big' Tool: Big Hammer +Optional 'Little' Hammer: Optional[Little Hammer] +Tools in the virtual ToolBox: + tool: Hammer:INIT + tool: BigHammer:ACTIVE + tool: LittleHammer:ACTIVE + tool: Drill{Hand}:PENDING + tool: Drill{Impact}:PENDING +``` diff --git a/examples/pico/configdriven/pom.xml b/examples/pico/configdriven/pom.xml new file mode 100644 index 00000000000..d3b18a3830a --- /dev/null +++ b/examples/pico/configdriven/pom.xml @@ -0,0 +1,96 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-nima + 4.0.0-SNAPSHOT + ../../../applications/nima/pom.xml + + io.helidon.examples.pico + helidon-examples-pico-configdriven + Helidon Pico Examples Config-Driven + + + Examples of Config-driven services in Pico. + + + + io.helidon.examples.pico.configdriven.Main + + + + + io.helidon.examples.pico + helidon-examples-pico-basics + ${helidon.version} + + + io.helidon.pico.configdriven + helidon-pico-configdriven-runtime + + + io.helidon.config + helidon-config-yaml + + + jakarta.annotation + jakarta.annotation-api + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + + io.helidon.pico.configdriven + helidon-pico-configdriven-processor + ${helidon.version} + + + + io.helidon.builder + helidon-builder-config-processor + ${helidon.version} + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/pico/configdriven/src/main/java/io/helidon/examples/pico/configdriven/Drill.java b/examples/pico/configdriven/src/main/java/io/helidon/examples/pico/configdriven/Drill.java new file mode 100644 index 00000000000..b840af85c3a --- /dev/null +++ b/examples/pico/configdriven/src/main/java/io/helidon/examples/pico/configdriven/Drill.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.configdriven; + +import java.util.Objects; + +import io.helidon.examples.pico.basics.Tool; +import io.helidon.pico.configdriven.api.ConfiguredBy; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; + +@ConfiguredBy(DrillConfig.class) +class Drill implements Tool { + + private final DrillConfig cfg; + + @Inject + Drill(DrillConfig cfg) { + this.cfg = Objects.requireNonNull(cfg); + } + + @Override + public String name() { + return cfg.name(); + } + + @PostConstruct + @SuppressWarnings("unused") + void init() { + System.out.println(name() + "; initialized"); + } + +} diff --git a/examples/pico/configdriven/src/main/java/io/helidon/examples/pico/configdriven/DrillConfig.java b/examples/pico/configdriven/src/main/java/io/helidon/examples/pico/configdriven/DrillConfig.java new file mode 100644 index 00000000000..74c7b8b82a8 --- /dev/null +++ b/examples/pico/configdriven/src/main/java/io/helidon/examples/pico/configdriven/DrillConfig.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.configdriven; + +import io.helidon.builder.config.ConfigBean; + +@ConfigBean(repeatable = true) +public interface DrillConfig { + + String name(); + +} diff --git a/examples/pico/configdriven/src/main/java/io/helidon/examples/pico/configdriven/Main.java b/examples/pico/configdriven/src/main/java/io/helidon/examples/pico/configdriven/Main.java new file mode 100644 index 00000000000..738b5ab0beb --- /dev/null +++ b/examples/pico/configdriven/src/main/java/io/helidon/examples/pico/configdriven/Main.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.configdriven; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.examples.pico.basics.ToolBox; +import io.helidon.pico.api.BootstrapDefault; +import io.helidon.pico.api.PicoServices; +import io.helidon.pico.api.Services; + +/** + * Config-driven example. + */ +public class Main { + + /** + * Executes the example. + * + * @param args arguments + */ + public static void main(String... args) { + // we need to first initialize Pico - informing Pico where to find the application's Config + Config config = Config.builder() + .addSource(ConfigSources.classpath("application.yaml")) + .disableSystemPropertiesSource() + .disableEnvironmentVariablesSource() + .build(); + BootstrapDefault bootstrap = BootstrapDefault.builder() + .config(config) + .build(); + PicoServices.globalBootstrap(bootstrap); + + // this drives config-driven service activations (see the contents of the toolbox being output) + Services services = PicoServices.realizedServices(); + + // this will trigger the PostConstruct method to display the contents of the toolbox + services.lookupFirst(ToolBox.class).get(); + } + +} diff --git a/examples/pico/configdriven/src/main/java/io/helidon/examples/pico/configdriven/package-info.java b/examples/pico/configdriven/src/main/java/io/helidon/examples/pico/configdriven/package-info.java new file mode 100644 index 00000000000..0b5b1906098 --- /dev/null +++ b/examples/pico/configdriven/src/main/java/io/helidon/examples/pico/configdriven/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Examples of Config-Driven Services in Pico. + */ +package io.helidon.examples.pico.configdriven; diff --git a/examples/integrations/neo4j/neo4j-mp/src/test/resources/META-INF/microprofile-config.properties b/examples/pico/configdriven/src/main/resources/application.yaml similarity index 70% rename from examples/integrations/neo4j/neo4j-mp/src/test/resources/META-INF/microprofile-config.properties rename to examples/pico/configdriven/src/main/resources/application.yaml index 0f7f7a9d224..4c37d537241 100644 --- a/examples/integrations/neo4j/neo4j-mp/src/test/resources/META-INF/microprofile-config.properties +++ b/examples/pico/configdriven/src/main/resources/application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2021 Oracle and/or its affiliates. +# Copyright (c) 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,9 +14,12 @@ # limitations under the License. # +# these are only needed for unit testing - if there is a repeated setup and tear down, etc. +pico: + permits-dynamic: true -# Override configuration to use a random port for the unit tests -config_ordinal=1000 -# Microprofile server properties -server.port=-1 -server.host=0.0.0.0 +drill: + hand: + name: "Hand" + impact: + name: "Impact" diff --git a/examples/pico/configdriven/src/main/resources/logging.properties b/examples/pico/configdriven/src/main/resources/logging.properties new file mode 100644 index 00000000000..bd06e0ed087 --- /dev/null +++ b/examples/pico/configdriven/src/main/resources/logging.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2023 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +handlers = java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = FINEST +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = [%1$tc] %4$s: %2$s - %5$s %6$s%n + +.level = INFO +io.helidon.config.level = WARNING +io.helidon.config.examples.level = FINEST diff --git a/examples/pico/interceptors/README.md b/examples/pico/interceptors/README.md new file mode 100644 index 00000000000..57e31529381 --- /dev/null +++ b/examples/pico/interceptors/README.md @@ -0,0 +1,21 @@ +# Helidon Pico Providers Example + +This example shows how interceptors can be leveraged to develop using Helidon Pico. The +[Main.java](./src/main/java/io/helidon/examples/pico/providers/Main.java) class shows: + +* Interception basics of Pico. + +## Build and run + +```bash +mvn package +java -jar target/helidon-examples-pico-interceptors.jar +``` + +Expected Output: +``` +Screw Driver (1st turn): +Screw Driver turning right +Screw Driver (2nd turn): +Screw Driver turning right +``` diff --git a/examples/webserver/jersey/pom.xml b/examples/pico/interceptors/pom.xml similarity index 54% rename from examples/webserver/jersey/pom.xml rename to examples/pico/interceptors/pom.xml index be992bb64bb..990b5bce979 100644 --- a/examples/webserver/jersey/pom.xml +++ b/examples/pico/interceptors/pom.xml @@ -1,7 +1,7 @@ - 4.0.0 io.helidon.applications - helidon-se + helidon-nima 4.0.0-SNAPSHOT - ../../../applications/se/pom.xml + ../../../applications/nima/pom.xml - io.helidon.examples.webserver - helidon-examples-webserver-jersey - Helidon WebServer Examples Jersey + io.helidon.examples.pico + helidon-examples-pico-interceptors + Helidon Pico Examples Interceptors - WebServer Jersey example application + Example usages of Pico Interceptors. - io.helidon.reactive.webserver.examples.jersey.Main + io.helidon.examples.pico.interceptors.Main - io.helidon.reactive.webserver - helidon-reactive-webserver + io.helidon.examples.pico + helidon-examples-pico-basics + ${helidon.version} - io.helidon.reactive.webserver - helidon-reactive-webserver-jersey - - - org.junit.jupiter - junit-jupiter-api - test + jakarta.annotation + jakarta.annotation-api + provided org.hamcrest @@ -59,14 +55,28 @@ test - io.helidon.reactive.webserver - helidon-reactive-webserver-test-support + org.junit.jupiter + junit-jupiter-api test + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + io.helidon.pico + helidon-pico-processor + ${helidon.version} + + + + org.apache.maven.plugins maven-dependency-plugin diff --git a/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/Main.java b/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/Main.java new file mode 100644 index 00000000000..ab9f6dd2176 --- /dev/null +++ b/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/Main.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.interceptors; + +import io.helidon.pico.api.PicoServices; +import io.helidon.pico.api.ServiceProvider; +import io.helidon.pico.api.Services; + +/** + * Interceptors example. + */ +public class Main { + + /** + * Executes the example. + * + * @param args arguments + */ + public static void main(String... args) { + Services services = PicoServices.realizedServices(); + + // use the intercepted screwdriver - note that hashCode(), equals(), and toString() are not intercepted + ServiceProvider screwDriver = services.lookupFirst(ScrewDriver.class); + System.out.println(screwDriver.get() + " (1st turn): "); + screwDriver.get().turn("left"); + + // use the intercepted screwdriver turning tool - note that hashCode(), equals(), and toString() are not intercepted + ServiceProvider turningTool = services.lookupFirst(TurningTool.class); + System.out.println(turningTool.get() + " (2nd turn): "); + turningTool.get().turn("left"); + } + +} diff --git a/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/ScrewDriver.java b/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/ScrewDriver.java new file mode 100644 index 00000000000..f4f0eb0e26c --- /dev/null +++ b/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/ScrewDriver.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.interceptors; + +import jakarta.inject.Singleton; + +@Singleton +class ScrewDriver implements TurningTool { + + @Override + public String name() { + return "Screw Driver"; + } + + @Turn + @Override + public void turn(String direction) { + System.out.println(name() + " turning " + direction); + } + + @Override + public String toString() { + return name(); + } + +} diff --git a/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/Turn.java b/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/Turn.java new file mode 100644 index 00000000000..54241cce136 --- /dev/null +++ b/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/Turn.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.interceptors; + +import io.helidon.pico.api.InterceptedTrigger; + +@InterceptedTrigger +public @interface Turn { +} diff --git a/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/TurnInterceptor.java b/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/TurnInterceptor.java new file mode 100644 index 00000000000..8b53e52ed1b --- /dev/null +++ b/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/TurnInterceptor.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.interceptors; + +import io.helidon.pico.api.ClassNamed; +import io.helidon.pico.api.InvocationContext; + +import jakarta.inject.Singleton; + +@ClassNamed(Turn.class) +@Singleton +@SuppressWarnings("unused") +class TurnInterceptor implements io.helidon.pico.api.Interceptor { + + @Override + @SuppressWarnings("unchecked") + public V proceed(InvocationContext ctx, + Chain chain, + Object... args) { + // in "real life" you'd use the ctx to determine the best decision - this is just for simple demonstration only! + if (args.length == 1) { + // this is the call to turn() + args[0] = "right"; + } else if (args.length == 0 && ctx.elementInfo().elementName().equals("name")) { + return (V) ("intercepted: " + chain.proceed(args)); + } + + return chain.proceed(args); + } + +} diff --git a/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/TurningTool.java b/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/TurningTool.java new file mode 100644 index 00000000000..3d3a296d469 --- /dev/null +++ b/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/TurningTool.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.interceptors; + +import io.helidon.examples.pico.basics.Tool; +import io.helidon.pico.api.Contract; + +@Contract +public interface TurningTool extends Tool { + + void turn(String direction); + +} diff --git a/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/package-info.java b/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/package-info.java new file mode 100644 index 00000000000..a337af36d05 --- /dev/null +++ b/examples/pico/interceptors/src/main/java/io/helidon/examples/pico/interceptors/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Examples of Intercepted services in Pico. + */ +package io.helidon.examples.pico.interceptors; diff --git a/examples/pico/interceptors/src/main/resources/logging.properties b/examples/pico/interceptors/src/main/resources/logging.properties new file mode 100644 index 00000000000..bd06e0ed087 --- /dev/null +++ b/examples/pico/interceptors/src/main/resources/logging.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2023 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +handlers = java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = FINEST +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = [%1$tc] %4$s: %2$s - %5$s %6$s%n + +.level = INFO +io.helidon.config.level = WARNING +io.helidon.config.examples.level = FINEST diff --git a/examples/pico/pom.xml b/examples/pico/pom.xml new file mode 100644 index 00000000000..aaf8c818dd4 --- /dev/null +++ b/examples/pico/pom.xml @@ -0,0 +1,41 @@ + + + + + 4.0.0 + + io.helidon.examples + helidon-examples-project + 4.0.0-SNAPSHOT + + io.helidon.examples.pico + helidon-examples-pico-project + pom + Helidon Pico Examples + + + basics + providers + configdriven + interceptors + application + + + diff --git a/examples/pico/providers/README.md b/examples/pico/providers/README.md new file mode 100644 index 00000000000..3014fb949e4 --- /dev/null +++ b/examples/pico/providers/README.md @@ -0,0 +1,38 @@ +# Helidon Pico Providers Example + +This example shows how providers can be leveraged to develop using Helidon Pico. The +[Main.java](./src/main/java/io/helidon/examples/pico/providers/Main.java) class shows: + +* multi-module usage (i.e., this example extends [basics](../basics)). +* [standard Providers](src/main/java/io/helidon/examples/pico/providers/NailProvider.java). +* [InjectionPoint Providers](src/main/java/io/helidon/examples/pico/providers/BladeProvider.java). +* additional lifecycle examples via PostConstruct and RunLevel. + +## Build and run + +```bash +mvn package +java -jar target/helidon-examples-pico-providers.jar +``` + +Expected Output: +``` +Startup service providers (ranked according to weight, pre-activated): [ToolBox:INIT, CircularSaw:INIT, NailGun:INIT, TableSaw:INIT] +Preferred (highest weighted) 'Big' Tool: Big Hammer +Optional 'Little' Hammer: Optional[Little Hammer] +Tools in the virtual ToolBox: + tool: Hammer:INIT + tool: BigHammer:ACTIVE + tool: LittleHammer:ACTIVE + tool: AngleGrinderSaw:INIT + tool: CircularSaw:INIT + tool: HandSaw:INIT + tool: NailGun:INIT + tool: TableSaw:INIT +io.helidon.examples.pico.providers.CircularSaw:: will be injected with Optional.empty +Circular Saw: (blade=null); initialized +Nail Gun: (nail provider=NailProvider:INIT); initialized +io.helidon.examples.pico.providers.TableSaw:: will be injected with Optional[LARGE Blade] +Table Saw: (blade=LARGE Blade); initialized +All service providers (after all activations): [ToolBox:ACTIVE, CircularSaw:ACTIVE, NailGun:ACTIVE, TableSaw:ACTIVE] +``` diff --git a/examples/pico/providers/pom.xml b/examples/pico/providers/pom.xml new file mode 100644 index 00000000000..3faf2de02d8 --- /dev/null +++ b/examples/pico/providers/pom.xml @@ -0,0 +1,95 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-nima + 4.0.0-SNAPSHOT + ../../../applications/nima/pom.xml + + io.helidon.examples.pico + helidon-examples-pico-providers + Helidon Pico Examples Providers + + + Example usages of Pico Providers. + + + + io.helidon.examples.pico.providers.Main + + + + + io.helidon.examples.pico + helidon-examples-pico-basics + ${helidon.version} + + + jakarta.annotation + jakarta.annotation-api + provided + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + -Apico.autoAddNonContractInterfaces=true + -Apico.debug=false + + + + io.helidon.pico + helidon-pico-processor + ${helidon.version} + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + + diff --git a/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/AngleGrinderSaw.java b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/AngleGrinderSaw.java new file mode 100644 index 00000000000..a133dae5a49 --- /dev/null +++ b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/AngleGrinderSaw.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.providers; + +import java.util.Optional; + +import io.helidon.examples.pico.basics.Little; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +@Singleton +class AngleGrinderSaw implements Saw { + + private final Blade blade; + + @Inject + AngleGrinderSaw(@Little Optional blade) { + this.blade = blade.orElse(null); + } + + @Override + public String name() { + return "Angle Grinder Saw: (blade=" + blade + ")"; + } + + @PostConstruct + @SuppressWarnings("unused") + void init() { + System.out.println(name() + "; initialized"); + } + +} diff --git a/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/Blade.java b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/Blade.java new file mode 100644 index 00000000000..ab906b7021f --- /dev/null +++ b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/Blade.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.providers; + +/** + * Normally, one would need to place {@link io.helidon.pico.api.Contract} on interfaces. Here, however, we used + * {@code -Apico.autoAddNonContractInterfaces=true} in the {@code pom.xml} thereby making all interfaces into contracts that + * can be found via {@link io.helidon.pico.api.Services#lookup}. + */ +//@Contract +public interface Blade { + + String name(); + +} diff --git a/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/BladeProvider.java b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/BladeProvider.java new file mode 100644 index 00000000000..c53b5d567c6 --- /dev/null +++ b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/BladeProvider.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.providers; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Optional; + +import io.helidon.common.LazyValue; +import io.helidon.examples.pico.basics.Big; +import io.helidon.examples.pico.basics.Little; +import io.helidon.pico.api.ContextualServiceQuery; +import io.helidon.pico.api.InjectionPointInfo; +import io.helidon.pico.api.InjectionPointProvider; +import io.helidon.pico.api.QualifierAndValue; +import io.helidon.pico.api.ServiceInfoCriteria; + +import jakarta.inject.Singleton; + +import static io.helidon.common.LazyValue.create; + +@Singleton +public class BladeProvider implements InjectionPointProvider { + + static final LazyValue> LARGE_BLADE = create(() -> Optional.of(new SizedBlade(SizedBlade.Size.LARGE))); + static final LazyValue> SMALL_BLADE = create(() -> Optional.of(new SizedBlade(SizedBlade.Size.SMALL))); + + /** + * Here we are creating the right sized blade based upon the injection point's criteria. Note that the scope/cardinality + * is still (0..1), meaning there will be at most 1 LARGE and at most 1 SMALL blades provided. + * All {@code Provider}s control the scope of the service instances they provide. + * + * @param query the service query + * @return the blade appropriate for the injection point, or empty if nothing matches + * + * @see NailProvider + */ + @Override + public Optional first(ContextualServiceQuery query) { + ServiceInfoCriteria criteria = query.serviceInfoCriteria(); + if (contains(criteria.qualifiers(), Big.class)) { + return logAndReturn(LARGE_BLADE.get(), query); + } else if (contains(criteria.qualifiers(), Little.class)) { + return logAndReturn(SMALL_BLADE.get(), query); + } + return logAndReturn(Optional.empty(), query); + } + + static Optional logAndReturn(Optional result, + ContextualServiceQuery query) { + InjectionPointInfo ip = query.injectionPointInfo().orElse(null); + // note: a "regular" service lookup via Pico will not have an injection point associated with it + if (ip != null) { + System.out.println(ip.serviceTypeName() + "::" + ip.elementName() + " will be injected with " + result); + } + return result; + } + + static boolean contains(Collection qualifiers, + Class anno) { + return qualifiers.stream().anyMatch(it -> it.typeName().name().equals(anno.getName())); + } + +} diff --git a/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/CircularSaw.java b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/CircularSaw.java new file mode 100644 index 00000000000..e529ca3eb40 --- /dev/null +++ b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/CircularSaw.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.providers; + +import java.util.Optional; + +import io.helidon.pico.api.RunLevel; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +@Singleton +@RunLevel(RunLevel.STARTUP) +class CircularSaw implements Saw { + + private final Blade blade; + + @Inject + CircularSaw(Optional blade) { + this.blade = blade.orElse(null); + } + + @Override + public String name() { + return "Circular Saw: (blade=" + blade + ")"; + } + + @PostConstruct + @SuppressWarnings("unused") + void init() { + System.out.println(name() + "; initialized"); + } + +} diff --git a/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/HandSaw.java b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/HandSaw.java new file mode 100644 index 00000000000..07dff59e9bb --- /dev/null +++ b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/HandSaw.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.providers; + +import java.util.Optional; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Singleton +class HandSaw implements Saw { + + private final Blade blade; + + @Inject + HandSaw(@Named("replacement-blade-that-does-not-exist") Optional blade) { + this.blade = blade.orElse(null); + } + + @Override + public String name() { + return "Hand Saw: (blade=" + blade + ")"; + } + + @PostConstruct + @SuppressWarnings("unused") + void init() { + System.out.println(name() + "; initialized"); + } + +} diff --git a/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/Main.java b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/Main.java new file mode 100644 index 00000000000..0a044ac3ebf --- /dev/null +++ b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/Main.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.providers; + +import java.util.List; + +import io.helidon.pico.api.PicoServices; +import io.helidon.pico.api.RunLevel; +import io.helidon.pico.api.ServiceInfoCriteria; +import io.helidon.pico.api.ServiceInfoCriteriaDefault; +import io.helidon.pico.api.ServiceProvider; +import io.helidon.pico.api.Services; + +/** + * Providers example. + */ +public class Main { + + /** + * Executes the example. + * + * @param args arguments + */ + public static void main(String... args) { + Services services = PicoServices.realizedServices(); + + ServiceInfoCriteria criteria = ServiceInfoCriteriaDefault.builder() + .runLevel(RunLevel.STARTUP) + .build(); + + List> startupServiceProviders = services.lookupAll(criteria); + System.out.println("Startup service providers (ranked according to weight, pre-activated): " + startupServiceProviders); + + // trigger all activations for startup service providers + startupServiceProviders.forEach(ServiceProvider::get); + System.out.println("All service providers (after all activations): " + startupServiceProviders); + } + +} diff --git a/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/Nail.java b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/Nail.java new file mode 100644 index 00000000000..c1ddcf9143e --- /dev/null +++ b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/Nail.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.providers; + +/** + * Normally, one would need to place {@link io.helidon.pico.api.Contract} on interfaces. Here, however, we used + * {@code -Apico.autoAddNonContractInterfaces=true} in the {@code pom.xml} thereby making all interfaces into contracts that + * can be found via {@link io.helidon.pico.api.Services#lookup}. + */ +//@Contract +public interface Nail { + + int id(); + +} diff --git a/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/NailGun.java b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/NailGun.java new file mode 100644 index 00000000000..21921f20cd2 --- /dev/null +++ b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/NailGun.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.providers; + +import java.util.Objects; + +import io.helidon.examples.pico.basics.Tool; +import io.helidon.pico.api.RunLevel; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +@Singleton +@RunLevel(RunLevel.STARTUP) +class NailGun implements Tool { + + private final Provider nailProvider; + + @Inject + NailGun(Provider nailProvider) { + this.nailProvider = Objects.requireNonNull(nailProvider); + } + + @Override + public String name() { + return "Nail Gun: (nail provider=" + nailProvider + ")"; + } + + /** + * This method will be called by Pico after this instance is lazily initialized (because this is the {@link PostConstruct} + * method). + */ + @PostConstruct + @SuppressWarnings("unused") + void init() { + System.out.println(name() + "; initialized"); + } + +} diff --git a/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/NailProvider.java b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/NailProvider.java new file mode 100644 index 00000000000..77f54b7eac3 --- /dev/null +++ b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/NailProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.providers; + +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +/** + * Showcases dependent scope-creating a nail for caller's demand for a {@link Nail} to be provided. + * All {@code Provider}s control the scope of the service instances they provide. + * + * @see BladeProvider + */ +@Singleton +class NailProvider implements Provider { + + /** + * Creates a new nail every its called. + * + * @return a new nail instance + */ + @Override + public Nail get() { + return new StandardNail(); + } + +} diff --git a/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/Saw.java b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/Saw.java new file mode 100644 index 00000000000..317a85b5759 --- /dev/null +++ b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/Saw.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.providers; + +import io.helidon.examples.pico.basics.Tool; + +/** + * Normally, one would need to place {@link io.helidon.pico.api.Contract} on interfaces. Here, however, we used + * {@code -Apico.autoAddNonContractInterfaces=true} in the {@code pom.xml} thereby making all interfaces into contracts that + * can be found via {@link io.helidon.pico.api.Services#lookup}. + */ +//@Contract +public interface Saw extends Tool { + +} diff --git a/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/SizedBlade.java b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/SizedBlade.java new file mode 100644 index 00000000000..2593c5b6524 --- /dev/null +++ b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/SizedBlade.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.providers; + +import java.util.Objects; + +/** + * See {@link Blade} + */ +class SizedBlade implements Blade { + + private final Size size; + + public enum Size { + SMALL, + LARGE + } + + public SizedBlade(Size size) { + this.size = Objects.requireNonNull(size); + } + + @Override + public String name() { + return size + " Blade"; + } + + @Override + public String toString() { + return name(); + } + +} diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MPOpenAPISupport.java b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/StandardNail.java similarity index 58% rename from microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MPOpenAPISupport.java rename to examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/StandardNail.java index 811e3c5b3e5..75103a3c842 100644 --- a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MPOpenAPISupport.java +++ b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/StandardNail.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,22 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.microprofile.openapi; -import io.helidon.nima.openapi.OpenApiService; +package io.helidon.examples.pico.providers; -/** - * MP variant of OpenAPISupport. - */ -class MPOpenAPISupport extends OpenApiService { +import java.util.concurrent.atomic.AtomicInteger; + +class StandardNail implements Nail { - protected MPOpenAPISupport(MPOpenAPIBuilder builder) { - super(builder); + private static final AtomicInteger counter = new AtomicInteger(); + private final int id = counter.incrementAndGet(); + + StandardNail() { } - // For visibility to the CDI extension @Override - protected void prepareModel() { - super.prepareModel(); + public int id() { + return id; } + } diff --git a/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/TableSaw.java b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/TableSaw.java new file mode 100644 index 00000000000..006e59694eb --- /dev/null +++ b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/TableSaw.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.providers; + +import java.util.Optional; + +import io.helidon.examples.pico.basics.Big; +import io.helidon.pico.api.RunLevel; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +@Singleton +@RunLevel(RunLevel.STARTUP) +class TableSaw implements Saw { + + private final Blade blade; + + @Inject + TableSaw(@Big Optional blade) { + this.blade = blade.orElse(null); + } + + @Override + public String name() { + return "Table Saw: (blade=" + blade + ")"; + } + + @PostConstruct + @SuppressWarnings("unused") + void init() { + System.out.println(name() + "; initialized"); + } + +} diff --git a/examples/integrations/neo4j/neo4j-se/src/test/java/io/helidon/examples/quickstart/se/package-info.java b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/package-info.java similarity index 81% rename from examples/integrations/neo4j/neo4j-se/src/test/java/io/helidon/examples/quickstart/se/package-info.java rename to examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/package-info.java index 55429240b5e..117b5eef258 100644 --- a/examples/integrations/neo4j/neo4j-se/src/test/java/io/helidon/examples/quickstart/se/package-info.java +++ b/examples/pico/providers/src/main/java/io/helidon/examples/pico/providers/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,6 @@ */ /** - * Tests for Neo4j Helidon SE app. + * Examples of providers in Pico. */ -package io.helidon.examples.quickstart.se; +package io.helidon.examples.pico.providers; diff --git a/examples/pico/providers/src/main/resources/logging.properties b/examples/pico/providers/src/main/resources/logging.properties new file mode 100644 index 00000000000..bd06e0ed087 --- /dev/null +++ b/examples/pico/providers/src/main/resources/logging.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2023 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +handlers = java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = FINEST +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format = [%1$tc] %4$s: %2$s - %5$s %6$s%n + +.level = INFO +io.helidon.config.level = WARNING +io.helidon.config.examples.level = FINEST diff --git a/examples/pico/providers/src/test/java/io/helidon/examples/pico/providers/AllenWrench.java b/examples/pico/providers/src/test/java/io/helidon/examples/pico/providers/AllenWrench.java new file mode 100644 index 00000000000..d984d1c61f0 --- /dev/null +++ b/examples/pico/providers/src/test/java/io/helidon/examples/pico/providers/AllenWrench.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.providers; + +import jakarta.inject.Singleton; + +@Singleton +@SuppressWarnings("unused") +public class AllenWrench implements Wrench { + + @Override + public String name() { + return "Allen Wrench"; + } + +} diff --git a/examples/pico/providers/src/test/java/io/helidon/examples/pico/providers/ProvidersTest.java b/examples/pico/providers/src/test/java/io/helidon/examples/pico/providers/ProvidersTest.java new file mode 100644 index 00000000000..880762ee70c --- /dev/null +++ b/examples/pico/providers/src/test/java/io/helidon/examples/pico/providers/ProvidersTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.examples.pico.providers; + +import org.junit.jupiter.api.Test; + +class ProvidersTest { + + /** + * Through testing, this will additionally have an {@link AllenWrench} in the {@link io.helidon.examples.pico.basics.ToolBox}. + */ + @Test + void main() { + Main.main(); + } + +} diff --git a/examples/integrations/oci/metrics-reactive/src/main/java/io/helidon/examples/integrations/oci/telemetry/reactive/package-info.java b/examples/pico/providers/src/test/java/io/helidon/examples/pico/providers/Wrench.java similarity index 75% rename from examples/integrations/oci/metrics-reactive/src/main/java/io/helidon/examples/integrations/oci/telemetry/reactive/package-info.java rename to examples/pico/providers/src/test/java/io/helidon/examples/pico/providers/Wrench.java index 5e3c842230a..f8cd0580ba2 100644 --- a/examples/integrations/oci/metrics-reactive/src/main/java/io/helidon/examples/integrations/oci/telemetry/reactive/package-info.java +++ b/examples/pico/providers/src/test/java/io/helidon/examples/pico/providers/Wrench.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,10 @@ * limitations under the License. */ -/** - * Example using OCI metrics reactive API. - */ -package io.helidon.examples.integrations.oci.telemetry.reactive; +package io.helidon.examples.pico.providers; + +import io.helidon.examples.pico.basics.Tool; + +public interface Wrench extends Tool { + +} diff --git a/examples/pom.xml b/examples/pom.xml index 532c70d3e3d..d84ae6bfb14 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -47,7 +47,9 @@ quickstarts microprofile media - openapi + + openapi-tools security todo-app @@ -63,6 +65,7 @@ metrics jbatch nima + pico diff --git a/examples/quickstarts/helidon-quickstart-mp/pom.xml b/examples/quickstarts/helidon-quickstart-mp/pom.xml index a28bbe02ce1..e214a8b1100 100644 --- a/examples/quickstarts/helidon-quickstart-mp/pom.xml +++ b/examples/quickstarts/helidon-quickstart-mp/pom.xml @@ -37,7 +37,7 @@ helidon-microprofile - org.jboss + io.smallrye jandex runtime true @@ -71,7 +71,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/quickstarts/helidon-quickstart-se/pom.xml b/examples/quickstarts/helidon-quickstart-se/pom.xml index 1884e4eb1d8..dc9c1152493 100644 --- a/examples/quickstarts/helidon-quickstart-se/pom.xml +++ b/examples/quickstarts/helidon-quickstart-se/pom.xml @@ -49,6 +49,10 @@ io.helidon.nima.observe helidon-nima-observe-health + + io.helidon.nima.openapi + helidon-nima-openapi + io.helidon.config helidon-config-yaml diff --git a/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java b/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java index 52953c0fab8..dc2d8be077c 100644 --- a/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java +++ b/examples/quickstarts/helidon-quickstart-se/src/main/java/io/helidon/examples/quickstart/se/Main.java @@ -18,6 +18,7 @@ import io.helidon.logging.common.LogConfig; import io.helidon.nima.observe.ObserveFeature; +import io.helidon.nima.openapi.SeOpenApiFeature; import io.helidon.nima.webserver.WebServer; import io.helidon.nima.webserver.http.HttpRouting; @@ -52,8 +53,10 @@ public static void main(String[] args) { * Updates HTTP Routing and registers observe providers. */ static void routing(HttpRouting.Builder routing) { + SeOpenApiFeature openApi = SeOpenApiFeature.builder().build(); GreetService greetService = new GreetService(); - routing.register("/greet", greetService) + routing.addFeature(openApi) + .register("/greet", greetService) .addFeature(ObserveFeature.create()); } } diff --git a/examples/quickstarts/helidon-standalone-quickstart-mp/pom.xml b/examples/quickstarts/helidon-standalone-quickstart-mp/pom.xml index a4b094e3f7a..041b3b2d3b9 100644 --- a/examples/quickstarts/helidon-standalone-quickstart-mp/pom.xml +++ b/examples/quickstarts/helidon-standalone-quickstart-mp/pom.xml @@ -37,13 +37,13 @@ 3.8.1 - 3.0.0 + 3.6.0 2.7.5.1 1.6.0 3.0.0-M5 3.0.3 3.0.3 - 1.0.6 + 3.1.2 3.0.2 1.5.0.Final 0.5.1 @@ -69,7 +69,7 @@ helidon-microprofile - org.jboss + io.smallrye jandex runtime true @@ -147,7 +147,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin ${version.plugin.jandex} @@ -189,13 +189,12 @@ true true runtime - test - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/quickstarts/helidon-standalone-quickstart-se/pom.xml b/examples/quickstarts/helidon-standalone-quickstart-se/pom.xml index a2b6119c212..5f697dc5e6e 100644 --- a/examples/quickstarts/helidon-standalone-quickstart-se/pom.xml +++ b/examples/quickstarts/helidon-standalone-quickstart-se/pom.xml @@ -38,7 +38,7 @@ 3.8.1 - 3.0.0 + 3.6.0 1.6.0 3.0.0-M5 3.0.3 @@ -202,7 +202,6 @@ true true runtime - test diff --git a/examples/security/attribute-based-access-control/pom.xml b/examples/security/attribute-based-access-control/pom.xml index 17c6a000e8d..b569ba3be76 100644 --- a/examples/security/attribute-based-access-control/pom.xml +++ b/examples/security/attribute-based-access-control/pom.xml @@ -53,7 +53,7 @@ jakarta.el - org.jboss + io.smallrye jandex runtime true @@ -77,7 +77,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/security/attribute-based-access-control/src/main/java/io/helidon/security/examples/abac/AtnProvider.java b/examples/security/attribute-based-access-control/src/main/java/io/helidon/security/examples/abac/AtnProvider.java index 5e016e11612..ca2ab9e8747 100644 --- a/examples/security/attribute-based-access-control/src/main/java/io/helidon/security/examples/abac/AtnProvider.java +++ b/examples/security/attribute-based-access-control/src/main/java/io/helidon/security/examples/abac/AtnProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,14 +41,13 @@ import io.helidon.security.Subject; import io.helidon.security.SubjectType; import io.helidon.security.spi.AuthenticationProvider; -import io.helidon.security.spi.SynchronousProvider; /** * Example authentication provider that reads annotation to create a subject. */ -public class AtnProvider extends SynchronousProvider implements AuthenticationProvider { +public class AtnProvider implements AuthenticationProvider { @Override - protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { List securityLevels = providerRequest.endpointConfig().securityLevels(); ListIterator listIterator = securityLevels.listIterator(securityLevels.size()); Subject user = null; diff --git a/examples/security/basic-auth-with-static-content/src/main/java/io/helidon/security/examples/webserver/basic/BasicExampleBuilderMain.java b/examples/security/basic-auth-with-static-content/src/main/java/io/helidon/security/examples/webserver/basic/BasicExampleBuilderMain.java index efb26162a6a..b53a09bf901 100644 --- a/examples/security/basic-auth-with-static-content/src/main/java/io/helidon/security/examples/webserver/basic/BasicExampleBuilderMain.java +++ b/examples/security/basic-auth-with-static-content/src/main/java/io/helidon/security/examples/webserver/basic/BasicExampleBuilderMain.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/examples/security/idcs-login/pom.xml b/examples/security/idcs-login/pom.xml index 22db6da9c92..71f57d6794d 100644 --- a/examples/security/idcs-login/pom.xml +++ b/examples/security/idcs-login/pom.xml @@ -91,7 +91,7 @@ helidon-config-yaml - org.jboss + io.smallrye jandex runtime true @@ -120,7 +120,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/security/nohttp-programmatic/src/main/java/io/helidon/security/examples/security/MyProvider.java b/examples/security/nohttp-programmatic/src/main/java/io/helidon/security/examples/security/MyProvider.java index 5cee9d079e2..7eb3ed82dec 100644 --- a/examples/security/nohttp-programmatic/src/main/java/io/helidon/security/examples/security/MyProvider.java +++ b/examples/security/nohttp-programmatic/src/main/java/io/helidon/security/examples/security/MyProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,15 +32,14 @@ import io.helidon.security.spi.AuthenticationProvider; import io.helidon.security.spi.AuthorizationProvider; import io.helidon.security.spi.OutboundSecurityProvider; -import io.helidon.security.spi.SynchronousProvider; /** * Sample provider. */ -class MyProvider extends SynchronousProvider implements AuthenticationProvider, AuthorizationProvider, OutboundSecurityProvider { +class MyProvider implements AuthenticationProvider, AuthorizationProvider, OutboundSecurityProvider { @Override - protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { //get username and password List headers = providerRequest.env().headers().getOrDefault("authorization", List.of()); if (headers.isEmpty()) { @@ -75,7 +74,7 @@ protected AuthenticationResponse syncAuthenticate(ProviderRequest providerReques } @Override - protected AuthorizationResponse syncAuthorize(ProviderRequest providerRequest) { + public AuthorizationResponse authorize(ProviderRequest providerRequest) { if ("CustomResourceType" .equals(providerRequest.env().abacAttribute("resourceType").orElseThrow(() -> new IllegalArgumentException( "Resource type is a required parameter")))) { @@ -98,9 +97,9 @@ protected AuthorizationResponse syncAuthorize(ProviderRequest providerRequest) { } @Override - protected OutboundSecurityResponse syncOutbound(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundEndpointConfig) { + public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv, + EndpointConfig outboundEndpointConfig) { return providerRequest.securityContext() .user() diff --git a/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/AtnProviderSync.java b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/AtnProviderSync.java index 2e62051d7ca..8c6dad4bed0 100644 --- a/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/AtnProviderSync.java +++ b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/AtnProviderSync.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,15 +37,14 @@ import io.helidon.security.SecurityLevel; import io.helidon.security.Subject; import io.helidon.security.spi.AuthenticationProvider; -import io.helidon.security.spi.SynchronousProvider; /** * Example of an authentication provider implementation - synchronous. * This is a full-blows example of a provider that requires additional configuration on a resource. */ -public class AtnProviderSync extends SynchronousProvider implements AuthenticationProvider { +public class AtnProviderSync implements AuthenticationProvider { @Override - protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { // first obtain the configuration of this request // either from annotation, custom object or config diff --git a/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/AtzProviderSync.java b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/AtzProviderSync.java index 0423efea736..a5508f3b7b6 100644 --- a/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/AtzProviderSync.java +++ b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/AtzProviderSync.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,16 +19,15 @@ import io.helidon.security.AuthorizationResponse; import io.helidon.security.ProviderRequest; import io.helidon.security.spi.AuthorizationProvider; -import io.helidon.security.spi.SynchronousProvider; /** * Authorization provider example. The most simplistic approach. * * @see AtnProviderSync on how to use custom objects, config and annotations in a provider */ -public class AtzProviderSync extends SynchronousProvider implements AuthorizationProvider { +public class AtzProviderSync implements AuthorizationProvider { @Override - protected AuthorizationResponse syncAuthorize(ProviderRequest providerRequest) { + public AuthorizationResponse authorize(ProviderRequest providerRequest) { // just check the path contains the string "public", otherwise allow only if user is logged in // if no path is defined, abstain (e.g. I do not care about such requests - I can neither allow or deny them) return providerRequest.env().path() diff --git a/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/OutboundProviderSync.java b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/OutboundProviderSync.java index f0590667fb4..f7217d4d69b 100644 --- a/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/OutboundProviderSync.java +++ b/examples/security/spi-examples/src/main/java/io/helidon/security/examples/spi/OutboundProviderSync.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,14 +26,13 @@ import io.helidon.security.SecurityEnvironment; import io.helidon.security.Subject; import io.helidon.security.spi.OutboundSecurityProvider; -import io.helidon.security.spi.SynchronousProvider; /** * Example of a simplistic outbound security provider. */ -public class OutboundProviderSync extends SynchronousProvider implements OutboundSecurityProvider { +public class OutboundProviderSync implements OutboundSecurityProvider { @Override - protected OutboundSecurityResponse syncOutbound(ProviderRequest providerRequest, + public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, SecurityEnvironment outboundEnv, EndpointConfig outboundEndpointConfig) { diff --git a/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AtnProviderSyncTest.java b/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AtnProviderSyncTest.java index 7bc45249f14..2b361c95a71 100644 --- a/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AtnProviderSyncTest.java +++ b/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AtnProviderSyncTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2020 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,7 +65,7 @@ public void testAbstain() { AtnProviderSync provider = new AtnProviderSync(); - AuthenticationResponse response = provider.syncAuthenticate(request); + AuthenticationResponse response = provider.authenticate(request); assertThat(response.status(), is(SecurityResponse.SecurityStatus.ABSTAIN)); } @@ -181,7 +181,7 @@ public void testFailure() { AtnProviderSync provider = new AtnProviderSync(); - AuthenticationResponse response = provider.syncAuthenticate(request); + AuthenticationResponse response = provider.authenticate(request); assertThat(response.status(), is(SecurityResponse.SecurityStatus.FAILURE)); } @@ -219,7 +219,7 @@ private void validateResponse(AuthenticationResponse response) { private void testSuccess(ProviderRequest request) { AtnProviderSync provider = new AtnProviderSync(); - AuthenticationResponse response = provider.syncAuthenticate(request); + AuthenticationResponse response = provider.authenticate(request); validateResponse(response); } } diff --git a/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AtzProviderSyncTest.java b/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AtzProviderSyncTest.java index 194f6ab1e97..340d68caeca 100644 --- a/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AtzProviderSyncTest.java +++ b/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/AtzProviderSyncTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ public void testPublic() { AtzProviderSync provider = new AtzProviderSync(); - AuthorizationResponse response = provider.syncAuthorize(request); + AuthorizationResponse response = provider.authorize(request); assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); } @@ -63,7 +63,7 @@ public void testAbstain() { AtzProviderSync provider = new AtzProviderSync(); - AuthorizationResponse response = provider.syncAuthorize(request); + AuthorizationResponse response = provider.authorize(request); assertThat(response.status(), is(SecurityResponse.SecurityStatus.ABSTAIN)); } @@ -85,7 +85,7 @@ public void testDenied() { AtzProviderSync provider = new AtzProviderSync(); - AuthorizationResponse response = provider.syncAuthorize(request); + AuthorizationResponse response = provider.authorize(request); assertThat(response.status(), is(SecurityResponse.SecurityStatus.FAILURE)); } @@ -107,7 +107,7 @@ public void testPermitted() { AtzProviderSync provider = new AtzProviderSync(); - AuthorizationResponse response = provider.syncAuthorize(request); + AuthorizationResponse response = provider.authorize(request); assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); } diff --git a/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/OutboundProviderSyncTest.java b/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/OutboundProviderSyncTest.java index 24acd24c221..e2dfa640fed 100644 --- a/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/OutboundProviderSyncTest.java +++ b/examples/security/spi-examples/src/test/java/io/helidon/security/examples/spi/OutboundProviderSyncTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,7 +52,7 @@ public void testAbstain() { when(request.env()).thenReturn(se); OutboundProviderSync ops = new OutboundProviderSync(); - OutboundSecurityResponse response = ops.syncOutbound(request, SecurityEnvironment.create(), EndpointConfig.create()); + OutboundSecurityResponse response = ops.outboundSecurity(request, SecurityEnvironment.create(), EndpointConfig.create()); assertThat(response.status(), is(SecurityResponse.SecurityStatus.ABSTAIN)); } @@ -73,7 +73,7 @@ public void testSuccess() { when(request.env()).thenReturn(se); OutboundProviderSync ops = new OutboundProviderSync(); - OutboundSecurityResponse response = ops.syncOutbound(request, SecurityEnvironment.create(), EndpointConfig.create()); + OutboundSecurityResponse response = ops.outboundSecurity(request, SecurityEnvironment.create(), EndpointConfig.create()); assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); assertThat(response.requestHeaders().get("X-AUTH-USER"), is(List.of(username))); diff --git a/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/DigestService.java b/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/DigestService.java index 0fae1611e1f..675ededded4 100644 --- a/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/DigestService.java +++ b/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/DigestService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,9 +41,8 @@ private void digest(ServerRequest req, ServerResponse res) { String configName = req.path().param("config"); String text = req.path().param("text"); - security.digest(configName, text.getBytes(StandardCharsets.UTF_8)) - .forSingle(res::send) - .exceptionally(res::send); + String toSend = security.digest(configName, text.getBytes(StandardCharsets.UTF_8)); + res.send(toSend); } private void verify(ServerRequest req, ServerResponse res) { @@ -51,9 +50,10 @@ private void verify(ServerRequest req, ServerResponse res) { String text = req.path().param("text"); String digest = req.path().param("digest"); - security.verifyDigest(configName, text.getBytes(StandardCharsets.UTF_8), digest) - .map(it -> it ? "Valid" : "Invalid") - .forSingle(res::send) - .exceptionally(res::send); + if (security.verifyDigest(configName, text.getBytes(StandardCharsets.UTF_8), digest)) { + res.send("Valid"); + } else { + res.send("Invalid"); + } } } diff --git a/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/EncryptionService.java b/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/EncryptionService.java index f076e299988..057538b21ec 100644 --- a/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/EncryptionService.java +++ b/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/EncryptionService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,17 +41,15 @@ private void encrypt(ServerRequest req, ServerResponse res) { String configName = req.path().param("config"); String text = req.path().param("text"); - security.encrypt(configName, text.getBytes(StandardCharsets.UTF_8)) - .forSingle(res::send) - .exceptionally(res::send); + String encrypted = security.encrypt(configName, text.getBytes(StandardCharsets.UTF_8)); + res.send(encrypted); } private void decrypt(ServerRequest req, ServerResponse res) { String configName = req.path().param("config"); String cipherText = req.path().param("cipherText"); - security.decrypt(configName, cipherText) - .forSingle(res::send) - .exceptionally(res::send); + byte[] decrypted = security.decrypt(configName, cipherText); + res.send(decrypted); } } diff --git a/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/SecretsService.java b/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/SecretsService.java index f77575160c9..76ee8e67e26 100644 --- a/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/SecretsService.java +++ b/examples/security/vaults/src/main/java/io/helidon/examples/security/vaults/SecretsService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,8 +36,7 @@ public void update(Routing.Rules rules) { private void secret(ServerRequest req, ServerResponse res) { String secretName = req.path().param("name"); - security.secret(secretName, "default-" + secretName) - .forSingle(res::send) - .exceptionally(res::send); + String secret = security.secret(secretName, "default-" + secretName); + res.send(secret); } } diff --git a/examples/security/webserver-digest-auth/src/main/java/io/helidon/security/examples/webserver/digest/DigestExampleBuilderMain.java b/examples/security/webserver-digest-auth/src/main/java/io/helidon/security/examples/webserver/digest/DigestExampleBuilderMain.java index 3dd11974d63..f991a2752f2 100644 --- a/examples/security/webserver-digest-auth/src/main/java/io/helidon/security/examples/webserver/digest/DigestExampleBuilderMain.java +++ b/examples/security/webserver-digest-auth/src/main/java/io/helidon/security/examples/webserver/digest/DigestExampleBuilderMain.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/examples/security/webserver-signatures/src/main/java/io/helidon/security/examples/signatures/SignatureExampleBuilderMain.java b/examples/security/webserver-signatures/src/main/java/io/helidon/security/examples/signatures/SignatureExampleBuilderMain.java index d43621efe99..eb2b9becb31 100644 --- a/examples/security/webserver-signatures/src/main/java/io/helidon/security/examples/signatures/SignatureExampleBuilderMain.java +++ b/examples/security/webserver-signatures/src/main/java/io/helidon/security/examples/signatures/SignatureExampleBuilderMain.java @@ -1,6 +1,5 @@ - /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/examples/todo-app/backend/pom.xml b/examples/todo-app/backend/pom.xml index 5d52c162ade..d97a8cded1b 100644 --- a/examples/todo-app/backend/pom.xml +++ b/examples/todo-app/backend/pom.xml @@ -149,7 +149,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/examples/translator-app/frontend/pom.xml b/examples/translator-app/frontend/pom.xml index ba82960c9fa..def27be845d 100644 --- a/examples/translator-app/frontend/pom.xml +++ b/examples/translator-app/frontend/pom.xml @@ -61,6 +61,10 @@ io.helidon.common helidon-common + + io.helidon.jersey + helidon-jersey-client + jakarta.inject jakarta.inject-api diff --git a/examples/webserver/jersey/README.md b/examples/webserver/jersey/README.md deleted file mode 100644 index 620d41954c2..00000000000 --- a/examples/webserver/jersey/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# WebServer Jersey Application Example - -An example of **Jersey** integration into the **Web Server**. - -This is just a simple Hello World example. A user can start the application using the `WebServerJerseyMain` class -and `GET` the `Hello World!` response by accessing `http://localhost:8080/jersey/hello`. - -## Build and run - -```bash -mvn package -java -jar target/helidon-examples-webserver-jersey.jar -``` - -Make an HTTP request to application: -```bash -curl http://localhost:8080/jersey/hello -``` diff --git a/examples/webserver/jersey/src/main/java/io/helidon/reactive/webserver/examples/jersey/Main.java b/examples/webserver/jersey/src/main/java/io/helidon/reactive/webserver/examples/jersey/Main.java deleted file mode 100644 index 97f4f7273f2..00000000000 --- a/examples/webserver/jersey/src/main/java/io/helidon/reactive/webserver/examples/jersey/Main.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2017, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.reactive.webserver.examples.jersey; - -import java.util.concurrent.CompletionStage; - -import io.helidon.logging.common.LogConfig; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.WebServer; -import io.helidon.reactive.webserver.jersey.JerseySupport; - -import org.glassfish.jersey.server.ResourceConfig; - -/** - * The WebServer Jersey Main example class. - * - * @see #main(String[]) - * @see #startServer(int) - */ -public final class Main { - - private Main() { - } - - /** - * Run the Jersey WebServer Example. - * - * @param args arguments are not used - */ - public static void main(String[] args) { - // configure logging in order to not have the standard JVM defaults - LogConfig.configureRuntime(); - - // start the server on port 8080 - startServer(8080); - } - - /** - * Start the WebServer based on the provided configuration. When running from - * a test, pass {@link null} to have a dynamically allocated port - * the server listens on. - * - * @param port port to start server on - * @return a completion stage indicating that the server has started and is ready to - * accept http requests - */ - static CompletionStage startServer(int port) { - WebServer webServer = WebServer.builder( - Routing.builder() - // register a Jersey Application at the '/jersey' context path - .register("/jersey", - JerseySupport.create(new ResourceConfig(HelloWorld.class))) - .build()) - .port(port) - .build(); - - return webServer.start() - .whenComplete((server, t) -> { - System.out.println("Jersey WebServer started."); - System.out.println("To stop the application, hit CTRL+C"); - System.out.println("Try the hello world resource at: http://localhost:" + server - .port() + "/jersey/hello"); - }); - } -} diff --git a/examples/webserver/jersey/src/test/java/io/helidon/reactive/webserver/examples/jersey/HelloWorldTest.java b/examples/webserver/jersey/src/test/java/io/helidon/reactive/webserver/examples/jersey/HelloWorldTest.java deleted file mode 100644 index 9cbdde4245b..00000000000 --- a/examples/webserver/jersey/src/test/java/io/helidon/reactive/webserver/examples/jersey/HelloWorldTest.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2017, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.reactive.webserver.examples.jersey; - -import java.util.concurrent.TimeUnit; - -import io.helidon.reactive.webserver.WebServer; - -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.core.Response; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -/** - * The Jersey Client based example that tests the {@link HelloWorld} resource - * that gets served by running {@link Main#startServer(int)} - * - * @see HelloWorld - * @see Main - */ -public class HelloWorldTest { - - private static WebServer webServer; - - @BeforeAll - public static void startTheServer() throws Exception { - webServer = Main.startServer(0) - .toCompletableFuture() - .get(10, TimeUnit.SECONDS); - } - - @AfterAll - public static void stopServer() throws Exception { - if (webServer != null) { - webServer.shutdown() - .toCompletableFuture() - .get(10, TimeUnit.SECONDS); - } - } - - @Test - public void testHelloWorld() throws Exception { - Client client = ClientBuilder.newClient(); - try (Response response = client.target("http://localhost:" + webServer.port()) - .path("jersey/hello") - .request() - .get()) { - assertThat("Unexpected response; status: " + response.getStatus(), - response.readEntity(String.class), is("Hello World!")); - } finally { - client.close(); - } - } -} diff --git a/examples/webserver/pom.xml b/examples/webserver/pom.xml index b12d98d8ff0..2b2d96d44b7 100644 --- a/examples/webserver/pom.xml +++ b/examples/webserver/pom.xml @@ -36,7 +36,6 @@ tutorial comment-aas static-content - jersey opentracing streaming websocket diff --git a/integrations/cdi/common-cdi/delegates/pom.xml b/integrations/cdi/common-cdi/delegates/pom.xml index c84ea793631..641d6160cda 100644 --- a/integrations/cdi/common-cdi/delegates/pom.xml +++ b/integrations/cdi/common-cdi/delegates/pom.xml @@ -39,7 +39,7 @@ - org.jboss + io.smallrye jandex runtime true diff --git a/integrations/cdi/common-cdi/reference-counted-context/pom.xml b/integrations/cdi/common-cdi/reference-counted-context/pom.xml index 49c6e4901a8..6757d5e0c4b 100644 --- a/integrations/cdi/common-cdi/reference-counted-context/pom.xml +++ b/integrations/cdi/common-cdi/reference-counted-context/pom.xml @@ -54,7 +54,7 @@ - org.jboss + io.smallrye jandex runtime true diff --git a/integrations/cdi/datasource-hikaricp/pom.xml b/integrations/cdi/datasource-hikaricp/pom.xml index 1309f752d8f..42ee6fe6879 100644 --- a/integrations/cdi/datasource-hikaricp/pom.xml +++ b/integrations/cdi/datasource-hikaricp/pom.xml @@ -76,7 +76,7 @@ - org.jboss + io.smallrye jandex runtime true diff --git a/integrations/cdi/datasource-ucp/pom.xml b/integrations/cdi/datasource-ucp/pom.xml index 79f5e7e9926..f4c2597fad3 100644 --- a/integrations/cdi/datasource-ucp/pom.xml +++ b/integrations/cdi/datasource-ucp/pom.xml @@ -71,7 +71,7 @@ - org.jboss + io.smallrye jandex runtime true diff --git a/integrations/cdi/datasource/pom.xml b/integrations/cdi/datasource/pom.xml index 35704417fb6..4b3abb4da36 100644 --- a/integrations/cdi/datasource/pom.xml +++ b/integrations/cdi/datasource/pom.xml @@ -60,7 +60,7 @@ - org.jboss + io.smallrye jandex runtime true diff --git a/integrations/cdi/eclipselink-cdi/pom.xml b/integrations/cdi/eclipselink-cdi/pom.xml index 993fcd284ba..4700f09d89e 100644 --- a/integrations/cdi/eclipselink-cdi/pom.xml +++ b/integrations/cdi/eclipselink-cdi/pom.xml @@ -71,7 +71,7 @@ - org.jboss + io.smallrye jandex runtime @@ -115,7 +115,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/integrations/cdi/hibernate-cdi/pom.xml b/integrations/cdi/hibernate-cdi/pom.xml index ea0b7b56b41..4ccd023f16b 100644 --- a/integrations/cdi/hibernate-cdi/pom.xml +++ b/integrations/cdi/hibernate-cdi/pom.xml @@ -44,7 +44,7 @@ - org.jboss + io.smallrye jandex runtime @@ -71,6 +71,13 @@ org.hibernate.orm hibernate-core compile + + + + org.jboss + jandex + + @@ -89,7 +96,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/integrations/cdi/jedis-cdi/pom.xml b/integrations/cdi/jedis-cdi/pom.xml index 3b4dff41927..9c78be50c57 100644 --- a/integrations/cdi/jedis-cdi/pom.xml +++ b/integrations/cdi/jedis-cdi/pom.xml @@ -60,7 +60,7 @@ - org.jboss + io.smallrye jandex runtime true diff --git a/integrations/cdi/jpa-cdi/pom.xml b/integrations/cdi/jpa-cdi/pom.xml index 97e08fc7669..b3dbc91c458 100644 --- a/integrations/cdi/jpa-cdi/pom.xml +++ b/integrations/cdi/jpa-cdi/pom.xml @@ -77,7 +77,7 @@ - org.jboss + io.smallrye jandex runtime @@ -204,7 +204,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/integrations/cdi/jta-cdi/pom.xml b/integrations/cdi/jta-cdi/pom.xml index a0e954a5f1a..be4a92b58cd 100644 --- a/integrations/cdi/jta-cdi/pom.xml +++ b/integrations/cdi/jta-cdi/pom.xml @@ -54,7 +54,7 @@ - org.jboss + io.smallrye jandex runtime @@ -98,7 +98,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/integrations/neo4j/health/pom.xml b/integrations/neo4j/health/pom.xml index 14a46bf0086..9ee32f65345 100644 --- a/integrations/neo4j/health/pom.xml +++ b/integrations/neo4j/health/pom.xml @@ -30,10 +30,6 @@ helidon-integrations-neo4j-health Helidon Neo4j Health Integrations - - etc/spotbugs/exclude.xml - - org.neo4j.driver @@ -41,25 +37,14 @@ provided - org.eclipse.microprofile.health - microprofile-health-api + io.helidon.health + helidon-health io.helidon.common.features helidon-common-features-api true - - io.helidon.microprofile.health - helidon-microprofile-health - provided - - - - jakarta.enterprise - jakarta.enterprise.cdi-api - true - diff --git a/integrations/neo4j/health/src/main/java/io/helidon/integrations/neo4j/health/Neo4jHealthCheck.java b/integrations/neo4j/health/src/main/java/io/helidon/integrations/neo4j/health/Neo4jHealthCheck.java index 58f49ba89a5..8469bc589f6 100644 --- a/integrations/neo4j/health/src/main/java/io/helidon/integrations/neo4j/health/Neo4jHealthCheck.java +++ b/integrations/neo4j/health/src/main/java/io/helidon/integrations/neo4j/health/Neo4jHealthCheck.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,43 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.integrations.neo4j.health; -import io.helidon.microprofile.health.BuiltInHealthCheck; +import io.helidon.health.HealthCheck; +import io.helidon.health.HealthCheckResponse; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import org.eclipse.microprofile.health.HealthCheck; -import org.eclipse.microprofile.health.HealthCheckResponse; -import org.eclipse.microprofile.health.HealthCheckResponseBuilder; -import org.eclipse.microprofile.health.Readiness; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; - /** - * Health support module for Neo4j. Follows the standard MicroProfile HealthCheck pattern. + * Health support module for Neo4j (implements {@link io.helidon.health.HealthCheck}). */ -@Readiness -@ApplicationScoped -@BuiltInHealthCheck public class Neo4jHealthCheck implements HealthCheck { + private static final String NAME = "Neo4j connection health check"; + private final Driver driver; /** * The Cypher statement used to verify Neo4j is up. */ static final String CYPHER = "CALL dbms.components() YIELD name, edition WHERE name = 'Neo4j Kernel' RETURN edition"; - private final Driver driver; - /** * Constructor for Health checks. * * @param driver Neo4j. */ - @Inject //will be ignored out of CDI - Neo4jHealthCheck(Driver driver) { + public Neo4jHealthCheck(Driver driver) { this.driver = driver; } @@ -63,10 +52,15 @@ public static Neo4jHealthCheck create(Driver driver) { return new Neo4jHealthCheck(driver); } - private HealthCheckResponse runHealthCheckQuery(HealthCheckResponseBuilder builder) { + @Override + public String name() { + return NAME; + } + @Override + public HealthCheckResponse call() { + HealthCheckResponse.Builder builder = HealthCheckResponse.builder(); try (Session session = this.driver.session()) { - return session.writeTransaction(tx -> { var result = tx.run(CYPHER); @@ -75,27 +69,19 @@ private HealthCheckResponse runHealthCheckQuery(HealthCheckResponseBuilder build var serverInfo = resultSummary.server(); var responseBuilder = builder - .withData("server", serverInfo.version() + "@" + serverInfo.address()) - .withData("edition", edition); + .detail("server", serverInfo.version() + "@" + serverInfo.address()) + .detail("edition", edition); var databaseInfo = resultSummary.database(); if (!databaseInfo.name().trim().isBlank()) { - responseBuilder.withData("database", databaseInfo.name().trim()); + responseBuilder.detail("database", databaseInfo.name().trim()); } - return responseBuilder.up().build(); + return responseBuilder.status(HealthCheckResponse.Status.UP).build(); }); + } catch (Exception e) { + return builder.status(HealthCheckResponse.Status.DOWN).build(); } } - @Override - public HealthCheckResponse call() { - - var builder = HealthCheckResponse.named("Neo4j connection health check"); - try { - return runHealthCheckQuery(builder); - } catch (Exception ex) { - return builder.down().withData("reason", ex.getMessage()).build(); - } - } } diff --git a/integrations/neo4j/health/src/main/java/module-info.java b/integrations/neo4j/health/src/main/java/module-info.java index 28272846ec0..b66447da240 100644 --- a/integrations/neo4j/health/src/main/java/module-info.java +++ b/integrations/neo4j/health/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,20 +24,17 @@ @Preview @Feature(value = "Neo4j Health", description = "Health check for Neo4j integration", - in = {HelidonFlavor.SE, HelidonFlavor.MP, HelidonFlavor.NIMA}, + in = {HelidonFlavor.SE, HelidonFlavor.NIMA}, path = {"Neo4j", "Health"} ) module io.helidon.integrations.neo4j.health { requires static io.helidon.common.features.api; + requires io.helidon.health; - requires microprofile.health.api; requires org.neo4j.driver; - requires static jakarta.cdi; - requires static jakarta.inject; - requires static io.helidon.microprofile.health; exports io.helidon.integrations.neo4j.health; - opens io.helidon.integrations.neo4j.health to weld.core.impl, io.helidon.microprofile.cdi; + opens io.helidon.integrations.neo4j.health; } diff --git a/integrations/neo4j/health/src/main/resources/META-INF/beans.xml b/integrations/neo4j/health/src/main/resources/META-INF/beans.xml deleted file mode 100644 index dbf3e648c1e..00000000000 --- a/integrations/neo4j/health/src/main/resources/META-INF/beans.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - diff --git a/integrations/neo4j/metrics/pom.xml b/integrations/neo4j/metrics/pom.xml index 66a62ee5684..7e110adba8a 100644 --- a/integrations/neo4j/metrics/pom.xml +++ b/integrations/neo4j/metrics/pom.xml @@ -52,12 +52,6 @@ helidon-common-features-api true - - - jakarta.enterprise - jakarta.enterprise.cdi-api - true - diff --git a/integrations/neo4j/metrics/src/main/java/io/helidon/integrations/neo4j/metrics/Neo4jMetricsCdiExtension.java b/integrations/neo4j/metrics/src/main/java/io/helidon/integrations/neo4j/metrics/Neo4jMetricsCdiExtension.java deleted file mode 100644 index 8476571dc28..00000000000 --- a/integrations/neo4j/metrics/src/main/java/io/helidon/integrations/neo4j/metrics/Neo4jMetricsCdiExtension.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2021 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.integrations.neo4j.metrics; - -import jakarta.annotation.Priority; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.context.Initialized; -import jakarta.enterprise.event.Observes; -import jakarta.enterprise.inject.Instance; -import jakarta.enterprise.inject.spi.CDI; -import jakarta.enterprise.inject.spi.Extension; -import org.neo4j.driver.Driver; - -import static jakarta.interceptor.Interceptor.Priority.PLATFORM_AFTER; - -/** - * CDI Extension, instantiated by CDI. - */ -public class Neo4jMetricsCdiExtension implements Extension { - - private void addMetrics(@Observes @Priority(PLATFORM_AFTER + 101) @Initialized(ApplicationScoped.class) Object event) { - Instance driver = CDI.current().select(Driver.class); - Neo4jMetricsSupport.builder() - .driver(driver.get()) - .build() - .initialize(); - } -} diff --git a/integrations/neo4j/metrics/src/main/java/io/helidon/integrations/neo4j/metrics/Neo4jMetricsSupport.java b/integrations/neo4j/metrics/src/main/java/io/helidon/integrations/neo4j/metrics/Neo4jMetricsSupport.java index c95ccf2b378..890db36dc71 100644 --- a/integrations/neo4j/metrics/src/main/java/io/helidon/integrations/neo4j/metrics/Neo4jMetricsSupport.java +++ b/integrations/neo4j/metrics/src/main/java/io/helidon/integrations/neo4j/metrics/Neo4jMetricsSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,6 +54,7 @@ public class Neo4jMetricsSupport { private final LazyValue metricRegistry; private final Driver driver; + @SuppressWarnings("removal") private Neo4jMetricsSupport(Builder builder) { this.driver = builder.driver; // Assuming for the moment that VENDOR is the correct registry to use. diff --git a/integrations/neo4j/metrics/src/main/java/module-info.java b/integrations/neo4j/metrics/src/main/java/module-info.java index 11956dae2e5..ef6c30936ee 100644 --- a/integrations/neo4j/metrics/src/main/java/module-info.java +++ b/integrations/neo4j/metrics/src/main/java/module-info.java @@ -32,18 +32,10 @@ requires io.helidon.common; requires io.helidon.integrations.neo4j; + requires io.helidon.metrics; requires org.neo4j.driver; - requires microprofile.metrics.api; - requires io.helidon.metrics; - - requires static jakarta.cdi; - requires static jakarta.inject; - requires static jakarta.annotation; exports io.helidon.integrations.neo4j.metrics; - - provides jakarta.enterprise.inject.spi.Extension with io.helidon.integrations.neo4j.metrics.Neo4jMetricsCdiExtension; - } diff --git a/integrations/neo4j/neo4j/pom.xml b/integrations/neo4j/neo4j/pom.xml index c76927713e5..5c712875e59 100644 --- a/integrations/neo4j/neo4j/pom.xml +++ b/integrations/neo4j/neo4j/pom.xml @@ -48,12 +48,6 @@ helidon-common-features-api true - - - jakarta.enterprise - jakarta.enterprise.cdi-api - true - io.helidon.config helidon-config-mp diff --git a/integrations/neo4j/neo4j/src/main/java/io/helidon/integrations/neo4j/Neo4j.java b/integrations/neo4j/neo4j/src/main/java/io/helidon/integrations/neo4j/Neo4j.java index 396e1761f16..703f3ac869d 100644 --- a/integrations/neo4j/neo4j/src/main/java/io/helidon/integrations/neo4j/Neo4j.java +++ b/integrations/neo4j/neo4j/src/main/java/io/helidon/integrations/neo4j/Neo4j.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ import java.util.Objects; import java.util.logging.Level; -import io.helidon.config.Config; +import io.helidon.common.config.Config; import org.neo4j.driver.AuthToken; import org.neo4j.driver.AuthTokens; diff --git a/integrations/neo4j/neo4j/src/main/java/io/helidon/integrations/neo4j/Neo4jCdiExtension.java b/integrations/neo4j/neo4j/src/main/java/io/helidon/integrations/neo4j/Neo4jCdiExtension.java deleted file mode 100644 index 71a07a04e70..00000000000 --- a/integrations/neo4j/neo4j/src/main/java/io/helidon/integrations/neo4j/Neo4jCdiExtension.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2021 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.integrations.neo4j; - -import io.helidon.config.Config; -import io.helidon.config.ConfigValue; -import io.helidon.config.mp.MpConfig; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.event.Observes; -import jakarta.enterprise.inject.Any; -import jakarta.enterprise.inject.Default; -import jakarta.enterprise.inject.spi.AfterBeanDiscovery; -import jakarta.enterprise.inject.spi.Extension; -import org.eclipse.microprofile.config.ConfigProvider; -import org.neo4j.driver.Driver; - -/** - * A CDI Extension for Neo4j support. To be used in MP environment. Delegates all of it activities to - * Neo4j for initialization and configuration. - * - */ -public class Neo4jCdiExtension implements Extension { - - private static final String NEO4J_CONFIG_NAME_PREFIX = "neo4j"; - - void afterBeanDiscovery(@Observes AfterBeanDiscovery addEvent) { - addEvent.addBean() - .types(Driver.class) - .qualifiers(Default.Literal.INSTANCE, Any.Literal.INSTANCE) - .scope(ApplicationScoped.class) - .name(Driver.class.getName()) - .beanClass(Driver.class) - .createWith(creationContext -> { - org.eclipse.microprofile.config.Config config = ConfigProvider.getConfig(); - Config helidonConfig = MpConfig.toHelidonConfig(config).get(NEO4J_CONFIG_NAME_PREFIX); - - ConfigValue configValue = helidonConfig.as(Neo4j::create); - if (configValue.isPresent()) { - return configValue.get().driver(); - } - throw new Neo4jException("There is no Neo4j driver configured in configuration under key 'neo4j"); - }); - } -} diff --git a/integrations/neo4j/neo4j/src/main/java/module-info.java b/integrations/neo4j/neo4j/src/main/java/module-info.java index e67f704c923..be636e63549 100644 --- a/integrations/neo4j/neo4j/src/main/java/module-info.java +++ b/integrations/neo4j/neo4j/src/main/java/module-info.java @@ -23,23 +23,18 @@ */ @Preview @Feature(value = "Neo4j integration", - description = "Integration with Neo4j driver", - in = {HelidonFlavor.MP, HelidonFlavor.SE, HelidonFlavor.NIMA}, - path = "Neo4j" + description = "Integration with Neo4j driver", + in = {HelidonFlavor.MP, HelidonFlavor.NIMA}, + invalidIn = HelidonFlavor.SE, + path = "Neo4j" ) module io.helidon.integrations.neo4j { - requires static io.helidon.common.features.api; - requires java.logging; - requires static jakarta.cdi; - requires static jakarta.inject; + requires static io.helidon.common.features.api; requires static io.helidon.config; - requires static io.helidon.config.mp; requires org.neo4j.driver; exports io.helidon.integrations.neo4j; - - provides jakarta.enterprise.inject.spi.Extension with io.helidon.integrations.neo4j.Neo4jCdiExtension; } diff --git a/integrations/oci/sdk/README.md b/integrations/oci/sdk/README.md new file mode 100644 index 00000000000..ecf9251dfbc --- /dev/null +++ b/integrations/oci/sdk/README.md @@ -0,0 +1,72 @@ +# helidon-integrations-oci-sdk + +There are two different approaches for [OCI SDK](https://docs.oracle.com/en-us/iaas/Content/API/SDKDocs/javasdk.htm) integration from Helidon depending upon which type of application you are developing. +* **Helidon MP** (using _CDI_). For this refer to the [cdi](./cdi) module. +* **Helidon SE** (not using _CDI_). For this refer to the information below. + + +## Helidon Injection Framework and OCI SDK Integration +This section only applies for **Helidon SE** type applications. If you are using **Helidon MP** then this section does not apply to you, and you should instead refer to the [cdi](./cdi) module. + +The **Helidon Injection Framework** offers a few different ways to integrate to 3rd party libraries. The **OCI SDK** library, however, is a little different in that a special type/style of fluent builder is needed when using the **OCI SDK**. This means that you can't simply use the _new_ operator when creating instances; you instead need to use the imperative fluent builder style. Fortunately, though, most of the **OCI SDK** follows the same pattern for accessing the API via this fluent builder style. Since the **Helidon Injection Framework** leverages compile-time DI code generation, this arrangement makes it very convenient to generate the correct underpinnings that leverages a template following this fluent builder style. + +The net of all of this is that there are two modules that you will need to integrate DI into your **Helidon SE** application. + +1. The [processor](./processor) module is required to be on your compiler / APT classpath. It will observe cases where you are _@Inject_ services from the **OCI SDK** and then code-generate the appropriate [Activator](../api/src/main/java/io/helidon/pico/api/Activator.java)s for those injected services. Remember, the _processor_ module only needs APT classpath during compilation - it is not needed at runtime. + +2. The [runtime](./runtime) module is required to be on your runtime classpath. This module supplies the default implementation for OCI authentication providers, and OCI extensibility into Helidon. + + +### MP Modules +* [cdi](./cdi) - required to be added as a normal dependency in your final application. + + +### Non-MP Modules +* [processor](./processor) - required to be in the APT classpath. +* [runtime](./runtime) - required to be added as a normal dependency in your final application. +* [tests](./tests) - tests for OCI SDK integration. + + +### Usage + +In your pom.xml, add this plugin to be run as part of the compilation phase: +```pom.xml + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-processor + ${helidon.version} + + + + +``` + +Add the runtime dependency to your pom.xml, along with any other OCI SDK library that is required by your application: +```pom.xml + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-runtime + + + ... + + + com.oracle.oci.sdk + oci-java-sdk-ailanguage + + + com.oracle.oci.sdk + oci-java-sdk-objectstorage + +``` + +Note that if you are using JPMS (i.e., _module-info.java_), then you will also need to be sure to export the _io.helidon.integrations.generated_ derivative package names from your module(s). + +### How it works +See the [InjectionProcessorObserverForOci javadoc](processor/src/main/java/io/helidon/integrations/oci/sdk/processor/InjectionProcessorObserverForOCI.java) for a description. In summary, this processor will observe **OCI SDK** injection points and then code generate **Activators** enabling injection of SDK services in conjuction with the [runtime](./runtime) module on the classpath. diff --git a/integrations/oci/sdk/cdi/pom.xml b/integrations/oci/sdk/cdi/pom.xml index 4e485ec27bf..67090ee1979 100644 --- a/integrations/oci/sdk/cdi/pom.xml +++ b/integrations/oci/sdk/cdi/pom.xml @@ -88,7 +88,7 @@ true - org.jboss + io.smallrye jandex runtime true diff --git a/integrations/oci/sdk/cdi/src/main/java/io/helidon/integrations/oci/sdk/cdi/AdpSelectionStrategy.java b/integrations/oci/sdk/cdi/src/main/java/io/helidon/integrations/oci/sdk/cdi/AdpSelectionStrategy.java index c2bf4b98301..329de31120a 100644 --- a/integrations/oci/sdk/cdi/src/main/java/io/helidon/integrations/oci/sdk/cdi/AdpSelectionStrategy.java +++ b/integrations/oci/sdk/cdi/src/main/java/io/helidon/integrations/oci/sdk/cdi/AdpSelectionStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; import java.io.UncheckedIOException; import java.lang.annotation.Annotation; import java.net.ConnectException; @@ -134,10 +133,10 @@ SimpleAuthenticationDetailsProviderBuilder produceBuilder(Selector selector, Con c.get(OCI_AUTH_PASSPHRASE, String.class).or(() -> c.get(OCI_AUTH_PASSPHRASE + "Characters", String.class)) .ifPresent(b::passPhrase); c.get(OCI_AUTH_PRIVATE_KEY, String.class).or(() -> c.get("oci.auth.privateKey", String.class)) - .ifPresentOrElse(pk -> b.privateKeySupplier((Supplier) new StringPrivateKeySupplier(pk)), - () -> b.privateKeySupplier((Supplier) new SimplePrivateKeySupplier(c.get(OCI_AUTH_PRIVATE_KEY + "-path", String.class) - .orElse(c.get("oci.auth.keyFile", String.class) - .orElse(Paths.get(System.getProperty("user.home"), ".oci", "oci_api_key.pem").toString()))))); + .ifPresentOrElse(pk -> b.privateKeySupplier(new StringPrivateKeySupplier(pk)), + () -> b.privateKeySupplier(new SimplePrivateKeySupplier(c.get(OCI_AUTH_PRIVATE_KEY + "-path", String.class) + .orElse(c.get("oci.auth.keyFile", String.class) + .orElse(Paths.get(System.getProperty("user.home"), ".oci", "oci_api_key.pem").toString()))))); c.get(OCI_AUTH_REGION, Region.class) .ifPresent(b::region); c.get(OCI_AUTH_TENANT_ID, String.class).or(() -> c.get("oci.auth.tenancy", String.class)) diff --git a/integrations/oci/sdk/cdi/src/main/java/io/helidon/integrations/oci/sdk/cdi/OciExtension.java b/integrations/oci/sdk/cdi/src/main/java/io/helidon/integrations/oci/sdk/cdi/OciExtension.java index 23596e73715..f40888e2fea 100644 --- a/integrations/oci/sdk/cdi/src/main/java/io/helidon/integrations/oci/sdk/cdi/OciExtension.java +++ b/integrations/oci/sdk/cdi/src/main/java/io/helidon/integrations/oci/sdk/cdi/OciExtension.java @@ -70,7 +70,10 @@ * asynchronous service client, or asynchronous service * client builder from the Oracle Cloud Infrastructure Java SDK. + * target="_top">Oracle Cloud Infrastructure Java SDK. It is intended for + * Helidon MP, CDI usage scenarios. For usages other than for + * Helidon MP please refer to + * {@code io.helidon.integrations.oci.sdk.runtime.OciExtension} instead. * *

Terminology

* diff --git a/integrations/oci/sdk/pom.xml b/integrations/oci/sdk/pom.xml index 7e3de393970..d67a44fcd62 100644 --- a/integrations/oci/sdk/pom.xml +++ b/integrations/oci/sdk/pom.xml @@ -33,6 +33,9 @@ cdi + processor + runtime + tests
diff --git a/integrations/oci/sdk/processor/README.md b/integrations/oci/sdk/processor/README.md new file mode 100644 index 00000000000..f3f62d15368 --- /dev/null +++ b/integrations/oci/sdk/processor/README.md @@ -0,0 +1,3 @@ +# helidon-integrations-oci-sdk-processor + +Refer to the [helidon-integrations-oci-sdk](../) documentation. diff --git a/integrations/oci/sdk/processor/pom.xml b/integrations/oci/sdk/processor/pom.xml new file mode 100644 index 00000000000..ab52fea7073 --- /dev/null +++ b/integrations/oci/sdk/processor/pom.xml @@ -0,0 +1,86 @@ + + + + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-integrations-oci-sdk-processor + Helidon Integrations OCI Injection Processor + + Helidon Injection Framework APT support for the OCI SDK + + + + io.helidon.pico + helidon-pico-processor + + + com.github.jknack + handlebars + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + com.oracle.oci.sdk + oci-java-sdk-common + test + + + + + com.oracle.oci.sdk + oci-java-sdk-ailanguage + test + + + com.oracle.oci.sdk + oci-java-sdk-objectstorage + test + + + + + com.oracle.oci.sdk + oci-java-sdk-streaming + test + + + + diff --git a/integrations/oci/sdk/processor/src/main/java/io/helidon/integrations/oci/sdk/processor/InjectionProcessorObserverForOCI.java b/integrations/oci/sdk/processor/src/main/java/io/helidon/integrations/oci/sdk/processor/InjectionProcessorObserverForOCI.java new file mode 100644 index 00000000000..74ba2e5438f --- /dev/null +++ b/integrations/oci/sdk/processor/src/main/java/io/helidon/integrations/oci/sdk/processor/InjectionProcessorObserverForOCI.java @@ -0,0 +1,328 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.processor; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.processing.Filer; +import javax.annotation.processing.FilerException; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.TypeElement; +import javax.tools.JavaFileObject; + +import io.helidon.common.LazyValue; +import io.helidon.common.types.TypeInfo; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypedElementInfo; +import io.helidon.pico.processor.spi.PicoAnnotationProcessorObserver; +import io.helidon.pico.processor.spi.ProcessingEvent; +import io.helidon.pico.tools.TemplateHelper; +import io.helidon.pico.tools.ToolsException; + +import jakarta.inject.Inject; + +import static io.helidon.common.types.TypeNameDefault.create; +import static io.helidon.pico.processor.GeneralProcessorUtils.findFirst; +import static io.helidon.pico.processor.GeneralProcessorUtils.isProviderType; +import static java.util.function.Predicate.not; + +/** + * This processor is an implementation of {@link PicoAnnotationProcessorObserver}. When on the APT classpath, it will monitor + * {@link io.helidon.pico.processor.PicoAnnotationProcessor} for all injection points that are + * using the {@code OCI SDK Services} and translate those injection points into code-generated + * {@link io.helidon.pico.api.Activator}s, {@link io.helidon.pico.api.ModuleComponent}, etc. for those services / components. + * This process will therefore make the {@code OCI SDK} set of services injectable by your (non-MP-based) Helidon application, and + * be tailored to exactly what is actually used by your application from the SDK. + *

+ * For example, if your code had this: + *

+ * {@code
+ *   @Inject
+ *   com.oracle.bmc.ObjectStorage objStore;
+ * }
+ * 
+ * This would result in code generating the necessary artifacts at compile time that will make {@code ObjectStorage} injectable. + *

+ * All injection points using the same package name as the OCI SDK (e.g., {@code com.oracle.bmc} as shown with ObjectStorage in + * the case above) will be observed and processed and eventually result in code generation into your + * {@code target/generated-sources} directory. This is the case for any artifact that is attempted to be injected unless there is + * found a configuration signaling an exception to avoid the code generation for the activator. + *

+ * The processor will allows exceptions in one of three ways: + *

    + *
  • via the code directly here - see the {@link #shouldProcess} method.
  • + *
  • via resources on the classpath - the implementation looks for files named {@link #TAG_TYPENAME_EXCEPTIONS} in the same + * package name as this class, and will read those resources during initialization. Each line of this file would be a fully + * qualified type name to avoid processing that type name.
  • + *
  • via {@code -A} directives on the compiler command line. Using the same tag as referenced above. The line can be + * comma-delimited, and each token will be treated as a fully qualified type name to signal that the type should be + * not be processed.
  • + *
+ */ +public class InjectionProcessorObserverForOCI implements PicoAnnotationProcessorObserver { + static final String OCI_ROOT_PACKAGE_NAME_PREFIX = "com.oracle.bmc."; + + // all generated sources will have this package prefix + static final String GENERATED_PREFIX = "io.helidon.integrations.generated."; + + // all generated sources will have this class name suffix + static final String GENERATED_CLIENT_SUFFIX = "$$Oci$$Client"; + static final String GENERATED_CLIENT_BUILDER_SUFFIX = GENERATED_CLIENT_SUFFIX + "Builder"; + static final String GENERATED_OCI_ROOT_PACKAGE_NAME_PREFIX = GENERATED_PREFIX + OCI_ROOT_PACKAGE_NAME_PREFIX; + + static final String TAG_TEMPLATE_SERVICE_CLIENT_PROVIDER_NAME = "service-client-provider.hbs"; + static final String TAG_TEMPLATE_SERVICE_CLIENT_BUILDER_PROVIDER_NAME = "service-client-builder-provider.hbs"; + + // note that these can be used as -A values also + static final String TAG_TYPENAME_EXCEPTIONS = "codegen-exclusions"; + static final String TAG_NO_DOT_EXCEPTIONS = "builder-name-exceptions"; + + static final LazyValue> TYPENAME_EXCEPTIONS = LazyValue + .create(InjectionProcessorObserverForOCI::loadTypeNameExceptions); + static final LazyValue> NO_DOT_EXCEPTIONS = LazyValue + .create(InjectionProcessorObserverForOCI::loadNoDotExceptions); + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public InjectionProcessorObserverForOCI() { + } + + @Override + public void onProcessingEvent(ProcessingEvent event) { + ProcessingEnvironment processingEnv = event.processingEnvironment().orElseThrow(); + layerInManualOptions(processingEnv); + event.elementsOfInterest().stream() + .filter(it -> shouldProcess(it, processingEnv)) + .forEach(it -> process(it, processingEnv)); + } + + private void process(TypedElementInfo element, + ProcessingEnvironment processingEnv) { + if (TypeInfo.KIND_FIELD.equalsIgnoreCase(element.elementTypeKind())) { + process(element.typeName(), processingEnv); + } else if (TypeInfo.KIND_METHOD.equalsIgnoreCase(element.elementTypeKind()) + || TypeInfo.KIND_CONSTRUCTOR.equalsIgnoreCase(element.elementTypeKind())) { + element.parameterArguments().stream() + .filter(it -> shouldProcess(it.typeName(), processingEnv)) + .forEach(it -> process(it.typeName(), processingEnv)); + } + } + + private void process(TypeName ociServiceTypeName, + ProcessingEnvironment processingEnv) { + if (isProviderType(ociServiceTypeName) + || ociServiceTypeName.isOptional()) { + ociServiceTypeName = ociServiceTypeName.typeArguments().get(0); + } + assert (!ociServiceTypeName.generic()) : ociServiceTypeName.name(); + assert (ociServiceTypeName.name().startsWith(OCI_ROOT_PACKAGE_NAME_PREFIX)) : ociServiceTypeName.name(); + + TypeName generatedOciServiceClientTypeName = toGeneratedServiceClientTypeName(ociServiceTypeName); + String serviceClientBody = toBody(TAG_TEMPLATE_SERVICE_CLIENT_PROVIDER_NAME, + ociServiceTypeName, + generatedOciServiceClientTypeName); + codegen(generatedOciServiceClientTypeName, serviceClientBody, processingEnv); + + TypeName generatedOciServiceClientBuilderTypeName = toGeneratedServiceClientBuilderTypeName(ociServiceTypeName); + String serviceClientBuilderBody = toBody(TAG_TEMPLATE_SERVICE_CLIENT_BUILDER_PROVIDER_NAME, + ociServiceTypeName, + generatedOciServiceClientTypeName); + codegen(generatedOciServiceClientBuilderTypeName, serviceClientBuilderBody, processingEnv); + } + + private void codegen(TypeName typeName, + String body, + ProcessingEnvironment processingEnv) { + Filer filer = processingEnv.getFiler(); + try { + JavaFileObject javaSrc = filer.createSourceFile(typeName.name()); + try (Writer os = javaSrc.openWriter()) { + os.write(body); + } + } catch (FilerException x) { + processingEnv.getMessager().printWarning("Failed to write java file: " + x); + } catch (Exception x) { + System.getLogger(getClass().getName()).log(System.Logger.Level.ERROR, "Failed to write java file: " + x, x); + processingEnv.getMessager().printError("Failed to write java file: " + x); + } + } + + static TypeName toGeneratedServiceClientTypeName(TypeName typeName) { + return create(GENERATED_PREFIX + typeName.packageName(), + typeName.className() + GENERATED_CLIENT_SUFFIX); + } + + static TypeName toGeneratedServiceClientBuilderTypeName(TypeName typeName) { + return create(GENERATED_PREFIX + typeName.packageName(), + typeName.className() + GENERATED_CLIENT_BUILDER_SUFFIX); + } + + static String toBody(String templateName, + TypeName ociServiceTypeName, + TypeName generatedOciActivatorTypeName) { + TemplateHelper templateHelper = TemplateHelper.create(); + String template = loadTemplate(templateName); + Map subst = new HashMap<>(); + subst.put("classname", ociServiceTypeName.name()); + subst.put("simpleclassname", ociServiceTypeName.className()); + subst.put("packagename", generatedOciActivatorTypeName.packageName()); + subst.put("generatedanno", templateHelper.generatedStickerFor(PicoAnnotationProcessorObserver.class.getName())); + subst.put("dot", maybeDot(ociServiceTypeName)); + subst.put("usesRegion", usesRegion(ociServiceTypeName)); + return templateHelper.applySubstitutions(template, subst, true).trim(); + } + + static String maybeDot(TypeName ociServiceTypeName) { + return NO_DOT_EXCEPTIONS.get().contains(ociServiceTypeName.name()) ? "" : "."; + } + + static boolean usesRegion(TypeName ociServiceTypeName) { + // it turns out that the same exceptions used for dotting the builder also applies to whether it uses region + return !NO_DOT_EXCEPTIONS.get().contains(ociServiceTypeName.name()); + } + + static String loadTemplate(String name) { + String path = "io/helidon/integrations/oci/sdk/processor/templates/" + name; + try { + InputStream in = InjectionProcessorObserverForOCI.class.getClassLoader().getResourceAsStream(path); + if (in == null) { + throw new IOException("Could not find template " + path + " on classpath."); + } + try (in) { + return new String(in.readAllBytes(), StandardCharsets.UTF_8); + } + } catch (Exception e) { + throw new ToolsException(e.getMessage(), e); + } + } + + static Set loadTypeNameExceptions() { + return loadSet(TAG_TYPENAME_EXCEPTIONS); + } + + static Set loadNoDotExceptions() { + return loadSet(TAG_NO_DOT_EXCEPTIONS); + } + + static void layerInManualOptions(ProcessingEnvironment processingEnv) { + Map opts = processingEnv.getOptions(); + TYPENAME_EXCEPTIONS.get().addAll(splitToSet(opts.get(TAG_TYPENAME_EXCEPTIONS))); + NO_DOT_EXCEPTIONS.get().addAll(splitToSet(opts.get(TAG_NO_DOT_EXCEPTIONS))); + } + + static Set splitToSet(String val) { + if (val == null) { + return Set.of(); + } + List list = Arrays.stream(val.split(",")) + .map(String::trim) + .filter(not(String::isEmpty)) + .filter(not(s -> s.startsWith("#"))) + .toList(); + return new LinkedHashSet<>(list); + } + + static Set loadSet(String name) { + // note that we need to keep this mutable for later when we process the env options passed manually in + Set result = new LinkedHashSet<>(); + + name = InjectionProcessorObserverForOCI.class.getPackageName().replace(".", "/") + "/" + name + ".txt"; + try { + Enumeration resources = InjectionProcessorObserverForOCI.class.getClassLoader().getResources(name); + while (resources.hasMoreElements()) { + URL url = resources.nextElement(); + try ( + InputStream in = url.openStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)) + ) { + reader.lines() + .map(String::trim) + .filter(not(String::isEmpty)) + .filter(not(s -> s.startsWith("#"))) + .forEach(result::add); + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + return result; + } + + static boolean shouldProcess(TypedElementInfo element, + ProcessingEnvironment processingEnv) { + if (findFirst(Inject.class, element.annotations()).isEmpty()) { + return false; + } + + if (TypeInfo.KIND_FIELD.equalsIgnoreCase(element.elementTypeKind())) { + return shouldProcess(element.typeName(), processingEnv); + } else if (TypeInfo.KIND_METHOD.equalsIgnoreCase(element.elementTypeKind()) + || TypeInfo.KIND_CONSTRUCTOR.equalsIgnoreCase(element.elementTypeKind())) { + return element.parameterArguments().stream() + .anyMatch(it -> shouldProcess(it.typeName(), processingEnv)); + } + + return false; + } + + static boolean shouldProcess(TypeName typeName, + ProcessingEnvironment processingEnv) { + if ((typeName.typeArguments().size() > 0) + && (isProviderType(typeName) || typeName.isOptional())) { + typeName = typeName.typeArguments().get(0); + } + + String name = typeName.name(); + if (!name.startsWith(OCI_ROOT_PACKAGE_NAME_PREFIX) + || name.endsWith(".Builder") + || name.endsWith("Client") + || name.endsWith("ClientBuilder") + || TYPENAME_EXCEPTIONS.get().contains(name)) { + return false; + } + + if (processingEnv != null) { + // check to see if we already generated it before, and if so we can skip creating it again + String generatedTypeName = toGeneratedServiceClientTypeName(typeName).name(); + TypeElement typeElement = processingEnv.getElementUtils() + .getTypeElement(generatedTypeName); + return (typeElement == null); + } + + return true; + } + +} diff --git a/integrations/oci/sdk/processor/src/main/java/io/helidon/integrations/oci/sdk/processor/ModuleComponentNamerDefault.java b/integrations/oci/sdk/processor/src/main/java/io/helidon/integrations/oci/sdk/processor/ModuleComponentNamerDefault.java new file mode 100644 index 00000000000..76e943554f2 --- /dev/null +++ b/integrations/oci/sdk/processor/src/main/java/io/helidon/integrations/oci/sdk/processor/ModuleComponentNamerDefault.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.processor; + +import java.util.Collection; +import java.util.Optional; + +import io.helidon.common.types.TypeName; +import io.helidon.pico.tools.spi.ModuleComponentNamer; + +import static java.util.function.Predicate.not; + +/** + * Avoids using any OCI SDK package name(s) as the {@link io.helidon.pico.api.ModuleComponent} name that is code-generated. + */ +public class ModuleComponentNamerDefault implements ModuleComponentNamer { + + /** + * Service loader based constructor. + * + * @deprecated this is a Java ServiceLoader implementation and the constructor should not be used directly + */ + @Deprecated + public ModuleComponentNamerDefault() { + } + + @Override + public Optional suggestedPackageName(Collection typeNames) { + String suggested = typeNames.stream() + .sorted() + .filter(not(it -> it.name().startsWith(InjectionProcessorObserverForOCI.GENERATED_OCI_ROOT_PACKAGE_NAME_PREFIX))) + .filter(not(it -> it.name().startsWith(InjectionProcessorObserverForOCI.OCI_ROOT_PACKAGE_NAME_PREFIX))) + .map(TypeName::packageName) + .findFirst().orElse(null); + return Optional.ofNullable(suggested); + } + +} diff --git a/integrations/oci/sdk/processor/src/main/java/io/helidon/integrations/oci/sdk/processor/package-info.java b/integrations/oci/sdk/processor/src/main/java/io/helidon/integrations/oci/sdk/processor/package-info.java new file mode 100644 index 00000000000..69f918b1b5c --- /dev/null +++ b/integrations/oci/sdk/processor/src/main/java/io/helidon/integrations/oci/sdk/processor/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon Injection Processor Integration for the OCI SDK. + */ +package io.helidon.integrations.oci.sdk.processor; diff --git a/integrations/oci/sdk/processor/src/main/java/module-info.java b/integrations/oci/sdk/processor/src/main/java/module-info.java new file mode 100644 index 00000000000..04f1cd4d51b --- /dev/null +++ b/integrations/oci/sdk/processor/src/main/java/module-info.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon Injection Integrations for OCI SDK. + */ +module io.helidon.integrations.oci.sdk.processor { + requires static jakarta.inject; + requires static jakarta.annotation; + requires static jdk.jfr; + requires java.compiler; + requires handlebars; + requires transitive io.helidon.pico.processor; + + exports io.helidon.integrations.oci.sdk.processor; + + uses io.helidon.pico.processor.spi.PicoAnnotationProcessorObserver; + uses io.helidon.pico.tools.spi.ModuleComponentNamer; + + provides io.helidon.pico.processor.spi.PicoAnnotationProcessorObserver with + io.helidon.integrations.oci.sdk.processor.InjectionProcessorObserverForOCI; + provides io.helidon.pico.tools.spi.ModuleComponentNamer with + io.helidon.integrations.oci.sdk.processor.ModuleComponentNamerDefault; +} diff --git a/integrations/oci/sdk/processor/src/main/resources/io/helidon/integrations/oci/sdk/processor/builder-name-exceptions.txt b/integrations/oci/sdk/processor/src/main/resources/io/helidon/integrations/oci/sdk/processor/builder-name-exceptions.txt new file mode 100644 index 00000000000..19aac9b165f --- /dev/null +++ b/integrations/oci/sdk/processor/src/main/resources/io/helidon/integrations/oci/sdk/processor/builder-name-exceptions.txt @@ -0,0 +1,20 @@ +# +# Copyright (c) 2023 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# these classes do not follow the normal dotted names for the Client Builder - see `{{dot}}` in the hbs template files + +com.oracle.bmc.streaming.Stream +com.oracle.bmc.streaming.StreamAsync diff --git a/integrations/oci/sdk/processor/src/main/resources/io/helidon/integrations/oci/sdk/processor/codegen-exclusions.txt b/integrations/oci/sdk/processor/src/main/resources/io/helidon/integrations/oci/sdk/processor/codegen-exclusions.txt new file mode 100644 index 00000000000..e42f64c50ee --- /dev/null +++ b/integrations/oci/sdk/processor/src/main/resources/io/helidon/integrations/oci/sdk/processor/codegen-exclusions.txt @@ -0,0 +1,21 @@ +# +# Copyright (c) 2023 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# these classes will result in code generation to be skipped if they are @Inject'ed + +com.oracle.bmc.Region +com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider +com.oracle.bmc.circuitbreaker.OciCircuitBreaker diff --git a/integrations/oci/sdk/processor/src/main/resources/io/helidon/integrations/oci/sdk/processor/templates/service-client-builder-provider.hbs b/integrations/oci/sdk/processor/src/main/resources/io/helidon/integrations/oci/sdk/processor/templates/service-client-builder-provider.hbs new file mode 100644 index 00000000000..0ba38356faf --- /dev/null +++ b/integrations/oci/sdk/processor/src/main/resources/io/helidon/integrations/oci/sdk/processor/templates/service-client-builder-provider.hbs @@ -0,0 +1,61 @@ +{{! +Copyright (c) 2023 Oracle and/or its affiliates. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +}}{{#header}}{{.}} +{{/header}} +package {{packagename}}; + +import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; + +import {{classname}}Client; +import {{classname}}Client{{dot}}Builder;{{#if usesRegion}} +import com.oracle.bmc.Region;{{/if}} + +import io.helidon.common.Weight; +import io.helidon.pico.api.ContextualServiceQuery; +import io.helidon.pico.api.InjectionPointProvider; +import io.helidon.pico.api.ServiceInfoBasics; + +import jakarta.annotation.Generated; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; +import java.util.Optional; + +@Generated({{{generatedanno}}}) +@Singleton +@Weight(ServiceInfoBasics.DEFAULT_PICO_WEIGHT) +class {{simpleclassname}}$$Oci$$ClientBuilder implements InjectionPointProvider<{{simpleclassname}}Client{{dot}}Builder> { +{{#if usesRegion}} private final InjectionPointProvider regionProvider; + + @Deprecated + @Inject + {{simpleclassname}}$$Oci$$ClientBuilder(Provider regionProvider) { + this.regionProvider = (InjectionPointProvider) regionProvider; + } +{{else}} + @Deprecated + @Inject + {{simpleclassname}}$$Oci$$ClientBuilder() { + } +{{/if}} + @Override + public Optional<{{simpleclassname}}Client{{dot}}Builder> first(ContextualServiceQuery query) { + {{simpleclassname}}Client{{dot}}Builder builder = {{simpleclassname}}Client.builder();{{#if usesRegion}} + regionProvider.first(query).ifPresent(it -> builder.region(it));{{/if}} + return Optional.of(builder); + } + +} diff --git a/integrations/oci/sdk/processor/src/main/resources/io/helidon/integrations/oci/sdk/processor/templates/service-client-provider.hbs b/integrations/oci/sdk/processor/src/main/resources/io/helidon/integrations/oci/sdk/processor/templates/service-client-provider.hbs new file mode 100644 index 00000000000..c3483bab9b5 --- /dev/null +++ b/integrations/oci/sdk/processor/src/main/resources/io/helidon/integrations/oci/sdk/processor/templates/service-client-provider.hbs @@ -0,0 +1,57 @@ +{{! +Copyright (c) 2023 Oracle and/or its affiliates. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +}}{{#header}}{{.}} +{{/header}} +package {{packagename}}; + +import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; + +import {{classname}}Client; +import {{classname}}Client{{dot}}Builder; + +import io.helidon.common.Weight; +import io.helidon.pico.api.ContextualServiceQuery; +import io.helidon.pico.api.ExternalContracts; +import io.helidon.pico.api.InjectionPointProvider; +import io.helidon.pico.api.ServiceInfoBasics; + +import jakarta.annotation.Generated; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; +import java.util.Optional; + +@Generated({{{generatedanno}}}) +@Singleton +@Weight(ServiceInfoBasics.DEFAULT_PICO_WEIGHT) +@ExternalContracts({{classname}}.class) +class {{simpleclassname}}$$Oci$$Client implements InjectionPointProvider<{{simpleclassname}}Client> { + private final InjectionPointProvider authProvider; + private final InjectionPointProvider<{{simpleclassname}}Client{{dot}}Builder> builderProvider; + + @Deprecated + @Inject + {{simpleclassname}}$$Oci$$Client(Provider authProvider, Provider<{{simpleclassname}}Client{{dot}}Builder> builderProvider) { + this.authProvider = (InjectionPointProvider) authProvider; + this.builderProvider = (InjectionPointProvider<{{simpleclassname}}Client{{dot}}Builder>) builderProvider; + } + + @Override + public Optional<{{simpleclassname}}Client> first(ContextualServiceQuery query) { + return Optional.of(builderProvider.first(query).orElseThrow().build(authProvider.first(query).orElseThrow())); + } + +} diff --git a/integrations/oci/sdk/processor/src/test/java/io/helidon/integrations/oci/sdk/processor/InjectionProcessorObserverForOCITest.java b/integrations/oci/sdk/processor/src/test/java/io/helidon/integrations/oci/sdk/processor/InjectionProcessorObserverForOCITest.java new file mode 100644 index 00000000000..32136de0168 --- /dev/null +++ b/integrations/oci/sdk/processor/src/test/java/io/helidon/integrations/oci/sdk/processor/InjectionProcessorObserverForOCITest.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.processor; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Set; + +import io.helidon.common.types.TypeName; +import io.helidon.pico.tools.ToolsException; + +import com.oracle.bmc.objectstorage.ObjectStorage; +import com.oracle.bmc.streaming.Stream; +import com.oracle.bmc.streaming.StreamAdmin; +import com.oracle.bmc.streaming.StreamAsync; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.Test; + +import static io.helidon.common.types.TypeNameDefault.create; +import static io.helidon.common.types.TypeNameDefault.createFromTypeName; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; + +class InjectionProcessorObserverForOCITest { + + @Test + void generatedPicoArtifactsForTypicalOciServices() { + TypeName ociServiceType = create(ObjectStorage.class); + + TypeName generatedOciServiceClientTypeName = InjectionProcessorObserverForOCI.toGeneratedServiceClientTypeName(ociServiceType); + assertThat(generatedOciServiceClientTypeName.name(), + equalTo("io.helidon.integrations.generated." + ociServiceType.name() + "$$Oci$$Client")); + + String serviceClientBody = InjectionProcessorObserverForOCI.toBody(InjectionProcessorObserverForOCI.TAG_TEMPLATE_SERVICE_CLIENT_PROVIDER_NAME, + ociServiceType, + generatedOciServiceClientTypeName); + assertThat(serviceClientBody, + equalTo(loadStringFromResource("expected/objectstorage$$Oci$$Client._java_"))); + + TypeName generatedOciServiceClientBuilderTypeName = InjectionProcessorObserverForOCI.toGeneratedServiceClientBuilderTypeName(ociServiceType); + assertThat(generatedOciServiceClientBuilderTypeName.name(), + equalTo("io.helidon.integrations.generated." + ociServiceType.name() + "$$Oci$$ClientBuilder")); + + String serviceClientBuilderBody = InjectionProcessorObserverForOCI.toBody(InjectionProcessorObserverForOCI.TAG_TEMPLATE_SERVICE_CLIENT_BUILDER_PROVIDER_NAME, + ociServiceType, + generatedOciServiceClientTypeName); + assertThat(serviceClientBuilderBody, + equalTo(loadStringFromResource("expected/objectstorage$$Oci$$ClientBuilder._java_"))); + } + + @Test + void oddballServiceTypeNames() { + TypeName ociServiceType = create(Stream.class); + MatcherAssert.assertThat(InjectionProcessorObserverForOCI.maybeDot(ociServiceType), + equalTo("")); + MatcherAssert.assertThat(InjectionProcessorObserverForOCI.usesRegion(ociServiceType), + equalTo(false)); + + ociServiceType = create(StreamAsync.class); + MatcherAssert.assertThat(InjectionProcessorObserverForOCI.maybeDot(ociServiceType), + equalTo("")); + MatcherAssert.assertThat(InjectionProcessorObserverForOCI.usesRegion(ociServiceType), + equalTo(false)); + + ociServiceType = create(StreamAdmin.class); + MatcherAssert.assertThat(InjectionProcessorObserverForOCI.maybeDot(ociServiceType), + equalTo(".")); + MatcherAssert.assertThat(InjectionProcessorObserverForOCI.usesRegion(ociServiceType), + equalTo(true)); + } + + @Test + void testShouldProcess() { + TypeName typeName = create(ObjectStorage.class); + MatcherAssert.assertThat(InjectionProcessorObserverForOCI.shouldProcess(typeName, null), + is(true)); + + typeName = createFromTypeName("com.oracle.bmc.circuitbreaker.OciCircuitBreaker"); + MatcherAssert.assertThat(InjectionProcessorObserverForOCI.shouldProcess(typeName, null), + is(false)); + + typeName = createFromTypeName("com.oracle.another.Service"); + MatcherAssert.assertThat(InjectionProcessorObserverForOCI.shouldProcess(typeName, null), + is(false)); + + typeName = createFromTypeName("com.oracle.bmc.Service"); + MatcherAssert.assertThat(InjectionProcessorObserverForOCI.shouldProcess(typeName, null), + is(true)); + + typeName = createFromTypeName("com.oracle.bmc.ServiceClient"); + MatcherAssert.assertThat(InjectionProcessorObserverForOCI.shouldProcess(typeName, null), + is(false)); + + typeName = createFromTypeName("com.oracle.bmc.ServiceClientBuilder"); + MatcherAssert.assertThat(InjectionProcessorObserverForOCI.shouldProcess(typeName, null), + is(false)); + } + + @Test + void loadTypeNameExceptions() { + Set set = InjectionProcessorObserverForOCI.TYPENAME_EXCEPTIONS.get(); + set.addAll(InjectionProcessorObserverForOCI.splitToSet(" M1, M2,,, ")); + assertThat(set, + containsInAnyOrder("M1", + "M2", + "test1", + "com.oracle.bmc.Region", + "com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider", + "com.oracle.bmc.circuitbreaker.OciCircuitBreaker" + )); + } + + @Test + void loadNoDotExceptions() { + Set set = InjectionProcessorObserverForOCI.NO_DOT_EXCEPTIONS.get(); + set.addAll(InjectionProcessorObserverForOCI.splitToSet("Manual1, Manual2 ")); + assertThat(set, + containsInAnyOrder("Manual1", + "Manual2", + "test2", + "com.oracle.bmc.streaming.Stream", + "com.oracle.bmc.streaming.StreamAsync" + )); + } + + static String loadStringFromResource(String resourceNamePath) { + try { + try (InputStream in = InjectionProcessorObserverForOCITest.class.getClassLoader().getResourceAsStream(resourceNamePath)) { + return new String(in.readAllBytes(), StandardCharsets.UTF_8).trim(); + } + } catch (Exception e) { + throw new ToolsException("Failed to load: " + resourceNamePath, e); + } + } + +} diff --git a/integrations/oci/sdk/processor/src/test/java/io/helidon/integrations/oci/sdk/processor/ModuleComponentNamerDefaultTest.java b/integrations/oci/sdk/processor/src/test/java/io/helidon/integrations/oci/sdk/processor/ModuleComponentNamerDefaultTest.java new file mode 100644 index 00000000000..c8890abc9f8 --- /dev/null +++ b/integrations/oci/sdk/processor/src/test/java/io/helidon/integrations/oci/sdk/processor/ModuleComponentNamerDefaultTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.processor; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static io.helidon.common.types.TypeNameDefault.createFromTypeName; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +class ModuleComponentNamerDefaultTest { + + @Test + void suggestedPackageName() { + ModuleComponentNamerDefault namer = new ModuleComponentNamerDefault(); + assertThat(namer.suggestedPackageName(Set.of()), + optionalEmpty()); + assertThat(namer.suggestedPackageName(Set.of(createFromTypeName("com.oracle.bmc.whatever.Service"))), + optionalEmpty()); + assertThat(namer.suggestedPackageName(Set.of(createFromTypeName("com.oracle.another.whatever.Service"))), + optionalValue(equalTo("com.oracle.another.whatever"))); + assertThat(namer.suggestedPackageName(Set.of(createFromTypeName("com.oracle.bmc.Service"), + createFromTypeName("com.oracle.another.whatever.Service"))), + optionalValue(equalTo("com.oracle.another.whatever"))); + } + +} diff --git a/integrations/oci/sdk/processor/src/test/resources/expected/objectstorage$$Oci$$Client._java_ b/integrations/oci/sdk/processor/src/test/resources/expected/objectstorage$$Oci$$Client._java_ new file mode 100644 index 00000000000..bffdf814618 --- /dev/null +++ b/integrations/oci/sdk/processor/src/test/resources/expected/objectstorage$$Oci$$Client._java_ @@ -0,0 +1,41 @@ +package io.helidon.integrations.generated.com.oracle.bmc.objectstorage; + +import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; + +import com.oracle.bmc.objectstorage.ObjectStorageClient; +import com.oracle.bmc.objectstorage.ObjectStorageClient.Builder; + +import io.helidon.common.Weight; +import io.helidon.pico.api.ContextualServiceQuery; +import io.helidon.pico.api.ExternalContracts; +import io.helidon.pico.api.InjectionPointProvider; +import io.helidon.pico.api.ServiceInfoBasics; + +import jakarta.annotation.Generated; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; +import java.util.Optional; + +@Generated(value = "io.helidon.pico.processor.spi.PicoAnnotationProcessorObserver", comments = "version=1") +@Singleton +@Weight(ServiceInfoBasics.DEFAULT_PICO_WEIGHT) +@ExternalContracts(com.oracle.bmc.objectstorage.ObjectStorage.class) +class ObjectStorage$$Oci$$Client implements InjectionPointProvider { + private final InjectionPointProvider authProvider; + private final InjectionPointProvider builderProvider; + + @Deprecated + @Inject + ObjectStorage$$Oci$$Client(Provider authProvider, Provider builderProvider) { + this.authProvider = (InjectionPointProvider) authProvider; + this.builderProvider = (InjectionPointProvider) builderProvider; + } + + @Override + public Optional first(ContextualServiceQuery query) { + return Optional.of(builderProvider.first(query).orElseThrow().build(authProvider.first(query).orElseThrow())); + } + +} diff --git a/integrations/oci/sdk/processor/src/test/resources/expected/objectstorage$$Oci$$ClientBuilder._java_ b/integrations/oci/sdk/processor/src/test/resources/expected/objectstorage$$Oci$$ClientBuilder._java_ new file mode 100644 index 00000000000..16f83996ddf --- /dev/null +++ b/integrations/oci/sdk/processor/src/test/resources/expected/objectstorage$$Oci$$ClientBuilder._java_ @@ -0,0 +1,40 @@ +package io.helidon.integrations.generated.com.oracle.bmc.objectstorage; + +import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; + +import com.oracle.bmc.objectstorage.ObjectStorageClient; +import com.oracle.bmc.objectstorage.ObjectStorageClient.Builder; +import com.oracle.bmc.Region; + +import io.helidon.common.Weight; +import io.helidon.pico.api.ContextualServiceQuery; +import io.helidon.pico.api.InjectionPointProvider; +import io.helidon.pico.api.ServiceInfoBasics; + +import jakarta.annotation.Generated; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; +import java.util.Optional; + +@Generated(value = "io.helidon.pico.processor.spi.PicoAnnotationProcessorObserver", comments = "version=1") +@Singleton +@Weight(ServiceInfoBasics.DEFAULT_PICO_WEIGHT) +class ObjectStorage$$Oci$$ClientBuilder implements InjectionPointProvider { + private final InjectionPointProvider regionProvider; + + @Deprecated + @Inject + ObjectStorage$$Oci$$ClientBuilder(Provider regionProvider) { + this.regionProvider = (InjectionPointProvider) regionProvider; + } + + @Override + public Optional first(ContextualServiceQuery query) { + ObjectStorageClient.Builder builder = ObjectStorageClient.builder(); + regionProvider.first(query).ifPresent(it -> builder.region(it)); + return Optional.of(builder); + } + +} diff --git a/examples/integrations/oci/atp-reactive/src/main/resources/META-INF/helidon/serial-config.properties b/integrations/oci/sdk/processor/src/test/resources/io/helidon/integrations/oci/sdk/processor/builder-name-exceptions.txt similarity index 86% rename from examples/integrations/oci/atp-reactive/src/main/resources/META-INF/helidon/serial-config.properties rename to integrations/oci/sdk/processor/src/test/resources/io/helidon/integrations/oci/sdk/processor/builder-name-exceptions.txt index 2ef2caf1d93..43a7bfd19f0 100644 --- a/examples/integrations/oci/atp-reactive/src/main/resources/META-INF/helidon/serial-config.properties +++ b/integrations/oci/sdk/processor/src/test/resources/io/helidon/integrations/oci/sdk/processor/builder-name-exceptions.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 Oracle and/or its affiliates. +# Copyright (c) 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,5 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # - -pattern=oracle.sql.converter.* +test2 diff --git a/tests/integration/native-image/se-1/src/main/resources/META-INF/native-image/native-image.properties b/integrations/oci/sdk/processor/src/test/resources/io/helidon/integrations/oci/sdk/processor/codegen-exclusions.txt similarity index 78% rename from tests/integration/native-image/se-1/src/main/resources/META-INF/native-image/native-image.properties rename to integrations/oci/sdk/processor/src/test/resources/io/helidon/integrations/oci/sdk/processor/codegen-exclusions.txt index 0bec3d2415f..868b3a34b6a 100644 --- a/tests/integration/native-image/se-1/src/main/resources/META-INF/native-image/native-image.properties +++ b/integrations/oci/sdk/processor/src/test/resources/io/helidon/integrations/oci/sdk/processor/codegen-exclusions.txt @@ -1,5 +1,5 @@ # -# Copyright (c) 2019, 2021 Oracle and/or its affiliates. +# Copyright (c) 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,6 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # - -# We must explicitly include resources from other modules -Args=-H:IncludeResources=web-jar/.* +test1 diff --git a/integrations/oci/sdk/runtime/README.md b/integrations/oci/sdk/runtime/README.md new file mode 100644 index 00000000000..3c5b2eac16f --- /dev/null +++ b/integrations/oci/sdk/runtime/README.md @@ -0,0 +1,3 @@ +# helidon-integrations-oci-sdk-runtime + +Refer to the [helidon-integrations-oci-sdk](../) documentation. diff --git a/integrations/neo4j/health/etc/spotbugs/exclude.xml b/integrations/oci/sdk/runtime/etc/spotbugs/exclude.xml similarity index 72% rename from integrations/neo4j/health/etc/spotbugs/exclude.xml rename to integrations/oci/sdk/runtime/etc/spotbugs/exclude.xml index 5ceb451eb78..e832dfab49e 100644 --- a/integrations/neo4j/health/etc/spotbugs/exclude.xml +++ b/integrations/oci/sdk/runtime/etc/spotbugs/exclude.xml @@ -1,6 +1,7 @@ + xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd"> - - - + + + diff --git a/integrations/oci/sdk/runtime/pom.xml b/integrations/oci/sdk/runtime/pom.xml new file mode 100644 index 00000000000..fba3d5d0352 --- /dev/null +++ b/integrations/oci/sdk/runtime/pom.xml @@ -0,0 +1,115 @@ + + + + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-integrations-oci-sdk-runtime + Helidon Integrations OCI Injection Runtime + + Helidon Injection Framework Runtime support for the OCI SDK + + + etc/spotbugs/exclude.xml + + + + + com.oracle.oci.sdk + oci-java-sdk-common + compile + + + io.helidon.builder + helidon-builder-config + + + io.helidon.pico + helidon-pico-runtime + + + jakarta.annotation + jakarta.annotation-api + provided + + + jakarta.inject + jakarta.inject-api + provided + + + io.helidon.pico.configdriven + helidon-pico-configdriven-processor + true + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + io.helidon.pico + helidon-pico-testing + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + io.helidon.pico.configdriven + helidon-pico-configdriven-processor + ${helidon.version} + + + io.helidon.builder + helidon-builder-config-processor + ${helidon.version} + + + + + + + + diff --git a/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciAuthenticationDetailsProvider.java b/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciAuthenticationDetailsProvider.java new file mode 100644 index 00000000000..19297cba766 --- /dev/null +++ b/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciAuthenticationDetailsProvider.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.runtime; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +import io.helidon.common.Weight; +import io.helidon.common.types.AnnotationAndValue; +import io.helidon.pico.api.ContextualServiceQuery; +import io.helidon.pico.api.InjectionPointInfo; +import io.helidon.pico.api.InjectionPointProvider; +import io.helidon.pico.api.ServiceInfoBasics; + +import com.oracle.bmc.ConfigFileReader; +import com.oracle.bmc.Region; +import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; +import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; +import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider; +import com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider; +import com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider; +import com.oracle.bmc.auth.SimplePrivateKeySupplier; +import com.oracle.bmc.auth.StringPrivateKeySupplier; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +import static io.helidon.common.types.AnnotationAndValueDefault.findFirst; + +/** + * This (overridable) provider will provide the default implementation for {@link AbstractAuthenticationDetailsProvider}. + * + * @see OciExtension + * @see OciConfigBean + */ +@Singleton +@Weight(ServiceInfoBasics.DEFAULT_PICO_WEIGHT) +class OciAuthenticationDetailsProvider implements InjectionPointProvider { + static final System.Logger LOGGER = System.getLogger(OciAuthenticationDetailsProvider.class.getName()); + + static final String TAG_AUTO = "auto"; + static final String TAG_CONFIG = "config"; + static final String TAG_CONFIG_FILE = "config-file"; + static final String TAG_INSTANCE_PRINCIPALS = "instance-principals"; + static final String TAG_RESOURCE_PRINCIPALS = "resource-principals"; + + static final List ALL_STRATEGIES = List.of(TAG_CONFIG, + TAG_CONFIG_FILE, + TAG_INSTANCE_PRINCIPALS, + TAG_RESOURCE_PRINCIPALS); + + OciAuthenticationDetailsProvider() { + } + + @Override + public Optional first(ContextualServiceQuery query) { + OciConfigBean ociConfig = OciExtension.ociConfig(); + + String requestedNamedProfile = toNamedProfile(query.injectionPointInfo().orElse(null)); + if (requestedNamedProfile != null && !requestedNamedProfile.isBlank()) { + ociConfig = OciConfigBeanDefault.toBuilder(ociConfig).configProfile(requestedNamedProfile).build(); + } + + return Optional.of(select(ociConfig)); + } + + private static AbstractAuthenticationDetailsProvider select(OciConfigBean ociConfigBean) { + List strategies = AuthStrategy.convert(ociConfigBean.potentialAuthStrategies()); + for (AuthStrategy s : strategies) { + if (s.isAvailable(ociConfigBean)) { + LOGGER.log(System.Logger.Level.DEBUG, "Using authentication strategy " + s + + "; selected AbstractAuthenticationDetailsProvider " + s.type().getTypeName()); + return s.select(ociConfigBean); + } else { + LOGGER.log(System.Logger.Level.TRACE, "Skipping authentication strategy " + s + " because it is not available"); + } + } + throw new NoSuchElementException("No instances of " + + AbstractAuthenticationDetailsProvider.class.getName() + + " available for use. Verify your configuration named: " + OciConfigBean.NAME); + } + + static String toNamedProfile(InjectionPointInfo ipi) { + if (ipi == null) { + return null; + } + + Optional named = findFirst(Named.class.getName(), ipi.qualifiers()); + if (named.isEmpty()) { + return null; + } + + String nameProfile = named.get().value().orElse(null); + if (nameProfile == null || nameProfile.isBlank()) { + return null; + } + + return nameProfile.trim(); + } + + static boolean canReadPath(String pathName) { + return (pathName != null && Path.of(pathName).toFile().canRead()); + } + + static String userHomePrivateKeyPath(OciConfigBean ociConfigBean) { + return Paths.get(System.getProperty("user.home"), ".oci", ociConfigBean.authKeyFile().orElseThrow()).toString(); + } + + + enum AuthStrategy { + /** + * Auto selection of the auth strategy. + */ + AUTO(TAG_AUTO, + AbstractAuthenticationDetailsProvider.class, + (ociConfigBean) -> true, + OciAuthenticationDetailsProvider::select), + + /** + * Corresponds to {@link SimpleAuthenticationDetailsProvider}. + * + * @see OciConfigBean#simpleConfigIsPresent() + */ + CONFIG(TAG_CONFIG, + SimpleAuthenticationDetailsProvider.class, + OciConfigBean::simpleConfigIsPresent, + (ociConfigBean) -> { + SimpleAuthenticationDetailsProvider.SimpleAuthenticationDetailsProviderBuilder b = + SimpleAuthenticationDetailsProvider.builder(); + ociConfigBean.authTenantId().ifPresent(b::tenantId); + ociConfigBean.authUserId().ifPresent(b::userId); + ociConfigBean.authRegion().ifPresent(it -> b.region(Region.fromRegionCodeOrId(it))); + ociConfigBean.authFingerprint().ifPresent(b::fingerprint); + ociConfigBean.authPassphrase().ifPresent(chars -> b.passPhrase(String.valueOf(chars))); + ociConfigBean.authPrivateKey() + .ifPresentOrElse(pk -> b.privateKeySupplier(new StringPrivateKeySupplier(String.valueOf(pk))), + () -> b.privateKeySupplier(new SimplePrivateKeySupplier( + userHomePrivateKeyPath(ociConfigBean)))); + return b.build(); + }), + + /** + * Corresponds to {@link ConfigFileAuthenticationDetailsProvider}. + * + * @see OciConfigBean + */ + CONFIG_FILE(TAG_CONFIG_FILE, + ConfigFileAuthenticationDetailsProvider.class, + (configBean) -> configBean.fileConfigIsPresent() + && canReadPath(configBean.configPath().orElse(null)), + (configBean) -> { + // https://github.com/oracle/oci-java-sdk/blob/master/bmc-common/src/main/java/com/oracle/bmc/auth/ConfigFileAuthenticationDetailsProvider.java + // https://github.com/oracle/oci-java-sdk/blob/master/bmc-examples/src/main/java/ObjectStorageSyncExample.java + try { + if (configBean.configPath().isPresent()) { + return new ConfigFileAuthenticationDetailsProvider(configBean.configPath().get(), + configBean.configProfile().orElseThrow()); + } else { + return new ConfigFileAuthenticationDetailsProvider(ConfigFileReader.parseDefault()); + } + } catch (IOException e) { + throw new UncheckedIOException(e.getMessage(), e); + } + }), + + /** + * Corresponds to {@link InstancePrincipalsAuthenticationDetailsProvider}. + */ + INSTANCE_PRINCIPALS(TAG_INSTANCE_PRINCIPALS, + InstancePrincipalsAuthenticationDetailsProvider.class, + OciAvailabilityDefault::runningOnOci, + (configBean) -> InstancePrincipalsAuthenticationDetailsProvider.builder().build()), + + /** + * Corresponds to {@link ResourcePrincipalAuthenticationDetailsProvider}. + */ + RESOURCE_PRINCIPAL(TAG_RESOURCE_PRINCIPALS, + ResourcePrincipalAuthenticationDetailsProvider.class, + (configBean) -> { + // https://github.com/oracle/oci-java-sdk/blob/v2.19.0/bmc-common/src/main/java/com/oracle/bmc/auth/ResourcePrincipalAuthenticationDetailsProvider.java#L246-L251 + return (System.getenv("OCI_RESOURCE_PRINCIPAL_VERSION") != null); + }, + (configBean) -> ResourcePrincipalAuthenticationDetailsProvider.builder().build()); + + private final String id; + private final Class type; + private final Availability availability; + private final Selector selector; + + AuthStrategy(String id, + Class type, + Availability availability, + Selector selector) { + this.id = id; + this.type = type; + this.availability = availability; + this.selector = selector; + } + + String id() { + return id; + } + + Class type() { + return type; + } + + boolean isAvailable(OciConfigBean ociConfigBean) { + return availability.isAvailable(ociConfigBean); + } + + AbstractAuthenticationDetailsProvider select(OciConfigBean ociConfigBean) { + return selector.select(ociConfigBean); + } + + static Optional fromNameOrId(String nameOrId) { + try { + return Optional.of(valueOf(nameOrId)); + } catch (Exception e) { + return Arrays.stream(AuthStrategy.values()) + .filter(it -> nameOrId.equalsIgnoreCase(it.id()) + || nameOrId.equalsIgnoreCase(it.name())) + .findFirst(); + } + } + + static List convert(Collection authStrategies) { + return authStrategies.stream() + .map(AuthStrategy::fromNameOrId) + .map(Optional::orElseThrow) + .toList(); + } + } + + + @FunctionalInterface + interface Availability { + boolean isAvailable(OciConfigBean ociConfigBean); + } + + + @FunctionalInterface + interface Selector { + AbstractAuthenticationDetailsProvider select(OciConfigBean ociConfigBean); + } + +} diff --git a/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciAvailability.java b/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciAvailability.java new file mode 100644 index 00000000000..4c3169e97a4 --- /dev/null +++ b/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciAvailability.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.runtime; + +import io.helidon.pico.api.Contract; + +/** + * Provides a convenient contract for checking whether the current runtime environment is running on/inside an OCI compute node. + * + * @see OciExtension + */ +@Contract +public interface OciAvailability { + + /** + * Returns true if the implementation determines it is running on/inside an OCI compute node. + * + * @param ociConfigBean the oci config bean + * @return true if there running on/inside an OCI compute node + */ + boolean isRunningOnOci(OciConfigBean ociConfigBean); + + /** + * Will source the config bean from {@link OciExtension#ociConfig()} to make the call to {@link #isRunningOnOci(OciConfigBean)}. + * + * @return true if there running on/inside an OCI compute node + */ + default boolean isRunningOnOci() { + return isRunningOnOci(OciExtension.ociConfig()); + } + +} diff --git a/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciAvailabilityDefault.java b/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciAvailabilityDefault.java new file mode 100644 index 00000000000..991c9fd404c --- /dev/null +++ b/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciAvailabilityDefault.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.runtime; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import io.helidon.common.Weight; +import io.helidon.pico.api.ServiceInfoBasics; + +import com.oracle.bmc.Region; +import jakarta.inject.Singleton; + +/** + * This (overridable) implementation will check the {@link OciConfigBean} for {@code IMDS} availability. And if it is found to be + * available, will also perform a secondary check on {@link Region#getRegionFromImds()} to ensure it returns a non-null value. + */ +@Singleton +@Weight(ServiceInfoBasics.DEFAULT_PICO_WEIGHT) +class OciAvailabilityDefault implements OciAvailability { + + @Override + public boolean isRunningOnOci(OciConfigBean ociConfigBean) { + return runningOnOci(ociConfigBean); + } + + static boolean runningOnOci(OciConfigBean ociConfigBean) { + if (!canReach(ociConfigBean.imdsHostName(), ociConfigBean.imdsTimeoutMilliseconds())) { + return false; + } + + return (Region.getRegionFromImds() != null); + } + + static boolean canReach(String address, + int timeoutInMills) { + if (address == null) { + return false; + } + + InetAddress imds; + try { + imds = InetAddress.getByName(address); + } catch (UnknownHostException unknownHostException) { + throw new UncheckedIOException(unknownHostException.getMessage(), unknownHostException); + } + + try { + return imds.isReachable(timeoutInMills); + } catch (ConnectException connectException) { + return false; + } catch (IOException ioException) { + throw new UncheckedIOException(ioException.getMessage(), ioException); + } + } + +} diff --git a/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciConfigBean.java b/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciConfigBean.java new file mode 100644 index 00000000000..8cf0182cfc8 --- /dev/null +++ b/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciConfigBean.java @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.runtime; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +import io.helidon.builder.config.ConfigBean; +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.config.metadata.ConfiguredValue; + +import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; + +import static io.helidon.integrations.oci.sdk.runtime.OciAuthenticationDetailsProvider.ALL_STRATEGIES; +import static io.helidon.integrations.oci.sdk.runtime.OciAuthenticationDetailsProvider.AuthStrategy; +import static io.helidon.integrations.oci.sdk.runtime.OciAuthenticationDetailsProvider.TAG_AUTO; +import static io.helidon.integrations.oci.sdk.runtime.OciAuthenticationDetailsProvider.TAG_CONFIG; +import static io.helidon.integrations.oci.sdk.runtime.OciAuthenticationDetailsProvider.TAG_CONFIG_FILE; +import static io.helidon.integrations.oci.sdk.runtime.OciAuthenticationDetailsProvider.TAG_INSTANCE_PRINCIPALS; +import static io.helidon.integrations.oci.sdk.runtime.OciAuthenticationDetailsProvider.TAG_RESOURCE_PRINCIPALS; + +/** + * Configuration used by {@link OciAuthenticationDetailsProvider}. + * + * @see OciExtension + */ +// note: this is intended to be a replica to the properties carried from the cdi integrations previously done for MP +@ConfigBean(OciConfigBean.NAME) +public interface OciConfigBean { + + /** + * The config is expected to be under this key. + */ + String NAME = "oci"; + + /** primary hostname of metadata service. */ + String IMDS_HOSTNAME = "169.254.169.254"; + /** primary base url of metadata service. */ + String PRIMARY_IMDS_URL = "http://" + IMDS_HOSTNAME + "/opc/v2/"; + + /** fallback base url of metadata service. */ + String FALLBACK_IMDS_URL = "http://" + IMDS_HOSTNAME + "/opc/v1/"; + + /** + * The singular authentication strategy to apply. This will be preferred over {@link #authStrategies()} if both are + * present. + * + * @return the singular authentication strategy to be applied + */ + @ConfiguredOption(allowedValues = { + @ConfiguredValue(value = TAG_AUTO, description = "auto select first applicable"), + @ConfiguredValue(value = TAG_CONFIG, description = "simple authentication provider"), + @ConfiguredValue(value = TAG_CONFIG_FILE, description = "config file authentication provider"), + @ConfiguredValue(value = TAG_INSTANCE_PRINCIPALS, description = "instance principals authentication provider"), + @ConfiguredValue(value = TAG_RESOURCE_PRINCIPALS, description = "resource principals authentication provider"), + }) + Optional authStrategy(); + + /** + * The list of authentication strategies that will be attempted by + * {@link com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider} when one is + * called for. This is only used if {@link #authStrategy()} is not present. + * + *
    + *
  • {@code auto} - if present in the list, or if no value + * for this property exists, the behavior will be as if {@code + * config,config-file,instance-principals,resource-principal} + * were supplied instead.
  • + *
  • {@code config} - the + * {@link com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider} + * will be used, customized with other configuration + * properties described here.
  • + *
  • {@code config-file} - the + * {@link com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider} + * will be used, customized with other configuration + * properties described here.
  • + *
  • {@code instance-principals} - the + * {@link com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider} + * will be used.
  • + *
  • {@code resource-principal} - the + * {@link com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider} + * will be used.
  • + *
+ *

+ * If there are many strategy descriptors supplied, the + * first one that is deemed to be available or suitable will + * be used and all others will be ignored. + * + * @return the list of authentication strategies that will be applied, defaulting to {@code auto} + * @see AuthStrategy + */ + @ConfiguredOption(allowedValues = { + @ConfiguredValue(value = TAG_AUTO, description = "auto select first applicable"), + @ConfiguredValue(value = TAG_CONFIG, description = "simple authentication provider"), + @ConfiguredValue(value = TAG_CONFIG_FILE, description = "config file authentication provider"), + @ConfiguredValue(value = TAG_INSTANCE_PRINCIPALS, description = "instance principals authentication provider"), + @ConfiguredValue(value = TAG_RESOURCE_PRINCIPALS, description = "resource principals authentication provider"), + }) + List authStrategies(); + + /** + * The OCI configuration profile path. + *

+ * This configuration property has an effect only when {@code config-file} is, explicitly or implicitly, + * present in the value for the {@link #authStrategies()}. This is also known as {@link #fileConfigIsPresent()}. + * When it is present, this property must also be present and then the + * {@linkplain com.oracle.bmc.ConfigFileReader#parse(String)} + * method will be passed this value. It is expected to be passed with a + * valid OCI configuration file path. + * + * @return the OCI configuration profile path + */ + @ConfiguredOption(key = "config.path") + Optional configPath(); + + /** + * The OCI configuration/auth profile name. + *

+ * This configuration property has an effect only when {@code config-file} is, explicitly or implicitly, + * present in the value for the {@link #authStrategies()}. This is also known as {@link #fileConfigIsPresent()}. + * When it is present, this property may also be optionally provided in order to override the default {@link + * com.oracle.bmc.ConfigFileReader#DEFAULT_PROFILE_NAME}. + * + * @return the optional OCI configuration/auth profile name + */ + @ConfiguredOption(value = "DEFAULT", key = "config.profile") + Optional configProfile(); + + /** + * The OCI authentication fingerprint. + *

+ * This configuration property has an effect only when {@code config} is, explicitly or implicitly, + * present in the value for the {@link #authStrategies()}. This is also known as {@link #simpleConfigIsPresent()}. + * When it is present, this property must be provided in order to set the API signing key's fingerprint. + * See {@linkplain com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider#getFingerprint()} for more details. + * + * @return the OCI authentication fingerprint + */ + @ConfiguredOption(key = "auth.fingerprint") + Optional authFingerprint(); + + /** + * The OCI authentication key file. + *

+ * This configuration property has an effect only when {@code config} is, explicitly or implicitly, + * present in the value for the {@link #authStrategies()}. This is also known as {@link #simpleConfigIsPresent()}. + * When it is present, this property must be provided in order to set the + * {@linkplain com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider#getPrivateKey()}. This file must exist in the + * {@code user.home} directory. Alternatively, this property can be set using either {@link #authPrivateKey()} or + * using {@link #authPrivateKeyPath()}. + * + * @return the OCI authentication key file + */ + @ConfiguredOption(value = "oci_api_key.pem", key = "auth.keyFile") + Optional authKeyFile(); + + /** + * The OCI authentication key file path. + *

+ * This configuration property has an effect only when {@code config} is, explicitly or implicitly, + * present in the value for the {@link #authStrategies()}. This is also known as {@link #simpleConfigIsPresent()}. + * When it is present, this property must be provided in order to set the + * {@linkplain com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider#getPrivateKey()}. This file path is + * an alternative for using {@link #authKeyFile()} where the file must exist in the {@code user.home} directory. + * Alternatively, this property can be set using {@link #authPrivateKey()}. + * + * @return the OCI authentication key file path + */ + @ConfiguredOption(key = "auth.private-key-path") + Optional authPrivateKeyPath(); + + /** + * The OCI authentication private key. + *

+ * This configuration property has an effect only when {@code config} is, explicitly or implicitly, + * present in the value for the {@link #authStrategies()}. This is also known as {@link #simpleConfigIsPresent()}. + * When it is present, this property must be provided in order to set the + * {@linkplain com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider#getPrivateKey()}. Alternatively, this property + * can be set using either {@link #authKeyFile()} residing in the {@code user.home} directory, or using + * {@link #authPrivateKeyPath()}. + * + * @return the OCI authentication private key + */ + @ConfiguredOption(key = "auth.private-key"/* securitySensitive = true*/) + Optional authPrivateKey(); + + /** + * The OCI authentication passphrase. + *

+ * This configuration property has an effect only when {@code config} is, explicitly or implicitly, + * present in the value for the {@link #authStrategies()}. This is also known as {@link #simpleConfigIsPresent()}. + * When it is present, this property must be provided in order to set the + * {@linkplain com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider#getPassphraseCharacters()}. + * + * @return the OCI authentication passphrase + */ + // See https://github.com/helidon-io/helidon/issues/6908 + @ConfiguredOption(key = "auth.passphrase"/* securitySensitive = true*/) + Optional authPassphrase(); + + /** + * The OCI region. + *

+ * This configuration property has an effect only when {@code config} is, explicitly or implicitly, + * present in the value for the {@link #authStrategies()}. This is also known as {@link #simpleConfigIsPresent()}. + * When it is present, either this property or {@link com.oracle.bmc.auth.RegionProvider} must be provide a value in order + * to set the {@linkplain ConfigFileAuthenticationDetailsProvider#getRegion()}. + * + * @return the OCI region + */ + @ConfiguredOption(key = "auth.region") + Optional authRegion(); + + /** + * The OCI tenant id. + *

+ * This configuration property has an effect only when {@code config} is, explicitly or implicitly, + * present in the value for the {@link #authStrategies()}. This is also known as {@link #simpleConfigIsPresent()}. + * When it is present, this property must be provided in order to set the + * {@linkplain ConfigFileAuthenticationDetailsProvider#getTenantId()}. + * + * @return the OCI tenant id + */ + @ConfiguredOption(key = "auth.tenant-id") + Optional authTenantId(); + + /** + * The OCI user id. + *

+ * This configuration property has an effect only when {@code config} is, explicitly or implicitly, + * present in the value for the {@link #authStrategies()}. + * When it is present, this property must be provided in order to set the + * {@linkplain ConfigFileAuthenticationDetailsProvider#getUserId()}. + * + * @return the OCI user id + */ + @ConfiguredOption(key = "auth.user-id") + Optional authUserId(); + + /** + * The OCI IMDS hostname. + *

+ * This configuration property is used to identify the metadata service url. + * + * @return the OCI IMDS hostname + */ + @ConfiguredOption(value = IMDS_HOSTNAME, key = "imds.hostname") + String imdsHostName(); + + /** + * The OCI IMDS connection timeout in millis. This is used to auto-detect availability. + *

+ * This configuration property is used when attempting to connect to the metadata service. + * + * @return the OCI IMDS connection timeout in millis + * @see OciAvailability + */ + @ConfiguredOption(value = "100", key = "imds.timeout.milliseconds") + int imdsTimeoutMilliseconds(); + + /** + * The list of {@link AuthStrategy} names + * (excluding {@link AuthStrategy#AUTO}) that + * are potentially applicable for use. Here, "potentially applicable or use" means that it is set explicitly by + * {@link #authStrategy()}, or else explicitly or implicitly by {@link #authStrategies()}. + * + * @return the list of potential auth strategies that are applicable + */ + default List potentialAuthStrategies() { + String authStrategy = authStrategy().orElse(null); + if (authStrategy != null + && !TAG_AUTO.equalsIgnoreCase(authStrategy) + && !authStrategy.isBlank()) { + if (!ALL_STRATEGIES.contains(authStrategy)) { + throw new IllegalStateException("Unknown auth strategy: " + authStrategy); + } + + return List.of(authStrategy); + } + + List result = new ArrayList<>(); + authStrategies().stream() + .map(String::trim) + .filter(Predicate.not(String::isBlank)) + .forEach(s -> { + if (!ALL_STRATEGIES.contains(s) && !TAG_AUTO.equals(s)) { + throw new IllegalStateException("Unknown auth strategy: " + s); + } + result.add(s); + }); + if (result.isEmpty() || result.contains(TAG_AUTO)) { + return ALL_STRATEGIES; + } + + return result; + } + + /** + * Determines whether there is sufficient configuration defined in this bean to be used for file-based authentication. This + * matches to the {@link AuthStrategy#CONFIG_FILE}. + * + * @return true if there is sufficient attributes defined for file-based OCI authentication provider applicability + * @see OciAuthenticationDetailsProvider + */ + default boolean fileConfigIsPresent() { + return configPath().isPresent() + && !configPath().orElseThrow().isBlank() + && configProfile().isPresent() + && !configProfile().orElseThrow().isBlank(); + } + + /** + * Determines whether there is sufficient configuration defined in this bean to be used for simple authentication. This + * matches to the {@link AuthStrategy#CONFIG}. + * + * @return true if there is sufficient attributes defined for simple OCI authentication provider applicability + * @see OciAuthenticationDetailsProvider + */ + default boolean simpleConfigIsPresent() { + return authTenantId().isPresent() + && authUserId().isPresent() + && authPassphrase().isPresent() + && authFingerprint().isPresent() + // don't test region since it can alternatively come from the region provider +// && authRegion().isPresent() + && (authPrivateKey().isPresent() + || authPrivateKeyPath().isPresent()); + } + +} diff --git a/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciExtension.java b/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciExtension.java new file mode 100644 index 00000000000..61a8b4ba76f --- /dev/null +++ b/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciExtension.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.runtime; + +import java.util.Arrays; +import java.util.Optional; + +import io.helidon.common.LazyValue; +import io.helidon.common.config.Config; +import io.helidon.pico.api.Bootstrap; +import io.helidon.pico.api.PicoServices; + +import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; + +import static java.util.function.Predicate.not; + +/** + * This class enables configuration access for integration to the Oracle Cloud Infrastructure Java SDK. It is intended for + * non-Helidon MP, non-CDI usage scenarios. For usages that involve + * Helidon MP and CDI please refer to + * {@code io.helidon.integrations.oci.sdk.cdi.OciExtension} instead. This + * integration will follow the same terminology and usage pattern as specified + * for Helidon MP integration. The implementation strategy, however, is + * different between the two. Please take a moment to familiarize yourself to the + * terminology and general approach before continuing further. + *

+ * This module enables the + * {@linkplain jakarta.inject.Inject injection} of any service + * interface, service client, service client + * builder, asynchronous service interface, + * asynchronous service client, or asynchronous service + * client builder from the Oracle Cloud Infrastructure Java SDK. + *

+ * Additionally, this module enables the {@linkplain jakarta.inject.Inject injection} + * of the {@link com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider}, + * which allows the corresponding service client to authenticate with the service. + *

In all cases, user-supplied configuration will be preferred over any + * default configuration. Please refer to {@link #ociConfig()} for details. + * + *

Basic Usage

+ * + * To use this extension, make sure it is on your project's runtime + * classpath. Also be sure the helidon-pico-integrations-oci-processor is + * on your APT/compile-time classpath. To {@linkplain jakarta.inject.Inject inject} a service + * interface named + * com.oracle.bmc.cloudexample.CloudExample + * (or an analogous asynchronous service interface), you will also + * need to ensure that its containing artifact is on your compile + * classpath (e.g. oci-java-sdk-cloudexample-$VERSION.jar, + * where {@code $VERSION} should be replaced by a suitable version + * number). + * + *

Advanced Usage

+ * + *

In the course of providing {@linkplain jakarta.inject.Inject + * injection support} for a service interface or an asynchronous + * service interface, this {@linkplain java.security.cert.Extension extension} will + * create service client builder and asynchronous service client + * builder instances by invoking the {@code static} {@code builder()} + * method that is present on all OCI service client classes, and will then + * provide those instances as regular pico services. The resulting service client or + * asynchronous service client will be built by that builder's {@link + * com.oracle.bmc.common.ClientBuilderBase#build(AbstractAuthenticationDetailsProvider) + * build(AbstractAuthenticationDetailsProvider)} method and will + * itself be provided as a pico service instance.

+ * + *

A user may wish to customize this builder so that the resulting + * service client or asynchronous service client reflects the + * customization. She has two options: + * + *

    + *
  1. She may provide her own instance with the service client builder + * type (or asynchronous client builder type). In this case, the user + * should supply an overriding (i.e., higher weighted) service provider + * implementation than the one provided by {@link OciAuthenticationDetailsProvider}. + * + *
  2. She may customize the service client builder (or asynchronous + * service client builder) supplied by this {@link OciAuthenticationDetailsProvider}. + * To do this, she must supply a custom configuration via {@link #ociConfig()}. + *
+ * + *

Configuration

+ * + * This extension uses the {@link OciConfigBean} for configuration. Refer to it + * for details. + * + * @see Oracle Cloud Infrastructure Java SDK + */ +public final class OciExtension { + static final System.Logger LOGGER = System.getLogger(OciExtension.class.getName()); + static final LazyValue DEFAULT_OCI_CONFIG_BEAN = LazyValue.create(() -> OciConfigBeanDefault.builder() + .authStrategies(Arrays.stream(OciAuthenticationDetailsProvider.AuthStrategy.values()) + .filter(not(it -> it == OciAuthenticationDetailsProvider.AuthStrategy.AUTO)) + .map(OciAuthenticationDetailsProvider.AuthStrategy::id) + .toList()) + .build()); + + private OciExtension() { + } + + /** + * Returns the {@link OciConfigBean} that is currently defined in the bootstrap environment. If one is not defined under + * config {@link OciConfigBean#NAME} then a default implementation will be constructed. + * + * @return the bootstrap oci config bean + */ + public static OciConfigBean ociConfig() { + Optional bootstrap = PicoServices.globalBootstrap(); + if (bootstrap.isEmpty()) { + LOGGER.log(System.Logger.Level.DEBUG, "No bootstrap - using default oci config"); + return DEFAULT_OCI_CONFIG_BEAN.get(); + } + + Config config = bootstrap.get().config().orElse(null); + if (config == null) { + LOGGER.log(System.Logger.Level.DEBUG, "No config in bootstrap - using default oci config"); + return DEFAULT_OCI_CONFIG_BEAN.get(); + } + + config = config.get(OciConfigBean.NAME); + if (!config.exists()) { + LOGGER.log(System.Logger.Level.DEBUG, "No oci config in bootstrap - using default oci config"); + return DEFAULT_OCI_CONFIG_BEAN.get(); + } + + LOGGER.log(System.Logger.Level.DEBUG, "Using specified oci config"); + return OciConfigBeanDefault.toBuilder(config); + } + +} diff --git a/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciRegionProvider.java b/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciRegionProvider.java new file mode 100644 index 00000000000..fea35b7a475 --- /dev/null +++ b/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/OciRegionProvider.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.runtime; + +import java.util.Optional; + +import io.helidon.common.Weight; +import io.helidon.pico.api.ContextualServiceQuery; +import io.helidon.pico.api.ContextualServiceQueryDefault; +import io.helidon.pico.api.InjectionPointProvider; +import io.helidon.pico.api.PicoServices; +import io.helidon.pico.api.ServiceInfoBasics; + +import com.oracle.bmc.Region; +import jakarta.inject.Singleton; + +import static io.helidon.integrations.oci.sdk.runtime.OciAuthenticationDetailsProvider.toNamedProfile; + +/** + * Can optionally be used to return a {@link Region} appropriate for the {@link io.helidon.pico.api.InjectionPointInfo} context. + */ +@Singleton +@Weight(ServiceInfoBasics.DEFAULT_PICO_WEIGHT) +class OciRegionProvider implements InjectionPointProvider { + + OciRegionProvider() { + } + + @Override + public Region get() { + return first(ContextualServiceQueryDefault.builder() + .serviceInfoCriteria(PicoServices.EMPTY_CRITERIA) + .expected(false) + .build()) + .orElseThrow(); + } + + @Override + public Optional first(ContextualServiceQuery query) { + String requestedNamedProfile = toNamedProfile(query.injectionPointInfo().orElse(null)); + Region region = toRegionFromNamedProfile(requestedNamedProfile); + if (region == null) { + region = Region.getRegionFromImds(); + } + return Optional.ofNullable(region); + } + + static Region toRegionFromNamedProfile(String requestedNamedProfile) { + if (requestedNamedProfile == null || requestedNamedProfile.isBlank()) { + return null; + } + + try { + return Region.fromRegionCodeOrId(requestedNamedProfile); + } catch (Exception e) { + // eat it + return null; + } + } + +} diff --git a/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/package-info.java b/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/package-info.java new file mode 100644 index 00000000000..46584e04038 --- /dev/null +++ b/integrations/oci/sdk/runtime/src/main/java/io/helidon/integrations/oci/sdk/runtime/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon Injection Runtime Integrations to support OCI SDK. + */ +package io.helidon.integrations.oci.sdk.runtime; diff --git a/integrations/oci/sdk/runtime/src/main/java/module-info.java b/integrations/oci/sdk/runtime/src/main/java/module-info.java new file mode 100644 index 00000000000..95da41bc969 --- /dev/null +++ b/integrations/oci/sdk/runtime/src/main/java/module-info.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon Injection Integrations to support OCI Runtime module. + */ +module io.helidon.integrations.oci.sdk.runtime { + requires static jakarta.inject; + requires static jakarta.annotation; + + requires io.helidon.builder.config; + requires io.helidon.common; + requires io.helidon.common.config; + requires io.helidon.config.metadata; + requires transitive io.helidon.pico.runtime; + requires oci.java.sdk.common; + requires io.helidon.common.types; + + exports io.helidon.integrations.oci.sdk.runtime; + + uses io.helidon.pico.api.ModuleComponent; + + provides io.helidon.pico.api.ModuleComponent with + io.helidon.integrations.oci.sdk.runtime.Pico$$Module; +} diff --git a/integrations/oci/sdk/runtime/src/test/java/io/helidon/integrations/oci/sdk/runtime/OciAuthenticationDetailsProviderTest.java b/integrations/oci/sdk/runtime/src/test/java/io/helidon/integrations/oci/sdk/runtime/OciAuthenticationDetailsProviderTest.java new file mode 100644 index 00000000000..d8cfede61e3 --- /dev/null +++ b/integrations/oci/sdk/runtime/src/test/java/io/helidon/integrations/oci/sdk/runtime/OciAuthenticationDetailsProviderTest.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.runtime; + +import java.io.UncheckedIOException; +import java.util.Objects; +import java.util.Set; + +import io.helidon.builder.Singular; +import io.helidon.common.types.AnnotationAndValueDefault; +import io.helidon.config.Config; +import io.helidon.pico.api.InjectionPointInfoDefault; +import io.helidon.pico.api.PicoServiceProviderException; +import io.helidon.pico.api.PicoServices; +import io.helidon.pico.api.QualifierAndValueDefault; +import io.helidon.pico.api.ServiceProvider; +import io.helidon.pico.api.Services; + +import com.oracle.bmc.Region; +import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider; +import com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider; +import jakarta.inject.Named; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.pico.testing.PicoTestingSupport.resetAll; +import static io.helidon.pico.testing.PicoTestingSupport.testableServices; +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class OciAuthenticationDetailsProviderTest { + + PicoServices picoServices; + Services services; + + @AfterAll + static void tearDown() { + resetAll(); + } + + void resetWith(Config config) { + resetAll(); + this.picoServices = testableServices(config); + this.services = picoServices.services(); + } + + @Test + void testCanReadPath() { + MatcherAssert.assertThat(OciAuthenticationDetailsProvider.canReadPath("./target"), + is(true)); + MatcherAssert.assertThat(OciAuthenticationDetailsProvider.canReadPath("./~bogus~"), + is(false)); + } + + @Test + void testUserHomePrivateKeyPath() { + OciConfigBean ociConfigBean = Objects.requireNonNull(OciExtension.ociConfig()); + MatcherAssert.assertThat(OciAuthenticationDetailsProvider.userHomePrivateKeyPath(ociConfigBean), + endsWith("/.oci/oci_api_key.pem")); + + ociConfigBean = OciConfigBeanDefault.toBuilder(ociConfigBean) + .configPath("/decoy/path") + .authKeyFile("key.pem") + .build(); + MatcherAssert.assertThat(OciAuthenticationDetailsProvider.userHomePrivateKeyPath(ociConfigBean), + endsWith("/.oci/key.pem")); + } + + @Test + void testToNamedProfile() { + assertThat(OciAuthenticationDetailsProvider.toNamedProfile(null), + nullValue()); + + InjectionPointInfoDefault.Builder ipi = InjectionPointInfoDefault.builder() + .annotations(Set.of()); + assertThat(OciAuthenticationDetailsProvider.toNamedProfile(ipi), + nullValue()); + + ipi.addAnnotation(AnnotationAndValueDefault.create(Singular.class)); + assertThat(OciAuthenticationDetailsProvider.toNamedProfile(ipi), + nullValue()); + + ipi.addAnnotation(AnnotationAndValueDefault.create(Named.class)); + assertThat(OciAuthenticationDetailsProvider.toNamedProfile(ipi), + nullValue()); + + ipi.qualifiers(Set.of(QualifierAndValueDefault.create(Singular.class), + QualifierAndValueDefault.create(Named.class, ""))); + assertThat(OciAuthenticationDetailsProvider.toNamedProfile(ipi), + nullValue()); + + ipi.qualifiers(Set.of(QualifierAndValueDefault.create(Singular.class), + QualifierAndValueDefault.create(Named.class, " profileName "))); + assertThat(OciAuthenticationDetailsProvider.toNamedProfile(ipi), + equalTo("profileName")); + } + + @Test + void authStrategiesAvailability() { + Config config = OciConfigBeanTest.createTestConfig( + OciConfigBeanTest.ociAuthConfigStrategies(OciAuthenticationDetailsProvider.TAG_AUTO), + OciConfigBeanTest.ociAuthSimpleConfig("tenant", "user", "phrase", "fp", null, null, "region")) + .get(OciConfigBean.NAME); + OciConfigBean cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(OciAuthenticationDetailsProvider.AuthStrategy.AUTO.isAvailable(cfg), + is(true)); + assertThat(OciAuthenticationDetailsProvider.AuthStrategy.CONFIG.isAvailable(cfg), + is(false)); + assertThat(OciAuthenticationDetailsProvider.AuthStrategy.CONFIG_FILE.isAvailable(cfg), + is(false)); + assertThat(OciAuthenticationDetailsProvider.AuthStrategy.INSTANCE_PRINCIPALS.isAvailable(cfg), + is(false)); + assertThat(OciAuthenticationDetailsProvider.AuthStrategy.RESOURCE_PRINCIPAL.isAvailable(cfg), + is(false)); + + config = OciConfigBeanTest.createTestConfig( + OciConfigBeanTest.ociAuthConfigStrategies(OciAuthenticationDetailsProvider.TAG_AUTO), + OciConfigBeanTest.ociAuthConfigFile("./target", null), + OciConfigBeanTest.ociAuthSimpleConfig("tenant", "user", "phrase", "fp", "pk", "pkp", null)) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(OciAuthenticationDetailsProvider.AuthStrategy.AUTO.isAvailable(cfg), + is(true)); + assertThat(OciAuthenticationDetailsProvider.AuthStrategy.CONFIG.isAvailable(cfg), + is(true)); + assertThat(OciAuthenticationDetailsProvider.AuthStrategy.CONFIG_FILE.isAvailable(cfg), + is(true)); + assertThat(OciAuthenticationDetailsProvider.AuthStrategy.INSTANCE_PRINCIPALS.isAvailable(cfg), + is(false)); + assertThat(OciAuthenticationDetailsProvider.AuthStrategy.RESOURCE_PRINCIPAL.isAvailable(cfg), + is(false)); + } + + @Test + void selectionWhenNoConfigIsSet() { + Config config = OciConfigBeanTest.createTestConfig( + OciConfigBeanTest.basicTestingConfigSource()); + resetWith(config); + + ServiceProvider authServiceProvider = + services.lookupFirst(AbstractAuthenticationDetailsProvider.class, true).orElseThrow(); + + PicoServiceProviderException e = assertThrows(PicoServiceProviderException.class, () -> authServiceProvider.get()); + assertThat(e.getCause().getMessage(), + equalTo("No instances of com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider available for use. " + + "Verify your configuration named: oci")); + } + + @Test + void selectionWhenFileConfigIsSetWithAuto() { + Config config = OciConfigBeanTest.createTestConfig( + OciConfigBeanTest.basicTestingConfigSource(), + OciConfigBeanTest.ociAuthConfigStrategies(OciAuthenticationDetailsProvider.TAG_AUTO), + OciConfigBeanTest.ociAuthConfigFile("./target", "profile")); + resetWith(config); + + ServiceProvider authServiceProvider = + services.lookupFirst(AbstractAuthenticationDetailsProvider.class, true).orElseThrow(); + + PicoServiceProviderException e = assertThrows(PicoServiceProviderException.class, authServiceProvider::get); + assertThat(e.getCause().getClass(), + equalTo(UncheckedIOException.class)); + } + + @Test + void selectionWhenSimpleConfigIsSetWithAuto() { + Config config = OciConfigBeanTest.createTestConfig( + OciConfigBeanTest.basicTestingConfigSource(), + OciConfigBeanTest.ociAuthConfigStrategies(OciAuthenticationDetailsProvider.TAG_AUTO), + OciConfigBeanTest.ociAuthSimpleConfig("tenant", "user", "passphrase", "fp", "privKey", null, "us-phoenix-1")); + resetWith(config); + + ServiceProvider authServiceProvider = + services.lookupFirst(AbstractAuthenticationDetailsProvider.class, true).orElseThrow(); + + AbstractAuthenticationDetailsProvider authProvider = authServiceProvider.get(); + assertThat(authProvider.getClass(), + equalTo(SimpleAuthenticationDetailsProvider.class)); + SimpleAuthenticationDetailsProvider auth = (SimpleAuthenticationDetailsProvider) authProvider; + assertThat(auth.getTenantId(), + equalTo("tenant")); + assertThat(auth.getUserId(), + equalTo("user")); + assertThat(auth.getRegion(), + equalTo(Region.US_PHOENIX_1)); + assertThat(new String(auth.getPassphraseCharacters()), + equalTo("passphrase")); + } + +} diff --git a/integrations/oci/sdk/runtime/src/test/java/io/helidon/integrations/oci/sdk/runtime/OciAvailabilityTest.java b/integrations/oci/sdk/runtime/src/test/java/io/helidon/integrations/oci/sdk/runtime/OciAvailabilityTest.java new file mode 100644 index 00000000000..198e6a4c692 --- /dev/null +++ b/integrations/oci/sdk/runtime/src/test/java/io/helidon/integrations/oci/sdk/runtime/OciAvailabilityTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.runtime; + +import java.util.Objects; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class OciAvailabilityTest { + + @Test + void isRunningOnOci() { + OciConfigBean ociConfigBean = Objects.requireNonNull(OciExtension.ociConfig()); + assertThat(OciAvailabilityDefault.runningOnOci(ociConfigBean), + is(false)); + } + +} diff --git a/integrations/oci/sdk/runtime/src/test/java/io/helidon/integrations/oci/sdk/runtime/OciConfigBeanTest.java b/integrations/oci/sdk/runtime/src/test/java/io/helidon/integrations/oci/sdk/runtime/OciConfigBeanTest.java new file mode 100644 index 00000000000..7c04e791e8e --- /dev/null +++ b/integrations/oci/sdk/runtime/src/test/java/io/helidon/integrations/oci/sdk/runtime/OciConfigBeanTest.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.runtime; + +import java.util.HashMap; +import java.util.Map; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.config.MapConfigSource; +import io.helidon.pico.api.PicoServicesConfig; + +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class OciConfigBeanTest { + + @Test + void potentialAuthStrategies() { + Config config = createTestConfig(ociAuthConfigStrategies(null)) + .get(OciConfigBean.NAME); + OciConfigBean cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.potentialAuthStrategies(), + contains("config", "config-file", "instance-principals", "resource-principals")); + + config = createTestConfig(ociAuthConfigStrategies("auto")) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.potentialAuthStrategies(), + contains("config", "config-file", "instance-principals", "resource-principals")); + + config = createTestConfig(ociAuthConfigStrategies(null, "instance-principals", "auto")) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.potentialAuthStrategies(), + contains("config", "config-file", "instance-principals", "resource-principals")); + + config = createTestConfig(ociAuthConfigStrategies(null, "instance-principals", "resource-principals")) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.potentialAuthStrategies(), + contains("instance-principals", "resource-principals")); + + config = createTestConfig(ociAuthConfigStrategies("config", "auto")) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.potentialAuthStrategies(), + contains("config")); + + config = createTestConfig(ociAuthConfigStrategies("config", "config", "config-file")) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.potentialAuthStrategies(), + contains("config")); + + config = createTestConfig(ociAuthConfigStrategies("auto", "config", "config-file")) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.potentialAuthStrategies(), + contains("config", "config-file")); + + config = createTestConfig(ociAuthConfigStrategies("", "")) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.potentialAuthStrategies(), + contains("config", "config-file", "instance-principals", "resource-principals")); + } + + @Test + void bogusAuthStrategyAttempted() { + Config config = createTestConfig(ociAuthConfigStrategies("bogus")) + .get(OciConfigBean.NAME); + OciConfigBean cfg = OciConfigBeanDefault.toBuilder(config).build(); + IllegalStateException e = assertThrows(IllegalStateException.class, cfg::potentialAuthStrategies); + assertThat(e.getMessage(), + equalTo("Unknown auth strategy: bogus")); + + config = createTestConfig(ociAuthConfigStrategies(null, "config", "bogus")) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + e = assertThrows(IllegalStateException.class, cfg::potentialAuthStrategies); + assertThat(e.getMessage(), + equalTo("Unknown auth strategy: bogus")); + } + + @Test + void fileConfigIsPresent() { + Config config = createTestConfig(ociAuthConfigFile("path", "profile")) + .get(OciConfigBean.NAME); + OciConfigBean cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.fileConfigIsPresent(), + is(true)); + assertThat(cfg.configProfile().orElseThrow(), + equalTo("profile")); + + config = createTestConfig(ociAuthConfigFile("path", null)) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.fileConfigIsPresent(), is(true)); + assertThat(cfg.configProfile().orElseThrow(), + equalTo("DEFAULT")); + + config = createTestConfig(ociAuthConfigFile("", "")) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.fileConfigIsPresent(), + is(false)); + + config = createTestConfig(ociAuthConfigFile(null, null)) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.fileConfigIsPresent(), + is(false)); + assertThat(cfg.configProfile().orElseThrow(), + equalTo("DEFAULT")); + } + + @Test + void simpleConfigIsPresent() { + Config config = createTestConfig(ociAuthSimpleConfig("tenant", "user", "phrase", "fp", "pk", "pkp", "region")) + .get(OciConfigBean.NAME); + OciConfigBean cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.simpleConfigIsPresent(), + is(true)); + + config = createTestConfig(ociAuthSimpleConfig("tenant", "user", "phrase", "fp", "pk", "pkp", null)) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.simpleConfigIsPresent(), + is(true)); + + config = createTestConfig(ociAuthSimpleConfig(null, "user", "phrase", "fp", "pk", "pkp", null)) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.simpleConfigIsPresent(), + is(false)); + + config = createTestConfig(ociAuthSimpleConfig("tenant", null, "phrase", "fp", "pk", "pkp", null)) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.simpleConfigIsPresent(), + is(false)); + + config = createTestConfig(ociAuthSimpleConfig("tenant", "user", null, "fp", "pk", "pkp", null)) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.simpleConfigIsPresent(), + is(false)); + + config = createTestConfig(ociAuthSimpleConfig("tenant", "user", "phrase", null, "pk", "pkp", null)) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.simpleConfigIsPresent(), + is(false)); + + config = createTestConfig(ociAuthSimpleConfig("tenant", "user", "phrase", "fp", null, "pkp", null)) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.simpleConfigIsPresent(), + is(true)); + + config = createTestConfig(ociAuthSimpleConfig("tenant", "user", "phrase", "fp", "pk", null, "region")) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.simpleConfigIsPresent(), + is(true)); + + config = createTestConfig(ociAuthSimpleConfig("tenant", "user", "phrase", "fp", null, null, "region")) + .get(OciConfigBean.NAME); + cfg = OciConfigBeanDefault.toBuilder(config).build(); + assertThat(cfg.simpleConfigIsPresent(), + is(false)); + } + + @Test + void defaultOciConfigBeanAttributes() { + assertThat(OciExtension.ociConfig().authKeyFile(), + optionalValue(equalTo("oci_api_key.pem"))); + assertThat(OciExtension.ociConfig().imdsHostName(), + equalTo(OciConfigBean.IMDS_HOSTNAME)); + assertThat(OciExtension.ociConfig().imdsTimeoutMilliseconds(), + equalTo(100)); + } + + static Config createTestConfig(MapConfigSource.Builder... builders) { + return Config.builder(builders) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + } + + static MapConfigSource.Builder basicTestingConfigSource() { + return ConfigSources.create( + Map.of( + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_PERMITS_DYNAMIC, "true", + PicoServicesConfig.NAME + "." + PicoServicesConfig.KEY_ACTIVATION_LOGS, "true" + ), "config-basic"); + } + + static MapConfigSource.Builder ociAuthConfigStrategies(String strategy, + String... strategies) { + Map map = new HashMap<>(); + if (strategy != null) { + map.put(OciConfigBean.NAME + ".auth-strategy", strategy); + } + if (strategies != null) { + map.put(OciConfigBean.NAME + ".auth-strategies", String.join(",", strategies)); + } + return ConfigSources.create(map, "config-oci-auth-strategies"); + } + + static MapConfigSource.Builder ociAuthConfigFile(String configPath, + String profile) { + Map map = new HashMap<>(); + if (configPath != null) { + map.put(OciConfigBean.NAME + ".config.path", configPath); + } + if (profile != null) { + map.put(OciConfigBean.NAME + ".config.profile", String.join(",", profile)); + } + return ConfigSources.create(map, "config-oci-auth-config"); + } + + static MapConfigSource.Builder ociAuthSimpleConfig(String tenantId, + String userId, + String passPhrase, + String fingerPrint, + String privateKey, + String privateKeyPath, + String region) { + Map map = new HashMap<>(); + if (tenantId != null) { + map.put(OciConfigBean.NAME + ".auth.tenant-id", tenantId); + } + if (userId != null) { + map.put(OciConfigBean.NAME + ".auth.user-id", userId); + } + if (passPhrase != null) { + map.put(OciConfigBean.NAME + ".auth.passphrase", passPhrase); + } + if (fingerPrint != null) { + map.put(OciConfigBean.NAME + ".auth.fingerprint", fingerPrint); + } + if (privateKey != null) { + map.put(OciConfigBean.NAME + ".auth.private-key", privateKey); + } + if (privateKeyPath != null) { + map.put(OciConfigBean.NAME + ".auth.private-key-path", privateKeyPath); + } + if (region != null) { + map.put(OciConfigBean.NAME + ".auth.region", region); + } + return ConfigSources.create(map, "config-oci-auth-simple"); + } + +} diff --git a/integrations/oci/sdk/runtime/src/test/java/io/helidon/integrations/oci/sdk/runtime/OciExtensionTest.java b/integrations/oci/sdk/runtime/src/test/java/io/helidon/integrations/oci/sdk/runtime/OciExtensionTest.java new file mode 100644 index 00000000000..2daeb9cfbb9 --- /dev/null +++ b/integrations/oci/sdk/runtime/src/test/java/io/helidon/integrations/oci/sdk/runtime/OciExtensionTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.runtime; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.sameInstance; + +class OciExtensionTest { + + @Test + void ociConfig() { + assertThat(OciExtension.ociConfig(), notNullValue()); + assertThat(OciExtension.ociConfig(), sameInstance(OciExtension.ociConfig())); + } + +} diff --git a/integrations/oci/sdk/runtime/src/test/java/io/helidon/integrations/oci/sdk/runtime/OciRegionProviderTest.java b/integrations/oci/sdk/runtime/src/test/java/io/helidon/integrations/oci/sdk/runtime/OciRegionProviderTest.java new file mode 100644 index 00000000000..65d3ce36218 --- /dev/null +++ b/integrations/oci/sdk/runtime/src/test/java/io/helidon/integrations/oci/sdk/runtime/OciRegionProviderTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.runtime; + +import io.helidon.config.Config; +import io.helidon.pico.api.ContextualServiceQuery; +import io.helidon.pico.api.ElementInfo; +import io.helidon.pico.api.InjectionPointInfoDefault; +import io.helidon.pico.api.PicoServiceProviderException; +import io.helidon.pico.api.PicoServices; +import io.helidon.pico.api.QualifierAndValueDefault; +import io.helidon.pico.api.ServiceInfoCriteriaDefault; +import io.helidon.pico.api.ServiceProvider; +import io.helidon.pico.api.Services; + +import com.oracle.bmc.Region; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static io.helidon.pico.testing.PicoTestingSupport.resetAll; +import static io.helidon.pico.testing.PicoTestingSupport.testableServices; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class OciRegionProviderTest { + PicoServices picoServices; + Services services; + + @AfterAll + static void tearDown() { + resetAll(); + } + + void resetWith(Config config) { + resetAll(); + this.picoServices = testableServices(config); + this.services = picoServices.services(); + } + + @Test + void regionProviderService() { + Config config = OciConfigBeanTest.createTestConfig( + OciConfigBeanTest.basicTestingConfigSource(), + OciConfigBeanTest.ociAuthConfigStrategies(OciAuthenticationDetailsProvider.TAG_AUTO), + OciConfigBeanTest.ociAuthSimpleConfig("tenant", "user", "phrase", "fp", null, null, "region")); + resetWith(config); + + ServiceProvider regionProvider = PicoServices.realizedServices() + .lookupFirst(Region.class, false).orElseThrow(); + assertThrows(PicoServiceProviderException.class, + regionProvider::get); + + ContextualServiceQuery query = ContextualServiceQuery.create( + InjectionPointInfoDefault.builder() + .serviceTypeName("whatever") + .elementKind(ElementInfo.ElementKind.METHOD) + .elementName("m") + .elementTypeName(Region.class.getName()) + .baseIdentity("m") + .id("m1") + .access(ElementInfo.Access.PUBLIC) + .addQualifier(QualifierAndValueDefault.createNamed("us-phoenix-1")) + .dependencyToServiceInfo(ServiceInfoCriteriaDefault.builder() + .addContractImplemented(Region.class.getName()) + .addQualifier(QualifierAndValueDefault.createNamed("us-phoenix-1")) + .build()) + .build(), + false); + assertThat(regionProvider.first(query), + optionalValue(equalTo(Region.US_PHOENIX_1))); + } + +} diff --git a/integrations/oci/sdk/tests/README.md b/integrations/oci/sdk/tests/README.md new file mode 100644 index 00000000000..afd32515e71 --- /dev/null +++ b/integrations/oci/sdk/tests/README.md @@ -0,0 +1,3 @@ +# helidon-integrations-oci-sdk-tests + +Refer to the [helidon-integrations-oci-sdk](../) documentation. diff --git a/integrations/oci/sdk/tests/pom.xml b/integrations/oci/sdk/tests/pom.xml new file mode 100644 index 00000000000..d7267496bb6 --- /dev/null +++ b/integrations/oci/sdk/tests/pom.xml @@ -0,0 +1,55 @@ + + + + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + io.helidon.integrations.oci.sdk.tests + helidon-integrations-oci-sdk-tests-project + Helidon Integrations OCI Injection Tests + + Helidon Injection Framework Testing for the OCI SDK + + pom + + + true + true + true + true + true + true + true + + + + test-module1 + test-module2 + test-application + + + diff --git a/integrations/oci/sdk/tests/test-application/pom.xml b/integrations/oci/sdk/tests/test-application/pom.xml new file mode 100644 index 00000000000..df3f4d710ac --- /dev/null +++ b/integrations/oci/sdk/tests/test-application/pom.xml @@ -0,0 +1,144 @@ + + + + + + io.helidon.integrations.oci.sdk.tests + helidon-integrations-oci-sdk-tests-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-integrations-oci-sdk-tests-test-application + Helidon Integrations OCI Test Application + + + + io.helidon.integrations.oci.sdk.tests + helidon-integrations-oci-sdk-tests-test-module1 + ${helidon.version} + + + io.helidon.integrations.oci.sdk.tests + helidon-integrations-oci-sdk-tests-test-module2 + ${helidon.version} + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-runtime + + + com.oracle.oci.sdk + oci-java-sdk-common + + + jakarta.annotation + jakarta.annotation-api + provided + + + jakarta.inject + jakarta.inject-api + provided + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + com.oracle.oci.sdk + oci-java-sdk-ailanguage + + + com.oracle.oci.sdk + oci-java-sdk-objectstorage + + + + + com.oracle.oci.sdk + oci-java-sdk-streaming + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-processor + ${helidon.version} + + + + + + io.helidon.pico + helidon-pico-maven-plugin + ${helidon.version} + + + compile + compile + + application-create + + + + testCompile + test-compile + + test-application-create + + + + + + + + + + + + + + + + diff --git a/integrations/oci/sdk/tests/test-application/src/test/java/io/helidon/integrations/oci/sdk/tests/test/application/TestOciServices.java b/integrations/oci/sdk/tests/test-application/src/test/java/io/helidon/integrations/oci/sdk/tests/test/application/TestOciServices.java new file mode 100644 index 00000000000..a738365c272 --- /dev/null +++ b/integrations/oci/sdk/tests/test-application/src/test/java/io/helidon/integrations/oci/sdk/tests/test/application/TestOciServices.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.tests.test.application; + +import java.util.Optional; + +import com.oracle.bmc.ailanguage.AIServiceLanguage; +import com.oracle.bmc.ailanguage.AIServiceLanguageAsync; +import com.oracle.bmc.ailanguage.AIServiceLanguageAsyncClient; +import com.oracle.bmc.ailanguage.AIServiceLanguageClient; +import com.oracle.bmc.circuitbreaker.OciCircuitBreaker; +import com.oracle.bmc.objectstorage.ObjectStorage; +import com.oracle.bmc.objectstorage.ObjectStorageAsync; +import com.oracle.bmc.objectstorage.ObjectStorageAsyncClient; +import com.oracle.bmc.objectstorage.ObjectStorageClient; +import com.oracle.bmc.streaming.Stream; +import com.oracle.bmc.streaming.StreamAdmin; +import com.oracle.bmc.streaming.StreamAdminClient; +import com.oracle.bmc.streaming.StreamAsync; +import com.oracle.bmc.streaming.StreamAsyncClient; +import com.oracle.bmc.streaming.StreamAsyncClientBuilder; +import com.oracle.bmc.streaming.StreamClient; +import com.oracle.bmc.streaming.StreamClientBuilder; +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalPresent; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * See {@code OciExtension} and {@code TestSpike} for a backgrounder. + */ +@Singleton +@SuppressWarnings("unused") +class TestOciServices { + int postConstructCalls; + + // The service* parameters below are + // arbitrary; pick another OCI service and + // substitute the classes as appropriate + // and this will still work (assuming of + // course you also add the proper jar file + // to this project's test-scoped + // dependencies). + @Inject Optional serviceInterface; + @Inject Optional serviceClient; + @Inject Optional serviceClientBuilder; + @Inject Optional serviceAsyncInterface; + @Inject Optional serviceAsyncClient; + @Inject Optional serviceAsyncClientBuilder; + + @Inject ObjectStorage objectStorage; + @Inject ObjectStorageClient objectStorageClient; + @Inject ObjectStorageClient.Builder objectStorageClientBuilder; + @Inject ObjectStorageAsync objectStorageAsync; + @Inject ObjectStorageAsyncClient objectStorageAsyncClient; + @Inject ObjectStorageAsyncClient.Builder objectStorageAsyncClientBuilder; + + // This one is an example of something + // that looks like a service client but + // isn't; see the comments in the + // constructor body below. It should be + // unsatisfied. + @Inject Optional unresolvedJaxRsCircuitBreakerInstance; + + // Streaming turns out to be the only + // convention-violating service in the + // entire portfolio, and the violation is + // extremely minor, and appears to be a + // mistake. Specifically, its root + // subpackage features two main domain + // objects (Stream, StreamAdmin) and only + // one of them (StreamAdmin) fully follows + // the service client pattern. The other + // one (Stream) features a builder class + // that is not a nested class + // (StreamClientBuilder) but maybe should + // be. We test this explicitly here + // because, again, it is the only service + // in the entire portfolio that breaks the + // pattern. + @Inject Stream streamingServiceInterface; + @Inject StreamAdmin streamingAdminServiceInterface; + @Inject StreamAdminClient streamingAdminServiceClient; + @Inject StreamAdminClient.Builder streamingAdminServiceClientBuilder; + @Inject StreamAsync streamingServiceAsyncInterface; + @Inject StreamAsyncClient streamingServiceAsyncClient; + @Inject StreamAsyncClientBuilder streamingServiceAsyncClientBuilder; + @Inject StreamClient streamingServiceClient; // oddball + @Inject StreamClientBuilder streamingServiceClientBuilder; // oddball + + @PostConstruct + void verifyEverything() { + postConstructCalls++; + + assertThat(serviceInterface, optionalPresent()); + assertThat(serviceClient, optionalPresent()); + assertThat(serviceClientBuilder, optionalPresent()); + assertThat(serviceAsyncInterface, optionalPresent()); + assertThat(serviceAsyncClient, optionalPresent()); + assertThat(serviceAsyncClientBuilder, optionalPresent()); + + assertThat(objectStorage, notNullValue()); + assertThat(objectStorageClient, notNullValue()); + assertThat(objectStorageClientBuilder, notNullValue()); + assertThat(objectStorageAsync, notNullValue()); + assertThat(objectStorageAsyncClient, notNullValue()); + assertThat(objectStorageAsyncClientBuilder, notNullValue()); + + assertThat(unresolvedJaxRsCircuitBreakerInstance, optionalEmpty()); + + assertThat(streamingServiceInterface, notNullValue()); + assertThat(streamingAdminServiceInterface, notNullValue()); + assertThat(streamingAdminServiceClient, notNullValue()); + assertThat(streamingAdminServiceClientBuilder, notNullValue()); + assertThat(streamingServiceAsyncInterface, notNullValue()); + assertThat(streamingServiceAsyncClient, notNullValue()); + assertThat(streamingServiceAsyncClientBuilder, notNullValue()); + assertThat(streamingServiceClient, notNullValue()); + assertThat(streamingServiceClientBuilder, notNullValue()); + } + + void assertCalledOnce() { + assertThat(postConstructCalls, is(1)); + } + +} diff --git a/integrations/oci/sdk/tests/test-module1/pom.xml b/integrations/oci/sdk/tests/test-module1/pom.xml new file mode 100644 index 00000000000..9e7e7239917 --- /dev/null +++ b/integrations/oci/sdk/tests/test-module1/pom.xml @@ -0,0 +1,110 @@ + + + + + + io.helidon.integrations.oci.sdk.tests + helidon-integrations-oci-sdk-tests-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-integrations-oci-sdk-tests-test-module1 + Helidon Integrations OCI Test Module 1 + + + + com.oracle.oci.sdk + oci-java-sdk-common + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-runtime + + + io.helidon.pico + helidon-pico-runtime + + + jakarta.annotation + jakarta.annotation-api + provided + + + jakarta.inject + jakarta.inject-api + provided + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + com.oracle.oci.sdk + oci-java-sdk-ailanguage + + + com.oracle.oci.sdk + oci-java-sdk-objectstorage + + + + + com.oracle.oci.sdk + oci-java-sdk-streaming + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-processor + ${helidon.version} + + + + + + + + diff --git a/integrations/oci/sdk/tests/test-module1/src/main/java/io/helidon/integrations/oci/sdk/tests/test/module1/AServiceUsingObjectStorage.java b/integrations/oci/sdk/tests/test-module1/src/main/java/io/helidon/integrations/oci/sdk/tests/test/module1/AServiceUsingObjectStorage.java new file mode 100644 index 00000000000..7180393994d --- /dev/null +++ b/integrations/oci/sdk/tests/test-module1/src/main/java/io/helidon/integrations/oci/sdk/tests/test/module1/AServiceUsingObjectStorage.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.tests.test.module1; + +import java.util.Objects; + +import com.oracle.bmc.objectstorage.ObjectStorage; +import com.oracle.bmc.objectstorage.requests.GetNamespaceRequest; +import com.oracle.bmc.objectstorage.responses.GetNamespaceResponse; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +@Singleton +class AServiceUsingObjectStorage { + + private final ObjectStorage objStorageClient; + private final Provider standbyObjStorageClientProvider; + + @Inject + AServiceUsingObjectStorage(ObjectStorage objStorage, + @Named("StandbyProfile") Provider standbyObjStorageProvider) { + this.objStorageClient = Objects.requireNonNull(objStorage); + this.standbyObjStorageClientProvider = Objects.requireNonNull(standbyObjStorageProvider); + } + + String namespaceName() { + GetNamespaceResponse namespaceResponse = objStorageClient + .getNamespace(GetNamespaceRequest.builder().build()); + return namespaceResponse.getValue(); + } + + String namespaceNameOfStandby() { + GetNamespaceResponse namespaceResponse = standbyObjStorageClientProvider.get() + .getNamespace(GetNamespaceRequest.builder().build()); + return namespaceResponse.getValue(); + } + +} diff --git a/examples/integrations/neo4j/neo4j-mp/src/test/java/io/helidon/examples/integrations/neo4j/mp/package-info.java b/integrations/oci/sdk/tests/test-module1/src/main/java/io/helidon/integrations/oci/sdk/tests/test/module1/package-info.java similarity index 81% rename from examples/integrations/neo4j/neo4j-mp/src/test/java/io/helidon/examples/integrations/neo4j/mp/package-info.java rename to integrations/oci/sdk/tests/test-module1/src/main/java/io/helidon/integrations/oci/sdk/tests/test/module1/package-info.java index acbfc3cde79..79970f57279 100644 --- a/examples/integrations/neo4j/neo4j-mp/src/test/java/io/helidon/examples/integrations/neo4j/mp/package-info.java +++ b/integrations/oci/sdk/tests/test-module1/src/main/java/io/helidon/integrations/oci/sdk/tests/test/module1/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,6 @@ */ /** - * Tests for MP Neo4j app. + * Test module. */ -package io.helidon.examples.integrations.neo4j.mp; \ No newline at end of file +package io.helidon.integrations.oci.sdk.tests.test.module1; diff --git a/integrations/oci/sdk/tests/test-module2/pom.xml b/integrations/oci/sdk/tests/test-module2/pom.xml new file mode 100644 index 00000000000..44eb1864358 --- /dev/null +++ b/integrations/oci/sdk/tests/test-module2/pom.xml @@ -0,0 +1,110 @@ + + + + + + io.helidon.integrations.oci.sdk.tests + helidon-integrations-oci-sdk-tests-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-integrations-oci-sdk-tests-test-module2 + Helidon Integrations OCI Test Module 2 + + + + com.oracle.oci.sdk + oci-java-sdk-common + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-runtime + + + io.helidon.pico + helidon-pico-runtime + + + jakarta.annotation + jakarta.annotation-api + provided + + + jakarta.inject + jakarta.inject-api + provided + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + com.oracle.oci.sdk + oci-java-sdk-ailanguage + + + com.oracle.oci.sdk + oci-java-sdk-objectstorage + + + + + com.oracle.oci.sdk + oci-java-sdk-streaming + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + io.helidon.integrations.oci.sdk + helidon-integrations-oci-sdk-processor + ${helidon.version} + + + + + + + + diff --git a/integrations/oci/sdk/tests/test-module2/src/main/java/io/helidon/integrations/oci/sdk/tests/test/module2/AnotherServiceUsingObjectStorage.java b/integrations/oci/sdk/tests/test-module2/src/main/java/io/helidon/integrations/oci/sdk/tests/test/module2/AnotherServiceUsingObjectStorage.java new file mode 100644 index 00000000000..8c6b5eeac24 --- /dev/null +++ b/integrations/oci/sdk/tests/test-module2/src/main/java/io/helidon/integrations/oci/sdk/tests/test/module2/AnotherServiceUsingObjectStorage.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.integrations.oci.sdk.tests.test.module2; + +import java.util.Objects; + +import com.oracle.bmc.objectstorage.ObjectStorage; +import com.oracle.bmc.objectstorage.requests.GetNamespaceRequest; +import com.oracle.bmc.objectstorage.responses.GetNamespaceResponse; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +@Singleton +class AnotherServiceUsingObjectStorage { + + @Inject + ObjectStorage objStorageClient; + Provider standbyObjStorageClientProvider; + + @Inject + void setStandbyObjectStorageProvider(@Named("StandbyProfile") Provider standbyObjStorageClientProvider) { + this.standbyObjStorageClientProvider = Objects.requireNonNull(standbyObjStorageClientProvider); + } + + String namespaceName() { + GetNamespaceResponse namespaceResponse = objStorageClient + .getNamespace(GetNamespaceRequest.builder().build()); + return namespaceResponse.getValue(); + } + + String namespaceNameOfStandby() { + GetNamespaceResponse namespaceResponse = standbyObjStorageClientProvider.get() + .getNamespace(GetNamespaceRequest.builder().build()); + return namespaceResponse.getValue(); + } + +} diff --git a/integrations/oci/sdk/tests/test-module2/src/main/java/io/helidon/integrations/oci/sdk/tests/test/module2/package-info.java b/integrations/oci/sdk/tests/test-module2/src/main/java/io/helidon/integrations/oci/sdk/tests/test/module2/package-info.java new file mode 100644 index 00000000000..0f7706fc22d --- /dev/null +++ b/integrations/oci/sdk/tests/test-module2/src/main/java/io/helidon/integrations/oci/sdk/tests/test/module2/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Test module. + */ +package io.helidon.integrations.oci.sdk.tests.test.module2; diff --git a/integrations/vault/secrets/cubbyhole/src/main/java/io/helidon/integrations/vault/secrets/cubbyhole/CubbyholeSecurityProvider.java b/integrations/vault/secrets/cubbyhole/src/main/java/io/helidon/integrations/vault/secrets/cubbyhole/CubbyholeSecurityProvider.java index fa128bc2271..530ef012f21 100644 --- a/integrations/vault/secrets/cubbyhole/src/main/java/io/helidon/integrations/vault/secrets/cubbyhole/CubbyholeSecurityProvider.java +++ b/integrations/vault/secrets/cubbyhole/src/main/java/io/helidon/integrations/vault/secrets/cubbyhole/CubbyholeSecurityProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,11 @@ package io.helidon.integrations.vault.secrets.cubbyhole; +import java.time.Duration; import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; -import io.helidon.common.reactive.Single; import io.helidon.config.Config; import io.helidon.integrations.vault.Vault; import io.helidon.integrations.vault.VaultOptionalResponse; @@ -38,17 +38,18 @@ public class CubbyholeSecurityProvider implements SecretsProvider>> secret(Config config) { + public Supplier> secret(Config config) { return secret(CubbyholeSecretConfig.create(config)); } @Override - public Supplier>> secret(CubbyholeSecretConfig providerConfig) { + public Supplier> secret(CubbyholeSecretConfig providerConfig) { String key = providerConfig.key; return () -> secrets.get(providerConfig.request()) .map(VaultOptionalResponse::entity) - .map(it -> it.flatMap(response -> response.value(key))); + .map(it -> it.flatMap(response -> response.value(key))) + .await(Duration.ofSeconds(10)); } /** diff --git a/integrations/vault/secrets/kv1/src/main/java/io/helidon/integrations/vault/secrets/kv1/Kv1SecurityProvider.java b/integrations/vault/secrets/kv1/src/main/java/io/helidon/integrations/vault/secrets/kv1/Kv1SecurityProvider.java index 377d003ca41..86afb6c3098 100644 --- a/integrations/vault/secrets/kv1/src/main/java/io/helidon/integrations/vault/secrets/kv1/Kv1SecurityProvider.java +++ b/integrations/vault/secrets/kv1/src/main/java/io/helidon/integrations/vault/secrets/kv1/Kv1SecurityProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,11 @@ package io.helidon.integrations.vault.secrets.kv1; +import java.time.Duration; import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; -import io.helidon.common.reactive.Single; import io.helidon.config.Config; import io.helidon.integrations.vault.Vault; import io.helidon.integrations.vault.VaultOptionalResponse; @@ -38,17 +38,18 @@ public class Kv1SecurityProvider implements SecretsProvider>> secret(Config config) { + public Supplier> secret(Config config) { return secret(Kv1SecretConfig.create(config)); } @Override - public Supplier>> secret(Kv1SecretConfig providerConfig) { + public Supplier> secret(Kv1SecretConfig providerConfig) { String key = providerConfig.key; return () -> secrets.get(providerConfig.request()) .map(VaultOptionalResponse::entity) - .map(it -> it.flatMap(response -> response.value(key))); + .map(it -> it.flatMap(response -> response.value(key))) + .await(Duration.ofSeconds(10)); } /** diff --git a/integrations/vault/secrets/kv2/src/main/java/io/helidon/integrations/vault/secrets/kv2/Kv2SecurityProvider.java b/integrations/vault/secrets/kv2/src/main/java/io/helidon/integrations/vault/secrets/kv2/Kv2SecurityProvider.java index c30f79b89ef..48c9e580b20 100644 --- a/integrations/vault/secrets/kv2/src/main/java/io/helidon/integrations/vault/secrets/kv2/Kv2SecurityProvider.java +++ b/integrations/vault/secrets/kv2/src/main/java/io/helidon/integrations/vault/secrets/kv2/Kv2SecurityProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,11 @@ package io.helidon.integrations.vault.secrets.kv2; +import java.time.Duration; import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; -import io.helidon.common.reactive.Single; import io.helidon.config.Config; import io.helidon.integrations.vault.Vault; import io.helidon.integrations.vault.VaultOptionalResponse; @@ -38,17 +38,18 @@ public class Kv2SecurityProvider implements SecretsProvider>> secret(Config config) { + public Supplier> secret(Config config) { return secret(Kv2SecretConfig.create(config)); } @Override - public Supplier>> secret(Kv2SecretConfig providerConfig) { + public Supplier> secret(Kv2SecretConfig providerConfig) { String key = providerConfig.key; return () -> secrets.get(providerConfig.request()) .map(VaultOptionalResponse::entity) - .map(it -> it.flatMap(response -> response.value(key))); + .map(it -> it.flatMap(response -> response.value(key))) + .await(Duration.ofSeconds(10)); } /** diff --git a/integrations/vault/secrets/transit/src/main/java/io/helidon/integrations/vault/secrets/transit/TransitSecurityProvider.java b/integrations/vault/secrets/transit/src/main/java/io/helidon/integrations/vault/secrets/transit/TransitSecurityProvider.java index 4b87804bacd..f73df99a2ab 100644 --- a/integrations/vault/secrets/transit/src/main/java/io/helidon/integrations/vault/secrets/transit/TransitSecurityProvider.java +++ b/integrations/vault/secrets/transit/src/main/java/io/helidon/integrations/vault/secrets/transit/TransitSecurityProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,12 @@ package io.helidon.integrations.vault.secrets.transit; +import java.time.Duration; import java.util.Objects; import java.util.Optional; import java.util.function.Function; import io.helidon.common.Base64Value; -import io.helidon.common.reactive.Single; import io.helidon.config.Config; import io.helidon.integrations.vault.Vault; import io.helidon.security.SecurityException; @@ -47,15 +47,17 @@ public EncryptionSupport encryption(Config config) { @Override public EncryptionSupport encryption(TransitEncryptionConfig providerConfig) { - Function> encrypt = bytes -> transit.encrypt(providerConfig.encryptionRequest() + Function encrypt = bytes -> transit.encrypt(providerConfig.encryptionRequest() .data(Base64Value.create(bytes))) .map(Encrypt.Response::encrypted) - .map(Encrypt.Encrypted::cipherText); + .map(Encrypt.Encrypted::cipherText) + .await(Duration.ofSeconds(10)); - Function> decrypt = encrypted -> transit.decrypt(providerConfig.decryptionRequest() + Function decrypt = encrypted -> transit.decrypt(providerConfig.decryptionRequest() .cipherText(encrypted)) .map(Decrypt.Response::decrypted) - .map(Base64Value::toBytes); + .map(Base64Value::toBytes) + .await(Duration.ofSeconds(10)); return EncryptionSupport.create(encrypt, decrypt); } @@ -81,7 +83,8 @@ private DigestSupport signature(TransitDigestConfig providerConfig) { .preHashed(preHashed); return transit.sign(request) - .map(Sign.Response::signature); + .map(Sign.Response::signature) + .await(Duration.ofSeconds(10)); }; VerifyFunction verifyFunction = (data, preHashed, digest) -> { @@ -91,7 +94,8 @@ private DigestSupport signature(TransitDigestConfig providerConfig) { .signature(digest); return transit.verify(verifyRequest) - .map(Verify.Response::isValid); + .map(Verify.Response::isValid) + .await(Duration.ofSeconds(10)); }; return DigestSupport.create(digestFunction, verifyFunction); @@ -103,7 +107,8 @@ private DigestSupport hmac(TransitDigestConfig providerConfig) { .data(Base64Value.create(data)); return transit.hmac(request) - .map(Hmac.Response::hmac); + .map(Hmac.Response::hmac) + .await(Duration.ofSeconds(10)); }; VerifyFunction verifyFunction = (data, preHashed, digest) -> { @@ -113,7 +118,8 @@ private DigestSupport hmac(TransitDigestConfig providerConfig) { .hmac(digest); return transit.verify(verifyRequest) - .map(Verify.Response::isValid); + .map(Verify.Response::isValid) + .await(Duration.ofSeconds(10)); }; return DigestSupport.create(digestFunction, verifyFunction); diff --git a/metrics/prometheus/pom.xml b/metrics/prometheus/pom.xml index a61cde630ad..0c68e98bd9b 100644 --- a/metrics/prometheus/pom.xml +++ b/metrics/prometheus/pom.xml @@ -31,8 +31,8 @@ - io.helidon.reactive.webserver - helidon-reactive-webserver + io.helidon.nima.service-common + helidon-nima-service-common io.prometheus @@ -49,13 +49,8 @@ test - org.mockito - mockito-core - test - - - io.helidon.reactive.webserver - helidon-reactive-webserver-test-support + io.helidon.nima.testing.junit5 + helidon-nima-testing-junit5-webserver test diff --git a/metrics/prometheus/src/main/java/io/helidon/metrics/prometheus/PrometheusSupport.java b/metrics/prometheus/src/main/java/io/helidon/metrics/prometheus/PrometheusSupport.java index b3529a5294b..21555ac4647 100644 --- a/metrics/prometheus/src/main/java/io/helidon/metrics/prometheus/PrometheusSupport.java +++ b/metrics/prometheus/src/main/java/io/helidon/metrics/prometheus/PrometheusSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2022 Oracle and/or its affiliates. + * Copyright (c) 2017, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,13 +19,16 @@ import java.util.Enumeration; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import io.helidon.common.http.HttpMediaType; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.ServerRequest; -import io.helidon.reactive.webserver.ServerResponse; -import io.helidon.reactive.webserver.Service; +import io.helidon.nima.servicecommon.HelidonFeatureSupport; +import io.helidon.nima.webserver.http.HttpRouting; +import io.helidon.nima.webserver.http.HttpRules; +import io.helidon.nima.webserver.http.HttpService; +import io.helidon.nima.webserver.http.ServerRequest; +import io.helidon.nima.webserver.http.ServerResponse; import io.prometheus.client.Collector; import io.prometheus.client.CollectorRegistry; @@ -33,16 +36,16 @@ /** * Support for Prometheus client endpoint. *

- * Default and simplest use on {@link Routing} creates {@code /metrics} endpoint + * Default and simplest use on {@link HttpRouting} creates {@code /metrics} endpoint * for {@link CollectorRegistry default CollectorRegistry}. *

{@code
- * Routing.builder()
- *        .register(PrometheusSupport.create())
+ * HttpRouting.builder()
+ *        ..addFeature(PrometheusSupport.create())
  * }
- *

- * It is possible to use */ -public final class PrometheusSupport implements Service { +public final class PrometheusSupport extends HelidonFeatureSupport { + + private static final System.Logger LOGGER = System.getLogger(PrometheusSupport.class.getName()); /** * Standard path of Prometheus client resource: {@code /metrics}. @@ -54,18 +57,23 @@ public final class PrometheusSupport implements Service { private final CollectorRegistry collectorRegistry; private final String path; - private PrometheusSupport(CollectorRegistry collectorRegistry, String path) { - this.collectorRegistry = collectorRegistry == null ? CollectorRegistry.defaultRegistry : collectorRegistry; - this.path = path == null ? DEFAULT_PATH : path; + private PrometheusSupport(Builder builder) { + super(LOGGER, builder, "prometheus"); + this.collectorRegistry = builder.registry; + this.path = builder.path; } - @Override - public void update(Routing.Rules rules) { + private void configureRoutes(HttpRules rules) { rules.get(path, this::process); } + @Override + public Optional service() { + return Optional.of(this::configureRoutes); + } + private void process(ServerRequest req, ServerResponse res) { - Set filters = new HashSet<>(req.queryParams().all("name[]", List::of)); + Set filters = new HashSet<>(req.query().all("name[]", List::of)); Enumeration mfs = collectorRegistry.filteredMetricFamilySamples(filters); res.headers().contentType(CONTENT_TYPE); res.send(compose(mfs)); @@ -171,7 +179,7 @@ private static String typeString(Collector.Type t) { * @see #builder() */ public static PrometheusSupport create(CollectorRegistry collectorRegistry) { - return new PrometheusSupport(collectorRegistry, DEFAULT_PATH); + return builder().collectorRegistry(collectorRegistry).build(); } /** @@ -183,7 +191,7 @@ public static PrometheusSupport create(CollectorRegistry collectorRegistry) { * @see #builder() */ public static PrometheusSupport create() { - return create(null); + return builder().build(); } /** @@ -200,12 +208,13 @@ public static Builder builder() { /** * A builder of {@link PrometheusSupport}. */ - public static final class Builder implements io.helidon.common.Builder { + public static final class Builder extends HelidonFeatureSupport.Builder { private CollectorRegistry registry = CollectorRegistry.defaultRegistry; - private String path; + private String path = DEFAULT_PATH; private Builder() { + super("/"); } /** @@ -236,7 +245,7 @@ public Builder path(String path) { @Override public PrometheusSupport build() { - return new PrometheusSupport(registry, path == null ? DEFAULT_PATH : path); + return new PrometheusSupport(this); } } } diff --git a/metrics/prometheus/src/main/java/module-info.java b/metrics/prometheus/src/main/java/module-info.java index 0af07eaaf8e..b764aea60a3 100644 --- a/metrics/prometheus/src/main/java/module-info.java +++ b/metrics/prometheus/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,7 @@ * Prometheus support. */ module io.helidon.metrics.prometheus { - requires io.helidon.reactive.webserver; - + requires io.helidon.nima.servicecommon; // prometheus :( requires simpleclient; diff --git a/metrics/prometheus/src/test/java/io/helidon/metrics/prometheus/PrometheusSupportTest.java b/metrics/prometheus/src/test/java/io/helidon/metrics/prometheus/PrometheusSupportTest.java index a2f5927cffc..5b5a2d5ff76 100644 --- a/metrics/prometheus/src/test/java/io/helidon/metrics/prometheus/PrometheusSupportTest.java +++ b/metrics/prometheus/src/test/java/io/helidon/metrics/prometheus/PrometheusSupportTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2022 Oracle and/or its affiliates. + * Copyright (c) 2017, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,17 +16,17 @@ package io.helidon.metrics.prometheus; -import java.util.concurrent.TimeUnit; - import io.helidon.common.http.Http; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.testsupport.TestClient; -import io.helidon.reactive.webserver.testsupport.TestRequest; -import io.helidon.reactive.webserver.testsupport.TestResponse; +import io.helidon.nima.testing.junit5.webserver.ServerTest; +import io.helidon.nima.testing.junit5.webserver.SetUpRoute; +import io.helidon.nima.webclient.http1.Http1Client; +import io.helidon.nima.webclient.http1.Http1ClientResponse; +import io.helidon.nima.webserver.http.HttpRouting; import io.prometheus.client.CollectorRegistry; import io.prometheus.client.Counter; import org.hamcrest.core.StringStartsWith; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,20 +35,25 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsNot.not; +@ServerTest public class PrometheusSupportTest { - private Routing routing; private Counter alpha; private Counter beta; + private static CollectorRegistry registry = new CollectorRegistry(); + private final Http1Client client; + + PrometheusSupportTest(Http1Client client) { + this.client = client; + } + + @SetUpRoute + static void routing(HttpRouting.Builder builder){ + builder.addFeature(PrometheusSupport.create(registry)).build(); + } @BeforeEach - public void prepareRouting() { - CollectorRegistry registry = new CollectorRegistry(); - // Routing - this.routing = Routing.builder() - .register(PrometheusSupport.create(registry)) - .build(); - // Metrics + public void prepareRegistry() { this.alpha = Counter.build() .name("alpha") .help("Alpha help with \\ and \n.") @@ -69,54 +74,53 @@ public void prepareRouting() { } } - private TestResponse doTestRequest(String nameQuery) throws Exception { - TestRequest request = TestClient.create(routing) - .path("/metrics"); - if (nameQuery != null && !nameQuery.isEmpty()) { - request.queryParameter("name[]", nameQuery); - } - TestResponse response = request.get(); - assertThat(response.status(), is(Http.Status.OK_200)); - return response; + @AfterEach + public void clearRegistry() { + registry.clear(); } @Test - public void simpleCall() throws Exception { - TestResponse response = doTestRequest(null); - assertThat(response.headers().first(Http.Header.CONTENT_TYPE).orElse(null), - StringStartsWith.startsWith("text/plain")); - String body = response.asString().get(5, TimeUnit.SECONDS); - assertThat(body, containsString("# HELP beta")); - assertThat(body, containsString("# TYPE beta counter")); - assertThat(body, containsString("beta 3.0")); - assertThat(body, containsString("# TYPE alpha counter")); - assertThat(body, containsString("# HELP alpha Alpha help with \\\\ and \\n.")); - assertThat(body, containsString("alpha{method=\"bar\",} 6.0")); - assertThat(body, containsString("alpha{method=\"\\\"foo\\\" \\\\ \\n\",} 5.0")); + public void simpleCall() { + try (Http1ClientResponse response = client.get("/metrics").request()) { + assertThat(response.status(), is(Http.Status.OK_200)); + assertThat(response.headers().first(Http.Header.CONTENT_TYPE).orElse(null), + StringStartsWith.startsWith("text/plain")); + String body = response.as(String.class); + assertThat(body, containsString("# HELP beta")); + assertThat(body, containsString("# TYPE beta counter")); + assertThat(body, containsString("beta 3.0")); + assertThat(body, containsString("# TYPE alpha counter")); + assertThat(body, containsString("# HELP alpha Alpha help with \\\\ and \\n.")); + assertThat(body, containsString("alpha{method=\"bar\",} 6.0")); + assertThat(body, containsString("alpha{method=\"\\\"foo\\\" \\\\ \\n\",} 5.0")); + } } @Test - public void doubleCall() throws Exception { - TestResponse response = doTestRequest(null); - assertThat(response.headers().first(Http.Header.CONTENT_TYPE).orElse(null), - StringStartsWith.startsWith("text/plain")); - String body = response.asString().get(5, TimeUnit.SECONDS); - assertThat(body, containsString("alpha{method=\"bar\",} 6.0")); - assertThat(body, not(containsString("alpha{method=\"baz\""))); - alpha.labels("baz").inc(); - response = doTestRequest(null); - body = response.asString().get(5, TimeUnit.SECONDS); - assertThat(body, containsString("alpha{method=\"baz\",} 1.0")); + public void doubleCall() { + try (Http1ClientResponse response = client.get("/metrics").request()) { + assertThat(response.headers().first(Http.Header.CONTENT_TYPE).orElse(null), + StringStartsWith.startsWith("text/plain")); + String body = response.as(String.class); + assertThat(body, containsString("alpha{method=\"bar\",} 6.0")); + assertThat(body, not(containsString("alpha{method=\"baz\""))); + alpha.labels("baz").inc(); + } + try (Http1ClientResponse response = client.get("/metrics").request()) { + String body = response.as(String.class); + assertThat(body, containsString("alpha{method=\"baz\",} 1.0")); + } } @Test - public void filter() throws Exception { - TestResponse response = doTestRequest("alpha"); - assertThat(response.status(), is(Http.Status.OK_200)); - String body = response.asString().get(5, TimeUnit.SECONDS); - assertThat(body, not(containsString("# TYPE beta"))); - assertThat(body, not(containsString("beta 3.0"))); - assertThat(body, containsString("# TYPE alpha counter")); - assertThat(body, containsString("alpha{method=\"bar\",} 6.0")); + public void filter() { + try (Http1ClientResponse response = client.get("/metrics").queryParam("name[]", "alpha").request()) { + assertThat(response.status(), is(Http.Status.OK_200)); + String body = response.as(String.class); + assertThat(body, not(containsString("# TYPE beta"))); + assertThat(body, not(containsString("beta 3.0"))); + assertThat(body, containsString("# TYPE alpha counter")); + assertThat(body, containsString("alpha{method=\"bar\",} 6.0")); + } } } diff --git a/microprofile/config/src/test/java/io/helidon/microprofile/config/MpConfigConvertTest.java b/microprofile/config/src/test/java/io/helidon/microprofile/config/MpConfigConvertTest.java new file mode 100644 index 00000000000..99c6ff41ddd --- /dev/null +++ b/microprofile/config/src/test/java/io/helidon/microprofile/config/MpConfigConvertTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.config; + +import io.helidon.config.mp.MpConfig; +import io.helidon.microprofile.tests.junit5.AddConfig; +import io.helidon.microprofile.tests.junit5.HelidonTest; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.Config; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Test for correct handling of other implementations of {@link Config}. + * + * @see "https://github.com/helidon-io/helidon/issues/6668" + */ +@HelidonTest +@AddConfig(key = "key", value = "value") +public class MpConfigConvertTest { + + @Inject + private Config mpConfig; + + //No exceptions should occur. + @Test + void testConvertToHelidonConfig() { + io.helidon.config.Config helidonConfig = MpConfig.toHelidonConfig(mpConfig); + assertThat(helidonConfig.get("key").asString().get(), is("value")); + } +} diff --git a/microprofile/graphql/server/pom.xml b/microprofile/graphql/server/pom.xml index 2f4f3b7939f..f1302469315 100644 --- a/microprofile/graphql/server/pom.xml +++ b/microprofile/graphql/server/pom.xml @@ -59,7 +59,7 @@ helidon-microprofile-config - org.jboss + io.smallrye jandex @@ -89,7 +89,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/microprofile/graphql/server/src/main/java/module-info.java b/microprofile/graphql/server/src/main/java/module-info.java index 175f1fc25b0..ad82a249355 100644 --- a/microprofile/graphql/server/src/main/java/module-info.java +++ b/microprofile/graphql/server/src/main/java/module-info.java @@ -47,7 +47,7 @@ requires io.helidon.microprofile.server; requires com.graphqljava; - requires graphql.java.extended.scalars; + requires com.graphqljava.extendedscalars; requires microprofile.graphql.api; requires microprofile.config.api; diff --git a/microprofile/jwt-auth/src/main/java/io/helidon/microprofile/jwt/auth/JwtAuthProvider.java b/microprofile/jwt-auth/src/main/java/io/helidon/microprofile/jwt/auth/JwtAuthProvider.java index 897ea3630e9..a427ab689b2 100644 --- a/microprofile/jwt-auth/src/main/java/io/helidon/microprofile/jwt/auth/JwtAuthProvider.java +++ b/microprofile/jwt-auth/src/main/java/io/helidon/microprofile/jwt/auth/JwtAuthProvider.java @@ -84,7 +84,6 @@ import io.helidon.security.providers.common.TokenCredential; import io.helidon.security.spi.AuthenticationProvider; import io.helidon.security.spi.OutboundSecurityProvider; -import io.helidon.security.spi.SynchronousProvider; import io.helidon.security.util.TokenHandler; import jakarta.enterprise.inject.spi.DeploymentException; @@ -100,7 +99,7 @@ /** * Provider that provides JWT authentication. */ -public class JwtAuthProvider extends SynchronousProvider implements AuthenticationProvider, OutboundSecurityProvider { +public class JwtAuthProvider implements AuthenticationProvider, OutboundSecurityProvider { /** * Configure this for outbound requests to override user to use. @@ -212,7 +211,7 @@ public Collection> supportedAnnotations() { } @Override - protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { if (!authenticate) { return AuthenticationResponse.abstain(); } @@ -376,9 +375,9 @@ public boolean isOutboundSupported(ProviderRequest providerRequest, } @Override - public OutboundSecurityResponse syncOutbound(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundEndpointConfig) { + public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv, + EndpointConfig outboundEndpointConfig) { Optional maybeUsername = outboundEndpointConfig.abacAttribute(EP_PROPERTY_OUTBOUND_USER); return maybeUsername diff --git a/microprofile/jwt-auth/src/test/java/io/helidon/microprofile/jwt/auth/JwtAuthProviderTest.java b/microprofile/jwt-auth/src/test/java/io/helidon/microprofile/jwt/auth/JwtAuthProviderTest.java index 63277e1e1f3..6043d8faa75 100644 --- a/microprofile/jwt-auth/src/test/java/io/helidon/microprofile/jwt/auth/JwtAuthProviderTest.java +++ b/microprofile/jwt-auth/src/test/java/io/helidon/microprofile/jwt/auth/JwtAuthProviderTest.java @@ -116,7 +116,7 @@ public String realmName() { when(atnRequest.env()).thenReturn(se); when(atnRequest.endpointConfig()).thenReturn(ec); - AuthenticationResponse authenticationResponse = provider.syncAuthenticate(atnRequest); + AuthenticationResponse authenticationResponse = provider.authenticate(atnRequest); assertThat(authenticationResponse.service(), is(Optional.empty())); assertThat(authenticationResponse.user(), is(Optional.empty())); @@ -169,7 +169,7 @@ public void testEcBothWays() { assertThat(provider.isOutboundSupported(request, outboundEnv, outboundEp), is(true)); - OutboundSecurityResponse response = provider.syncOutbound(request, outboundEnv, outboundEp); + OutboundSecurityResponse response = provider.outboundSecurity(request, outboundEnv, outboundEp); String signedToken = response.requestHeaders().get("Authorization").get(0); signedToken = signedToken.substring("bearer ".length()); @@ -208,7 +208,7 @@ public void testEcBothWays() { //now we need to use the same token to invoke authentication ProviderRequest atnRequest = mockRequest(signedToken); - AuthenticationResponse authenticationResponse = provider.syncAuthenticate(atnRequest); + AuthenticationResponse authenticationResponse = provider.authenticate(atnRequest); authenticationResponse.user() .map(Subject::principal) .ifPresentOrElse(atnPrincipal -> { @@ -252,7 +252,7 @@ public void testOctBothWays() { assertThat(provider.isOutboundSupported(request, outboundEnv, outboundEp), is(true)); - OutboundSecurityResponse response = provider.syncOutbound(request, outboundEnv, outboundEp); + OutboundSecurityResponse response = provider.outboundSecurity(request, outboundEnv, outboundEp); String signedToken = response.requestHeaders().get("Authorization").get(0); signedToken = signedToken.substring("bearer ".length()); @@ -284,7 +284,7 @@ public void testOctBothWays() { //now we need to use the same token to invoke authentication ProviderRequest atnRequest = mockRequest(signedToken); - AuthenticationResponse authenticationResponse = provider.syncAuthenticate(atnRequest); + AuthenticationResponse authenticationResponse = provider.authenticate(atnRequest); authenticationResponse.user() .map(Subject::principal) .ifPresentOrElse(atnPrincipal -> { @@ -340,7 +340,7 @@ public void testRsaBothWays() { assertThat(provider.isOutboundSupported(request, outboundEnv, outboundEp), is(true)); - OutboundSecurityResponse response = provider.syncOutbound(request, outboundEnv, outboundEp); + OutboundSecurityResponse response = provider.outboundSecurity(request, outboundEnv, outboundEp); String signedToken = response.requestHeaders().get("Authorization").get(0); signedToken = signedToken.substring("bearer ".length()); @@ -375,7 +375,7 @@ public void testRsaBothWays() { //now we need to use the same token to invoke authentication ProviderRequest atnRequest = mockRequest(signedToken); - AuthenticationResponse authenticationResponse = provider.syncAuthenticate(atnRequest); + AuthenticationResponse authenticationResponse = provider.authenticate(atnRequest); authenticationResponse.user() .map(Subject::principal) .ifPresentOrElse(atnPrincipal -> { diff --git a/microprofile/jwt-auth/src/test/java/io/helidon/microprofile/jwt/auth/JwtAuthTest.java b/microprofile/jwt-auth/src/test/java/io/helidon/microprofile/jwt/auth/JwtAuthTest.java index ecfb5254564..6257c4b9c27 100644 --- a/microprofile/jwt-auth/src/test/java/io/helidon/microprofile/jwt/auth/JwtAuthTest.java +++ b/microprofile/jwt-auth/src/test/java/io/helidon/microprofile/jwt/auth/JwtAuthTest.java @@ -104,7 +104,7 @@ void testRsa() { assertThat(provider.isOutboundSupported(request, outboundEnv, outboundEp), is(true)); - OutboundSecurityResponse response = provider.syncOutbound(request, outboundEnv, outboundEp); + OutboundSecurityResponse response = provider.outboundSecurity(request, outboundEnv, outboundEp); String signedToken = response.requestHeaders().get("Authorization").get(0); diff --git a/microprofile/lra/jax-rs/pom.xml b/microprofile/lra/jax-rs/pom.xml index 01182282e03..9c1e1afb40b 100644 --- a/microprofile/lra/jax-rs/pom.xml +++ b/microprofile/lra/jax-rs/pom.xml @@ -36,7 +36,7 @@ compile - org.jboss + io.smallrye jandex diff --git a/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/InspectionService.java b/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/InspectionService.java index d0de6ed237f..1b9a58a00ab 100644 --- a/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/InspectionService.java +++ b/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/InspectionService.java @@ -158,7 +158,7 @@ Set lookUpLraAnnotations(MethodInfo methodInfo) { methodInfo.declaringClass(), annotations, methodInfo.name(), - methodInfo.parameters().toArray(new Type[0]) + methodInfo.parameterTypes().toArray(new Type[0]) ); HashSet result = new HashSet<>(annotations.values()); diff --git a/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/LraCdiExtension.java b/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/LraCdiExtension.java index 903743025de..364d9f1cf4f 100644 --- a/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/LraCdiExtension.java +++ b/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/LraCdiExtension.java @@ -15,6 +15,8 @@ */ package io.helidon.microprofile.lra; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.System.Logger.Level; @@ -68,6 +70,7 @@ import org.jboss.jandex.ClassInfo; import org.jboss.jandex.CompositeIndex; import org.jboss.jandex.DotName; +import org.jboss.jandex.Index; import org.jboss.jandex.IndexReader; import org.jboss.jandex.IndexView; import org.jboss.jandex.Indexer; @@ -255,9 +258,18 @@ void runtimeIndex(DotName fqdn) { if (fqdn == null) return; LOGGER.log(Level.DEBUG, "Indexing " + fqdn); ClassInfo classInfo; - try { - classInfo = indexer.index(classLoader.getResourceAsStream(fqdn.toString().replace('.', '/') + ".class")); - // look also for extended classes + try (InputStream classStream = classLoader.getResourceAsStream(fqdn.toString().replace('.', '/') + ".class")) { + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int bytesRead; + byte[] buffer = new byte[512]; + while ((bytesRead = classStream.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + + indexer.index(bais); + classInfo = Index.singleClass(new ByteArrayInputStream(baos.toByteArray())); // look also for extended classes runtimeIndex(classInfo.superName()); // and implemented interfaces classInfo.interfaceNames().forEach(this::runtimeIndex); diff --git a/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/ParticipantValidationModel.java b/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/ParticipantValidationModel.java index 6ce5637f939..5f0399dfbf4 100644 --- a/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/ParticipantValidationModel.java +++ b/microprofile/lra/jax-rs/src/main/java/io/helidon/microprofile/lra/ParticipantValidationModel.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -154,7 +154,7 @@ private static class ParticipantMethod { } String nameWithParams() { - return methodInfo.name() + methodInfo.parameters().stream() + return methodInfo.name() + methodInfo.parameterTypes().stream() .map(Type::name) .map(DotName::toString) .collect(Collectors.joining()); diff --git a/microprofile/messaging/health/pom.xml b/microprofile/messaging/health/pom.xml index 1ec6191dfd8..0b6b2646f69 100644 --- a/microprofile/messaging/health/pom.xml +++ b/microprofile/messaging/health/pom.xml @@ -55,21 +55,6 @@ junit-jupiter-params test - - io.helidon.reactive.webclient - helidon-reactive-webclient - test - - - io.helidon.reactive.media - helidon-reactive-media-jsonp - test - - - io.helidon.reactive.media - helidon-reactive-media-jsonb - test - io.helidon.microprofile.tests helidon-microprofile-tests-junit5 diff --git a/microprofile/messaging/health/src/test/java/io/helidon/microprofile/messaging/health/MessagingHealthTest.java b/microprofile/messaging/health/src/test/java/io/helidon/microprofile/messaging/health/MessagingHealthTest.java index 7734b2a6876..9f0218c7841 100644 --- a/microprofile/messaging/health/src/test/java/io/helidon/microprofile/messaging/health/MessagingHealthTest.java +++ b/microprofile/messaging/health/src/test/java/io/helidon/microprofile/messaging/health/MessagingHealthTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,14 +30,15 @@ import io.helidon.microprofile.tests.junit5.AddExtensions; import io.helidon.microprofile.tests.junit5.DisableDiscovery; import io.helidon.microprofile.tests.junit5.HelidonTest; -import io.helidon.reactive.media.jsonp.JsonpSupport; -import io.helidon.reactive.webclient.WebClient; import jakarta.enterprise.inject.se.SeContainer; import jakarta.enterprise.inject.spi.CDI; import jakarta.json.JsonObject; import jakarta.json.JsonValue; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; import org.eclipse.microprofile.health.HealthCheckResponse; +import org.glassfish.jersey.ext.cdi1x.internal.CdiComponentProvider; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.opentest4j.AssertionFailedError; @@ -78,33 +79,28 @@ @AddExtension(JaxRsCdiExtension.class), @AddExtension(HealthCdiExtension.class), @AddExtension(MessagingCdiExtension.class), + @AddExtension(CdiComponentProvider.class) }) public class MessagingHealthTest { private static final String ERROR_MESSAGE = "BOOM!"; - private WebClient client; - @BeforeEach void setUp() { ServerCdiExtension server = CDI.current().select(ServerCdiExtension.class).get(); - client = WebClient.builder() - .baseUri("http://localhost:" + server.port()) - .addReader(JsonpSupport.reader()) - .build(); } @Test - void alivenessWithErrorSignal(SeContainer container) { + void alivenessWithErrorSignal(WebTarget webTarget, SeContainer container) { TestMessagingBean bean = container.select(TestMessagingBean.class).get(); - assertMessagingHealth(UP, Map.of( + assertMessagingHealth(webTarget, UP, Map.of( CHANNEL_1, UP, CHANNEL_2, UP )); bean.getEmitter1().fail(new RuntimeException(ERROR_MESSAGE)); - assertMessagingHealth(DOWN, Map.of( + assertMessagingHealth(webTarget, DOWN, Map.of( CHANNEL_1, DOWN, CHANNEL_2, UP )); @@ -112,7 +108,7 @@ void alivenessWithErrorSignal(SeContainer container) { equalTo(ERROR_MESSAGE)); bean.getEmitter2().fail(new RuntimeException(ERROR_MESSAGE)); - assertMessagingHealth(DOWN, Map.of( + assertMessagingHealth(webTarget, DOWN, Map.of( CHANNEL_1, DOWN, CHANNEL_2, DOWN )); @@ -121,10 +117,10 @@ void alivenessWithErrorSignal(SeContainer container) { } @Test - void alivenessWithCancelSignal(SeContainer container) { + void alivenessWithCancelSignal(WebTarget webTarget, SeContainer container) { TestMessagingBean bean = container.select(TestMessagingBean.class).get(); - assertMessagingHealth(UP, Map.of( + assertMessagingHealth(webTarget, UP, Map.of( CHANNEL_1, UP, CHANNEL_2, UP )); @@ -132,40 +128,35 @@ void alivenessWithCancelSignal(SeContainer container) { assertThat(bean.getEmitter2().isCancelled(), equalTo(Boolean.FALSE)); bean.getSubscriber1().cancel(); - assertMessagingHealth(DOWN, Map.of( + assertMessagingHealth(webTarget, DOWN, Map.of( CHANNEL_1, DOWN, CHANNEL_2, UP )); assertThat(bean.getEmitter1().isCancelled(), equalTo(Boolean.TRUE)); bean.getSubscriber2().cancel(); - assertMessagingHealth(DOWN, Map.of( + assertMessagingHealth(webTarget, DOWN, Map.of( CHANNEL_1, DOWN, CHANNEL_2, DOWN )); assertThat(bean.getEmitter2().isCancelled(), equalTo(Boolean.TRUE)); } - private void assertMessagingHealth(HealthCheckResponse.Status rootState, Map channels) { - JsonObject messaging = getHealthCheck("messaging"); + private void assertMessagingHealth(WebTarget webTarget, HealthCheckResponse.Status rootState, Map channels) { + JsonObject messaging = getHealthCheck(webTarget, "messaging"); assertThat(messaging.getString("status"), equalTo(rootState.name())); JsonObject data = messaging.getJsonObject("data"); channels.forEach((name, state) -> assertThat(data.getString(name), equalTo(state.name()))); } - private JsonObject getHealthCheck(String checkName) { - return client.get() - .path("/health") - .submit() - .await(5, TimeUnit.SECONDS) - .content() - .as(JsonObject.class) - .await(500, TimeUnit.MILLISECONDS) - .getValue("/checks") - .asJsonArray().stream() - .map(JsonValue::asJsonObject) - .filter(check -> check.getString("name").equals(checkName)) - .findFirst() - .orElseThrow(() -> new AssertionFailedError("Health check 'messaging' is missing!")); + private JsonObject getHealthCheck(WebTarget webTarget, String checkName) { + Response response = webTarget.path("/health").request().get(); + JsonObject jsonObject = response.readEntity(JsonObject.class); + return jsonObject.getValue("/checks") + .asJsonArray().stream() + .map(JsonValue::asJsonObject) + .filter(check -> check.getString("name").equals(checkName)) + .findFirst() + .orElseThrow(() -> new AssertionFailedError("Health check 'messaging' is missing!")); } } diff --git a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldAsyncResponseTest.java b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldAsyncResponseTest.java index c41041e6c5b..e5122fa1e4e 100644 --- a/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldAsyncResponseTest.java +++ b/microprofile/metrics/src/test/java/io/helidon/microprofile/metrics/HelloWorldAsyncResponseTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import org.eclipse.microprofile.metrics.SimpleTimer; import org.eclipse.microprofile.metrics.Timer; import org.eclipse.microprofile.metrics.annotation.RegistryType; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static io.helidon.common.testing.junit5.MatcherWithRetry.assertThatWithRetry; @@ -72,6 +73,7 @@ public class HelloWorldAsyncResponseTest { @RegistryType(type = MetricRegistry.Type.VENDOR) private MetricRegistry vendorRegistry; + @Disabled @Test public void test() throws Exception { MetricID metricID = MetricsCdiExtension diff --git a/microprofile/openapi/etc/spotbugs/exclude.xml b/microprofile/openapi/etc/spotbugs/exclude.xml index ebd4a4f77bc..860b15bdb87 100644 --- a/microprofile/openapi/etc/spotbugs/exclude.xml +++ b/microprofile/openapi/etc/spotbugs/exclude.xml @@ -1,7 +1,7 @@ + + + + + + + + + + diff --git a/microprofile/openapi/pom.xml b/microprofile/openapi/pom.xml index 2be1f182933..8eb56f243cc 100644 --- a/microprofile/openapi/pom.xml +++ b/microprofile/openapi/pom.xml @@ -36,9 +36,15 @@ etc/spotbugs/exclude.xml + ${project.build.directory}/extracted-sources/openapi-interfaces + ${project.build.directory}/extracted-sources/openapi-impls + + io.helidon.openapi + helidon-openapi + org.eclipse.microprofile.config microprofile-config-api @@ -48,13 +54,44 @@ helidon-microprofile-server - io.helidon.openapi - helidon-openapi + io.helidon.nima.service-common + helidon-nima-service-common + + + io.helidon.microprofile.service-common + helidon-microprofile-service-common + + + io.smallrye + smallrye-open-api-core + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + - io.helidon.nima.openapi - helidon-nima-openapi + io.smallrye + smallrye-open-api-jaxrs + + + org.yaml + snakeyaml + + io.smallrye + jandex + + io.helidon.common.features helidon-common-features-api @@ -85,11 +122,6 @@ junit-jupiter-params test - - org.yaml - snakeyaml - test - io.helidon.microprofile.tests helidon-microprofile-tests-junit5 @@ -108,7 +140,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin @@ -127,6 +159,7 @@ + ${project.build.testOutputDirectory}/META-INF @@ -146,6 +179,7 @@ + ${project.build.testOutputDirectory}/META-INF @@ -163,6 +197,76 @@ + + org.apache.maven.plugins + maven-dependency-plugin + + + unpack-openapi-interfaces + + unpack-dependencies + + generate-sources + + sources + true + ${openapi-interfaces-dir} + org.eclipse.microprofile.openapi + microprofile-openapi-api + org/eclipse/microprofile/openapi/models/**/*.java + + + + unpack-openapi-impls + + unpack-dependencies + + generate-sources + + sources + true + ${openapi-impls-dir} + io.smallrye + smallrye-open-api-core + io/smallrye/openapi/api/models/**/*.java + + + + + + io.helidon.build-tools + snakeyaml-codegen-maven-plugin + + + generate-snakeyaml-parsing-helper + + generate + + generate-sources + + io.helidon.microprofile.openapi.SnakeYAMLParserHelper + + ${openapi-interfaces-dir} + + + ${openapi-impls-dir} + + io.smallrye + org.eclipse.microprofile.openapi + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + true + + + diff --git a/openapi/src/main/java/io/helidon/openapi/CustomConstructor.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/CustomConstructor.java similarity index 80% rename from openapi/src/main/java/io/helidon/openapi/CustomConstructor.java rename to microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/CustomConstructor.java index 9dd8d074ce6..67e8cc02086 100644 --- a/openapi/src/main/java/io/helidon/openapi/CustomConstructor.java +++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/CustomConstructor.java @@ -13,14 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.openapi; +package io.helidon.microprofile.openapi; import java.lang.System.Logger.Level; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Function; +import org.eclipse.microprofile.openapi.models.Extensible; import org.eclipse.microprofile.openapi.models.PathItem; import org.eclipse.microprofile.openapi.models.Paths; import org.eclipse.microprofile.openapi.models.callbacks.Callback; @@ -36,6 +38,8 @@ import org.yaml.snakeyaml.nodes.MappingNode; import org.yaml.snakeyaml.nodes.Node; import org.yaml.snakeyaml.nodes.NodeId; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.ScalarNode; import org.yaml.snakeyaml.nodes.SequenceNode; import org.yaml.snakeyaml.nodes.Tag; @@ -228,17 +232,57 @@ class ConstructMapping extends Constructor.ConstructMapping { @Override public Object construct(Node node) { - Class parentType = node.getType(); + MappingNode mappingNode = (MappingNode) node; + Class parentType = mappingNode.getType(); + Map extensions = new HashMap<>(); + + // If the element has extension properties, SnakeYAML will have prepared a MappingNode even if + // the node type is actually a scalar, for example. + if (Extensible.class.isAssignableFrom(parentType)) { + // Save the extension property names and values, remove the corresponding child nodes, + // let SnakeYAML process the adjusted node, then set the saved extension properties. + var allTuples = mappingNode.getValue(); + List extensionTuples = new ArrayList<>(); + allTuples.forEach(tuple -> { + String name = ((ScalarNode) tuple.getKeyNode()).getValue(); + if (name.startsWith("x-")) { + extensionTuples.add(tuple); + // Extension values can be scalars, sequences, or maps. Using constructObject here will create the + // correct value type. + Node valueNode = tuple.getValueNode(); + Object value; + if (valueNode.getTag().equals(Tag.STR)) { + value = constructScalar((ScalarNode) valueNode); + } else if (valueNode.getTag().equals(Tag.SEQ)) { + value = constructSequence((SequenceNode) valueNode); + } else if (valueNode.getTag().equals(Tag.MAP)) { + value = constructMapping((MappingNode) valueNode); + } else { + value = constructObject(valueNode); + } + extensions.put(name, value); + } + }); + allTuples.removeAll(extensionTuples); + } + + Object result; if (CHILD_MAP_TYPES.containsKey(parentType) || CHILD_MAP_OF_LIST_TYPES.containsKey(parentType)) { - // Following is inspired by SnakeYAML Constructor$ConstructMapping#construct. - MappingNode mappingNode = (MappingNode) node; - if (node.isTwoStepsConstruction()) { - return newMap(mappingNode); + // Following is inspired by SnakeYAML Constructor$ConstructMapping#construct + // and Constructor#ConstructSequence#construct. + if (mappingNode.isTwoStepsConstruction()) { + result = newMap(mappingNode); } else { - return constructMapping(mappingNode); + result = constructMapping(mappingNode); } + } else { + result = super.construct(mappingNode); + } + + if (!extensions.isEmpty()) { + ((Extensible) result).setExtensions(extensions); } - return super.construct(node); + return result; } } } diff --git a/openapi/src/main/java/io/helidon/openapi/ExpandedTypeDescription.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/ExpandedTypeDescription.java similarity index 92% rename from openapi/src/main/java/io/helidon/openapi/ExpandedTypeDescription.java rename to microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/ExpandedTypeDescription.java index 76bba584776..c985b7fc131 100644 --- a/openapi/src/main/java/io/helidon/openapi/ExpandedTypeDescription.java +++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/ExpandedTypeDescription.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.openapi; +package io.helidon.microprofile.openapi; import java.beans.IntrospectionException; import java.beans.PropertyDescriptor; @@ -22,9 +22,8 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; +import io.smallrye.openapi.api.models.media.SchemaImpl; import org.eclipse.microprofile.openapi.models.Extensible; import org.eclipse.microprofile.openapi.models.media.Schema; import org.yaml.snakeyaml.TypeDescription; @@ -33,7 +32,9 @@ import org.yaml.snakeyaml.introspector.Property; import org.yaml.snakeyaml.introspector.PropertySubstitute; import org.yaml.snakeyaml.introspector.PropertyUtils; +import org.yaml.snakeyaml.nodes.MappingNode; import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.NodeTuple; import org.yaml.snakeyaml.nodes.ScalarNode; import org.yaml.snakeyaml.nodes.Tag; @@ -112,20 +113,6 @@ public static ExpandedTypeDescription create(Class clazz, Class impl) { return result; } - /** - * Build a map of implementations to types. - * - * @param helper parser helper - * @return map of implementation classes to descriptions - */ - public static Map, ExpandedTypeDescription> buildImplsToTypes(ParserHelper helper) { - return Collections.unmodifiableMap(helper.entrySet() - .stream() - .map(Map.Entry::getValue) - .collect(Collectors.toMap(ExpandedTypeDescription::impl, - Function.identity()))); - } - /** * Adds property handling for a {@code $ref} reference. * @@ -226,6 +213,15 @@ Property getPropertyNoEx(String name) { } } + /** + * Returns the default property for the type. + * + * @return the 'default' property for this type; null if none + */ + Property defaultProperty() { + return getPropertyNoEx("defaultValue"); + } + private static boolean setupExtensionType(String key, Node valueNode) { if (isExtension(key)) { /* @@ -271,7 +267,7 @@ private static boolean isRef(String name) { * This type description customizes the handling of {@code additionalProperties} to account for all that. *

* - * @see io.helidon.openapi.Serializer (specifically doRepresentJavaBeanProperty) for output handling for + * @see Serializer (specifically doRepresentJavaBeanProperty) for output handling for * additionalProperties */ static final class SchemaTypeDescription extends ExpandedTypeDescription { @@ -303,6 +299,26 @@ private SchemaTypeDescription(Class clazz, Class impl) { super(clazz, impl); } + @Override + public Object newInstance(Node node) { + // Schemas specified in config often have a name, and in SmallRye we need to provide the name to the constructor. + // So find the name if it's there. + String name = ""; + if (node instanceof MappingNode mappingNode) { + for (NodeTuple nodeTuple : mappingNode.getValue()) { + if (nodeTuple.getKeyNode() instanceof ScalarNode scalarKeyNode) { + if (scalarKeyNode.getValue().equals("name")) { + if (nodeTuple.getValueNode() instanceof ScalarNode scalarValueNode) { + name = scalarValueNode.getValue(); + break; + } + } + } + } + } + return new SchemaImpl(name); + } + @Override public Property getProperty(String name) { return name.equals("additionalProperties") ? ADDL_PROPS_PROPERTY : super.getProperty(name); diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/HelidonAnnotationScannerExtension.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/HelidonAnnotationScannerExtension.java new file mode 100644 index 00000000000..62cc0c33290 --- /dev/null +++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/HelidonAnnotationScannerExtension.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.openapi; + +import java.io.StringReader; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; + +import io.smallrye.openapi.runtime.scanner.AnnotationScannerExtension; +import jakarta.json.Json; +import jakarta.json.JsonNumber; +import jakarta.json.JsonReader; +import jakarta.json.JsonReaderFactory; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; +import org.eclipse.microprofile.openapi.models.media.Schema; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.representer.Representer; + +/** + * Extension we want SmallRye's OpenAPI implementation to use for parsing the JSON content in Extension annotations. + */ +class HelidonAnnotationScannerExtension implements AnnotationScannerExtension { + + private static final System.Logger LOGGER = System.getLogger(HelidonAnnotationScannerExtension.class.getName()); + + private static final JsonReaderFactory JSON_READER_FACTORY = Json.createReaderFactory(Collections.emptyMap()); + + private static final Representer MISSING_FIELD_TOLERANT_REPRESENTER; + + static { + MISSING_FIELD_TOLERANT_REPRESENTER = new Representer(new DumperOptions()); + MISSING_FIELD_TOLERANT_REPRESENTER.getPropertyUtils().setSkipMissingProperties(true); + } + + @Override + public Object parseExtension(String key, String value) { + try { + return parseValue(value); + } catch (Exception ex) { + LOGGER.log(System.Logger.Level.ERROR, + String.format("Error parsing extension key: %s, value: %s", key, value), + ex); + return null; + } + } + + @Override + public Object parseValue(String value) { + // Inspired by SmallRye's JsonUtil#parseValue method. + if (value == null || value.isBlank()) { + return null; + } + + value = value.trim(); + + if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) { + return Boolean.valueOf(value); + } + + // See if we should parse the value fully. + switch (value.charAt(0)) { + case '{', '[', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> { + try { + JsonReader reader = JSON_READER_FACTORY.createReader(new StringReader(value)); + JsonValue jsonValue = reader.readValue(); + // readValue will truncate the input to convert to a number if it can. Make sure the value is the same length + // as the original. + if (jsonValue.getValueType().equals(JsonValue.ValueType.NUMBER) + && value.length() != jsonValue.toString().length()) { + return value; + } + + return convertJsonValue(jsonValue); + } catch (Exception ex) { + LOGGER.log(System.Logger.Level.ERROR, + String.format("Error parsing JSON value: %s", value), + ex); + throw ex; + } + } + default -> { + } + } + + // Treat as JSON string. + return value; + } + + @Override + public Schema parseSchema(String jsonSchema) { + return OpenApiParser.parse(MpOpenApiFeature.PARSER_HELPER.get().types(), + Schema.class, + new StringReader(jsonSchema), + MISSING_FIELD_TOLERANT_REPRESENTER); + } + + private static Object convertJsonValue(JsonValue jsonValue) { + return switch (jsonValue.getValueType()) { + case ARRAY -> jsonValue.asJsonArray() + .stream() + .map(HelidonAnnotationScannerExtension::convertJsonValue) + .collect(Collectors.toList()); + case FALSE -> Boolean.FALSE; + case TRUE -> Boolean.TRUE; + case NULL -> null; + case STRING -> JsonString.class.cast(jsonValue).getString(); + case NUMBER -> JsonNumber.class.cast(jsonValue).numberValue(); + case OBJECT -> jsonValue.asJsonObject() + .entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> convertJsonValue(entry.getValue()))); + }; + } +} diff --git a/openapi/src/main/java/io/helidon/openapi/ImplTypeDescription.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/ImplTypeDescription.java similarity index 98% rename from openapi/src/main/java/io/helidon/openapi/ImplTypeDescription.java rename to microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/ImplTypeDescription.java index e00dbc4d563..162dc84e7c1 100644 --- a/openapi/src/main/java/io/helidon/openapi/ImplTypeDescription.java +++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/ImplTypeDescription.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.openapi; +package io.helidon.microprofile.openapi; import java.util.Set; diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MPOpenAPIBuilder.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MPOpenAPIBuilder.java index acc8eb7d124..41558f5e43e 100644 --- a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MPOpenAPIBuilder.java +++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MPOpenAPIBuilder.java @@ -15,22 +15,26 @@ */ package io.helidon.microprofile.openapi; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; import java.lang.System.Logger.Level; import java.lang.reflect.Modifier; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.Comparator; +import java.util.Enumeration; import java.util.HashSet; import java.util.List; -import java.util.Objects; +import java.util.Optional; import java.util.Set; -import java.util.function.Supplier; -import java.util.regex.Pattern; import java.util.stream.Collectors; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.config.Config; import io.helidon.microprofile.server.JaxRsApplication; -import io.helidon.microprofile.server.JaxRsCdiExtension; -import io.helidon.nima.openapi.OpenApiService; +import io.helidon.openapi.OpenApiFeature; import io.smallrye.openapi.api.OpenApiConfig; import io.smallrye.openapi.api.OpenApiConfigImpl; @@ -40,21 +44,25 @@ import jakarta.ws.rs.core.Application; import jakarta.ws.rs.core.Feature; import jakarta.ws.rs.ext.Provider; -import org.eclipse.microprofile.config.Config; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.CompositeIndex; import org.jboss.jandex.DotName; +import org.jboss.jandex.Index; +import org.jboss.jandex.IndexReader; import org.jboss.jandex.IndexView; +import org.jboss.jandex.Indexer; /** - * Fluent builder for OpenAPISupport in Helidon MP. + * Builder for the MP OpenAPI feature. */ -@Configured(prefix = MPOpenAPIBuilder.MP_OPENAPI_CONFIG_PREFIX) -public final class MPOpenAPIBuilder extends OpenApiService.AbstractBuilder { +class MPOpenAPIBuilder extends OpenApiFeature.Builder { + + private static final System.Logger LOGGER = System.getLogger(MPOpenAPIBuilder.class.getName()); // This is the prefix users will use in the config file. - static final String MP_OPENAPI_CONFIG_PREFIX = "mp." + OpenApiService.Builder.CONFIG_KEY; + static final String MP_OPENAPI_CONFIG_PREFIX = "mp." + OpenApiFeature.Builder.CONFIG_KEY; private static final String USE_JAXRS_SEMANTICS_CONFIG_KEY = "use-jaxrs-semantics"; @@ -62,134 +70,103 @@ public final class MPOpenAPIBuilder extends OpenApiService.AbstractBuilder singleIndexViewSupplier = null; - - private Config mpConfig; + MPOpenAPIBuilder() { + super(); + } @Override - protected OpenApiConfig openAPIConfig() { - return openAPIConfig; + public MpOpenApiFeature build() { + List indexURLs = findIndexFiles(indexPaths); + indexURLCount = indexURLs.size(); + if (indexURLs.isEmpty()) { + LOGGER.log(Level.INFO, String.format(""" + OpenAPI feature could not locate the Jandex index file %s so will build an in-memory index. + This slows your app start-up and, depending on CDI configuration, might omit some type information \ + needed for a complete OpenAPI document. + Consider using the Jandex maven plug-in during your build to create the index and add it to your app.""", + OpenApiCdiExtension.INDEX_PATH)); + } + if (openApiConfig == null) { + openApiConfig = new OpenApiConfigImpl(mpConfig); + } + return new MpOpenApiFeature(this); } @Override - public MPOpenAPISupport build() { - MPOpenAPISupport result = new MPOpenAPISupport(this); - validate(); - return result; + public MPOpenAPIBuilder config(Config config) { + super.config(config); + return identity(); } /** - * Returns the {@code JaxRsApplication} instances that should be run, according to the JAX-RS CDI extension. + * Sets the SmallRye OpenAPI configuration. * - * @return List of JaxRsApplication instances that should be run + * @param openApiConfig the {@link io.smallrye.openapi.api.OpenApiConfig} settings + * @return updated builder */ - static List jaxRsApplicationsToRun() { - JaxRsCdiExtension ext = CDI.current() - .getBeanManager() - .getExtension(JaxRsCdiExtension.class); - - return ext.applicationsToRun(); + public MPOpenAPIBuilder openApiConfig(OpenApiConfig openApiConfig) { + this.openApiConfig = openApiConfig; + return this; } /** - * Builds a list of filtered index views, one for each JAX-RS application, sorted by the Application class name to help - * keep the list of endpoints in the OpenAPI document in a stable order. - *

- * First, we find all resource, provider, and feature classes present in the index. This is the same for all - * applications. - *

- *

- * Each filtered index view is tuned to one JAX-RS application. + * Returns an {@link org.jboss.jandex.IndexView} for the Jandex index that describes + * annotated classes for endpoints. * - * @return list of {@code FilteredIndexView}s, one per JAX-RS application + * @return {@code IndexView} describing discovered classes */ - private List buildPerAppFilteredIndexViews() { - - List jaxRsApplications = jaxRsApplicationsToRun().stream() - .filter(jaxRsApp -> jaxRsApp.applicationClass().isPresent()) - .sorted(Comparator.comparing(jaxRsApplication -> jaxRsApplication.applicationClass() - .get() - .getName())) - .collect(Collectors.toList()); - - IndexView indexView = singleIndexViewSupplier.get(); - - FilteredIndexView viewFilteredByConfig = new FilteredIndexView(indexView, OpenApiConfigImpl.fromConfig(mpConfig)); - Set ancillaryClassNames = ancillaryClassNames(viewFilteredByConfig); - - /* - * Filter even for a single-application class in case it implements getClasses or getSingletons. - */ - return jaxRsApplications.stream() - .map(jaxRsApp -> filteredIndexView(viewFilteredByConfig, - jaxRsApplications, - jaxRsApp, - ancillaryClassNames)) - .collect(Collectors.toList()); - } - - private static Set ancillaryClassNames(IndexView indexView) { - Set result = new HashSet<>(resourceClassNames(indexView)); - result.addAll(providerClassNames(indexView)); - result.addAll(featureClassNames(indexView)); - if (LOGGER.isLoggable(Level.TRACE)) { - LOGGER.log(Level.TRACE, "Ancillary classes: {0}", result); + IndexView indexView() { + try { + return indexURLCount > 0 ? existingIndexFileReader() : indexFromHarvestedClasses(); + } catch (IOException e) { + throw new RuntimeException(e); } - return result; } - private static Set resourceClassNames(IndexView indexView) { - return annotatedClassNames(indexView, Path.class); - } - - private static Set providerClassNames(IndexView indexView) { - return annotatedClassNames(indexView, Provider.class); + /** + * Returns the {@link io.smallrye.openapi.api.OpenApiConfig} instance the builder uses. + * + * @return {@code OpenApiConfig} instance in use by the builder + */ + OpenApiConfig openApiConfig() { + return openApiConfig; } - private static Set featureClassNames(IndexView indexView) { - return annotatedClassNames(indexView, Feature.class); + @Override + protected System.Logger logger() { + return LOGGER; } - private static Set annotatedClassNames(IndexView indexView, Class annotationClass) { - // Partially inspired by the SmallRye code. - return indexView - .getAnnotations(DotName.createSimple(annotationClass.getName())) - .stream() - .map(AnnotationInstance::target) - .filter(target -> target.kind() == AnnotationTarget.Kind.CLASS) - .map(AnnotationTarget::asClass) - .filter(classInfo -> hasImplementationOrIsIncluded(indexView, classInfo)) - .map(ClassInfo::toString) - .collect(Collectors.toSet()); - } + MPOpenAPIBuilder config(org.eclipse.microprofile.config.Config mpConfig) { + this.mpConfig = mpConfig; + // use-jaxrs-semantics is intended for Helidon's private use in running the TCKs to work around a problem there. + // We do not document its use. + useJaxRsSemantics = mpConfig + .getOptionalValue(USE_JAXRS_SEMANTICS_FULL_CONFIG_KEY, Boolean.class) + .orElse(USE_JAXRS_SEMANTICS_DEFAULT); - private static boolean hasImplementationOrIsIncluded(IndexView indexView, ClassInfo classInfo) { - // Partially inspired by the SmallRye code. - return !Modifier.isInterface(classInfo.flags()) - || indexView.getAllKnownImplementors(classInfo.name()).stream() - .anyMatch(MPOpenAPIBuilder::isConcrete); + return openApiConfig(new OpenApiConfigImpl(mpConfig)); } - private static boolean isConcrete(ClassInfo classInfo) { - return !Modifier.isAbstract(classInfo.flags()); + MPOpenAPIBuilder indexPaths(String... indexPaths) { + this.indexPaths = indexPaths; + return identity(); } /** * Creates a {@link io.smallrye.openapi.runtime.scanner.FilteredIndexView} tailored to the specified JAX-RS application. *

- * Use an {@link io.smallrye.openapi.api.OpenApiConfig} instance which (possibly) limits scanning for this application - * by excluding classes that are not "relevant" to the specified application. For our purposes, the classes "relevant" - * to an application are those: + * Use an {@link io.smallrye.openapi.api.OpenApiConfig} instance which (possibly) limits scanning for this application + * by excluding classes that are not "relevant" to the specified application. For our purposes, the classes "relevant" + * to an application are those: *

    *
  • returned by the application's {@code getClasses} method, and
  • *
  • inferred from the objects returned from the application's {@code getSingletons} method.
  • @@ -202,9 +179,9 @@ private static boolean isConcrete(ClassInfo classInfo) { *

    * * @param viewFilteredByConfig filtered index view based only on MP config - * @param jaxRsApplications all JAX-RS applications discovered - * @param jaxRsApp the specific JAX-RS application of interest - * @param ancillaryClassNames names of resource, provider, and feature classes + * @param jaxRsApplications all JAX-RS applications discovered + * @param jaxRsApp the specific JAX-RS application of interest + * @param ancillaryClassNames names of resource, provider, and feature classes * @return the filtered index view suitable for the specified JAX-RS application */ private FilteredIndexView filteredIndexView(FilteredIndexView viewFilteredByConfig, @@ -229,8 +206,8 @@ private FilteredIndexView filteredIndexView(FilteredIndexView viewFilteredByConf if (classesExplicitlyReferenced.isEmpty() && jaxRsApplications.size() == 1) { // No need to do filtering at all. - if (LOGGER.isLoggable(Level.DEBUG)) { - LOGGER.log(Level.DEBUG, String.format( + if (LOGGER.isLoggable(Level.TRACE)) { + LOGGER.log(Level.TRACE, String.format( "No filtering required for %s which reports no explicitly referenced classes and " + "is the only JAX-RS application", appClassName)); @@ -244,54 +221,44 @@ private FilteredIndexView filteredIndexView(FilteredIndexView viewFilteredByConf // Note that the MP OpenAPI TCK does not follow JAX-RS behavior if the application class returns a non-empty set from // getSingletons; in that case, the TCK incorrectly expects the endpoints defined by other resources as well to appear // in the OpenAPI document. - if ((classesFromGetClasses.isEmpty() - && (classesFromGetSingletons.isEmpty() || !useJaxRsSemantics)) + if (( + classesFromGetClasses.isEmpty() + && (classesFromGetSingletons.isEmpty() || !useJaxRsSemantics)) && jaxRsApplications.size() == 1) { - if (LOGGER.isLoggable(Level.DEBUG)) { - LOGGER.log(Level.DEBUG, String.format( - "No filtering required for %s; although it returns a non-empty set from getSingletons, JAX-RS semantics " - + "has been turned off for OpenAPI processing using " + USE_JAXRS_SEMANTICS_FULL_CONFIG_KEY, - appClassName)); + if (LOGGER.isLoggable(Level.TRACE)) { + LOGGER.log(Level.TRACE, String.format(""" + No filtering required for %s; although it returns a non-empty set from getSingletons, JAX-RS semantics \ + has been turned off for OpenAPI processing using %s""", + appClassName, MPOpenAPIBuilder.USE_JAXRS_SEMANTICS_FULL_CONFIG_KEY)); } return viewFilteredByConfig; } - /* - * If the classes to be ignored are A and B, the exclusion regex expression we want for filtering is - * - * ^(A|B)$ - * - * The ^ and $ avoid incorrect prefix/suffix matches. - */ - Pattern excludePattern = Pattern.compile( - classNamesToIgnore(jaxRsApplications, - jaxRsApp, - ancillaryClassNames, - classesExplicitlyReferenced) - .stream() - .map(Pattern::quote) - .collect(Collectors.joining("|", "^(", ")$"))); + Set excludedClasses = classNamesToIgnore(jaxRsApplications, + jaxRsApp, + ancillaryClassNames, + classesExplicitlyReferenced); // Create a new filtered index view for this application which excludes the irrelevant classes we just identified. Its // delegate is the previously-created view based only on the MP configuration. FilteredIndexView result = new FilteredIndexView(viewFilteredByConfig, - new FilteringOpenApiConfigImpl(mpConfig, excludePattern)); - if (LOGGER.isLoggable(Level.DEBUG)) { + new FilteringOpenApiConfigImpl(mpConfig, excludedClasses)); + if (LOGGER.isLoggable(Level.TRACE)) { String knownClassNames = result .getKnownClasses() .stream() .map(ClassInfo::toString) .sorted() .collect(Collectors.joining("," + System.lineSeparator() + " ")); - LOGGER.log(Level.DEBUG, + LOGGER.log(Level.TRACE, String.format("FilteredIndexView for %n" + " application class %s%n" + " with explicitly-referenced classes %s%n" - + " yields exclude pattern: %s%n" + + " yields exclude list: %s%n" + " and known classes: %n %s", appClassName, classesExplicitlyReferenced, - excludePattern, + excludedClasses, knownClassNames)); } @@ -327,88 +294,192 @@ private static Set classNamesToIgnore(List jaxRsApplic return result; } + private static boolean isConcrete(ClassInfo classInfo) { + return !Modifier.isAbstract(classInfo.flags()); + } + private static class FilteringOpenApiConfigImpl extends OpenApiConfigImpl { - private final Pattern classesToExclude; + private final Set classesToExclude; - FilteringOpenApiConfigImpl(Config config, Pattern classesToExclude) { + FilteringOpenApiConfigImpl(org.eclipse.microprofile.config.Config config, Set classesToExclude) { super(config); this.classesToExclude = classesToExclude; } @Override - public Pattern scanExcludeClasses() { + public Set scanExcludeClasses() { return classesToExclude; } } /** - * Sets the OpenApiConfig instance to use in governing the behavior of the - * smallrye OpenApi implementation. + * Builds a list of filtered index views, one for each JAX-RS application, sorted by the Application class name to help + * keep the list of endpoints in the OpenAPI document in a stable order. + *

    + * First, we find all resource, provider, and feature classes present in the index. This is the same for all + * applications. + *

    + *

    + * Each filtered index view is tuned to one JAX-RS application. * - * @param config {@link OpenApiConfig} instance to control OpenAPI behavior - * @return updated builder instance + * @return list of {@code FilteredIndexView}s, one per JAX-RS application */ - private MPOpenAPIBuilder openAPIConfig(OpenApiConfig config) { - this.openAPIConfig = config; - return this; + List buildPerAppFilteredIndexViews() { + + List jaxRsApplications = MpOpenApiFeature.jaxRsApplicationsToRun().stream() + .filter(jaxRsApp -> jaxRsApp.applicationClass().isPresent()) + .sorted(Comparator.comparing(jaxRsApplication -> jaxRsApplication.applicationClass() + .get() + .getName())) + .collect(Collectors.toList()); + + IndexView indexView = indexView(); + + FilteredIndexView viewFilteredByConfig = new FilteredIndexView(indexView, new OpenApiConfigImpl(mpConfig)); + Set ancillaryClassNames = ancillaryClassNames(viewFilteredByConfig); + + /* + * Filter even for a single-application class in case it implements getClasses or getSingletons. + */ + return jaxRsApplications.stream() + .map(jaxRsApp -> filteredIndexView(viewFilteredByConfig, + jaxRsApplications, + jaxRsApp, + ancillaryClassNames)) + .collect(Collectors.toList()); + } + + private static Set ancillaryClassNames(IndexView indexView) { + Set result = new HashSet<>(resourceClassNames(indexView)); + result.addAll(providerClassNames(indexView)); + result.addAll(featureClassNames(indexView)); + if (LOGGER.isLoggable(Level.DEBUG)) { + LOGGER.log(Level.DEBUG, "Ancillary classes: {0}", result); + } + return result; + } + + private static Set resourceClassNames(IndexView indexView) { + return annotatedClassNames(indexView, Path.class); + } + + private static Set providerClassNames(IndexView indexView) { + return annotatedClassNames(indexView, Provider.class); + } + + private static Set featureClassNames(IndexView indexView) { + return annotatedClassNames(indexView, Feature.class); + } + + private static Set annotatedClassNames(IndexView indexView, Class annotationClass) { + // Partially inspired by the SmallRye code. + return indexView + .getAnnotations(DotName.createSimple(annotationClass.getName())) + .stream() + .map(AnnotationInstance::target) + .filter(target -> target.kind() == AnnotationTarget.Kind.CLASS) + .map(AnnotationTarget::asClass) + .filter(classInfo -> hasImplementationOrIsIncluded(indexView, classInfo)) + .map(ClassInfo::toString) + .collect(Collectors.toSet()); + } + + private static boolean hasImplementationOrIsIncluded(IndexView indexView, ClassInfo classInfo) { + // Partially inspired by the SmallRye code. + return !Modifier.isInterface(classInfo.flags()) + || indexView.getAllKnownImplementors(classInfo.name()).stream() + .anyMatch(MPOpenAPIBuilder::isConcrete); } /** - * Assigns various OpenAPI settings from the specified MP OpenAPI {@code Config} object. + * Builds an {@code IndexView} from existing Jandex index file(s) on the classpath. * - * @param mpConfig the OpenAPI {@code Config} object possibly containing settings - * @return updated builder instance + * @return IndexView from all index files + * @throws java.io.IOException in case of error attempting to open an index file */ - @ConfiguredOption(type = OpenApiConfig.class, mergeWithParent = true) - @ConfiguredOption(key = "scan.disable", - type = Boolean.class, - value = "false", - description = "Disable annotation scanning.") - @ConfiguredOption(key = "scan.packages", - type = String.class, - kind = ConfiguredOption.Kind.LIST, - description = "Specify the list of packages to scan.") - @ConfiguredOption(key = "scan.classes", - type = String.class, - kind = ConfiguredOption.Kind.LIST, - description = "Specify the list of classes to scan.") - @ConfiguredOption(key = "scan.exclude.packages", - type = String.class, - kind = ConfiguredOption.Kind.LIST, - description = "Specify the list of packages to exclude from scans.") - @ConfiguredOption(key = "scan.exclude.classes", - type = String.class, - kind = ConfiguredOption.Kind.LIST, - description = "Specify the list of classes to exclude from scans.") - public MPOpenAPIBuilder config(Config mpConfig) { - this.mpConfig = mpConfig; + private IndexView existingIndexFileReader() throws IOException { + List indices = new ArrayList<>(); + /* + * Do not reuse the previously-computed indexURLs; those values will be incorrect with native images. + */ + for (URL indexURL : findIndexFiles(indexPaths)) { + try (InputStream indexIS = indexURL.openStream()) { + LOGGER.log(Level.TRACE, "Adding Jandex index at {0}", indexURL.toString()); + indices.add(new IndexReader(indexIS).read()); + } catch (Exception ex) { + throw new IOException("Attempted to read from previously-located index file " + + indexURL + " but the index cannot be read", ex); + } + } + return indices.size() == 1 ? indices.get(0) : CompositeIndex.create(indices); + } - // use-jaxrs-semantics is intended for Helidon's private use in running the TCKs to work around a problem there. - // We do not document its use. - useJaxRsSemantics = mpConfig - .getOptionalValue(USE_JAXRS_SEMANTICS_FULL_CONFIG_KEY, Boolean.class) - .orElse(USE_JAXRS_SEMANTICS_DEFAULT); - return openAPIConfig(new OpenApiConfigImpl(mpConfig)); + private IndexView indexFromHarvestedClasses() throws IOException { + Indexer indexer = new Indexer(); + annotatedTypes().forEach(c -> addClassToIndexer(indexer, c)); + + /* + * Some apps might be added dynamically, not via annotation processing. Add those classes to the index if they are not + * already present. + */ + MpOpenApiFeature.jaxRsApplicationsToRun().stream() + .map(JaxRsApplication::applicationClass) + .filter(Optional::isPresent) + .forEach(appClassOpt -> addClassToIndexer(indexer, appClassOpt.get())); + + LOGGER.log(Level.TRACE, "Using internal Jandex index created from CDI bean discovery"); + Index result = indexer.complete(); + dumpIndex(Level.DEBUG, result); + return result; } - MPOpenAPIBuilder singleIndexViewSupplier(Supplier singleIndexViewSupplier) { - this.singleIndexViewSupplier = singleIndexViewSupplier; - return this; + private void addClassToIndexer(Indexer indexer, Class c) { + try (InputStream is = MpOpenApiFeature.contextClassLoader().getResourceAsStream(resourceNameForClass(c))) { + indexer.index(is); + } catch (IOException ex) { + throw new RuntimeException(String.format("Cannot load bytecode from class %s at %s for annotation processing", + c.getName(), resourceNameForClass(c)), ex); + } } - @Override - protected Supplier> indexViewsSupplier() { - return this::buildPerAppFilteredIndexViews; + private static void dumpIndex(Level level, Index index) { + if (LOGGER.isLoggable(level)) { + LOGGER.log(level, "Dump of internal Jandex index:"); + PrintStream oldStdout = System.out; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (PrintStream newPS = new PrintStream(baos, true, Charset.defaultCharset())) { + System.setOut(newPS); + index.printAnnotations(); + index.printSubclasses(); + LOGGER.log(level, baos.toString(Charset.defaultCharset())); + } finally { + System.setOut(oldStdout); + } + } } - @Override - public MPOpenAPIBuilder validate() throws IllegalStateException { - super.validate(); - if (openAPIConfig == null) { - throw new IllegalStateException("OpenApiConfig has not been set in MPBuilder"); + private static String resourceNameForClass(Class c) { + return c.getName().replace('.', '/') + ".class"; + } + + private List findIndexFiles(String... indexPaths) { + List result = new ArrayList<>(); + for (String indexPath : indexPaths) { + Enumeration urls = null; + try { + urls = MpOpenApiFeature.contextClassLoader().getResources(indexPath); + } catch (IOException e) { + throw new RuntimeException(e); + } + while (urls.hasMoreElements()) { + result.add(urls.nextElement()); + } } - Objects.requireNonNull(singleIndexViewSupplier, "singleIndexViewSupplier must be set but was not"); - return this; + return result; + } + + private Set> annotatedTypes() { + return CDI.current().getBeanManager().getExtension(OpenApiCdiExtension.class).annotatedTypes(); } } diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MpOpenApiFeature.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MpOpenApiFeature.java new file mode 100644 index 00000000000..d6bde0bbef9 --- /dev/null +++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MpOpenApiFeature.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.openapi; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import io.helidon.common.LazyValue; +import io.helidon.microprofile.server.JaxRsApplication; +import io.helidon.microprofile.server.JaxRsCdiExtension; +import io.helidon.openapi.OpenApiFeature; + +import io.smallrye.openapi.api.OpenApiConfig; +import io.smallrye.openapi.api.OpenApiDocument; +import io.smallrye.openapi.api.models.OpenAPIImpl; +import io.smallrye.openapi.api.util.MergeUtil; +import io.smallrye.openapi.runtime.OpenApiProcessor; +import io.smallrye.openapi.runtime.OpenApiStaticFile; +import io.smallrye.openapi.runtime.io.Format; +import io.smallrye.openapi.runtime.scanner.FilteredIndexView; +import io.smallrye.openapi.runtime.scanner.OpenApiAnnotationScanner; +import jakarta.enterprise.inject.spi.CDI; +import org.eclipse.microprofile.openapi.models.OpenAPI; +import org.jboss.jandex.IndexView; + +/** + * MP variant of OpenApiFeature. + */ +public class MpOpenApiFeature extends OpenApiFeature { + + /** + * Creates a new builder for the MP OpenAPI feature. + * + * @return new builder + */ + static MPOpenAPIBuilder builder() { + return new MPOpenAPIBuilder(); + } + + /** + * Parser helper. + */ + static final LazyValue PARSER_HELPER = LazyValue.create(ParserHelper::create); + + /** + * Returns the {@code JaxRsApplication} instances that should be run, according to the JAX-RS CDI extension. + * + * @return List of JaxRsApplication instances that should be run + */ + static List jaxRsApplicationsToRun() { + JaxRsCdiExtension ext = CDI.current() + .getBeanManager() + .getExtension(JaxRsCdiExtension.class); + + return ext.applicationsToRun(); + } + + private static final System.Logger LOGGER = System.getLogger(MpOpenApiFeature.class.getName()); + + private final Supplier> filteredIndexViewsSupplier; + + private final Lock modelAccess = new ReentrantLock(true); + + private final OpenApiConfig openApiConfig; + private final io.helidon.openapi.OpenApiStaticFile openApiStaticFile; + + private final MPOpenAPIBuilder builder; + private OpenAPI model; + + private final Map, ExpandedTypeDescription> implsToTypes; + + protected MpOpenApiFeature(MPOpenAPIBuilder builder) { + super(LOGGER, builder); + this.builder = builder; + implsToTypes = buildImplsToTypes(); + openApiConfig = builder.openApiConfig(); + openApiStaticFile = builder.staticFile(); + filteredIndexViewsSupplier = builder::buildPerAppFilteredIndexViews; + } + + @Override + protected String openApiContent(OpenAPIMediaType openApiMediaType) { + + return openApiContent(openApiMediaType, model()); + } + + /** + * Triggers preparation of the model from external code. + */ + protected void prepareModel() { + model(); + } + + /** + * Returns the current thread's context class loader. + * + * @return class loader in use by the thread + */ + static ClassLoader contextClassLoader() { + return Thread.currentThread().getContextClassLoader(); + } + + // For testing + IndexView indexView() { + return builder.indexView(); + } + + Map, ExpandedTypeDescription> buildImplsToTypes() { + return Collections.unmodifiableMap(PARSER_HELPER.get().types() + .values() + .stream() + .collect(Collectors.toMap(ExpandedTypeDescription::impl, + Function.identity()))); + } + + + private String openApiContent(OpenAPIMediaType openAPIMediaType, OpenAPI model) { + StringWriter sw = new StringWriter(); + Serializer.serialize(PARSER_HELPER.get().types(), implsToTypes, model, openAPIMediaType, sw); + return sw.toString(); + } + + /** + * Prepares the OpenAPI model that later will be used to create the OpenAPI + * document for endpoints in this application. + * + * @param config {@code OpenApiConfig} object describing paths, servers, etc. + * @param staticFile the static file, if any, to be included in the resulting model + * @param filteredIndexViews possibly empty list of FilteredIndexViews to use in harvesting definitions from the code + * @return the OpenAPI model + * @throws RuntimeException in case of errors reading any existing static + * OpenAPI document + */ + private OpenAPI prepareModel(OpenApiConfig config, OpenApiStaticFile staticFile, + List filteredIndexViews) { + try { + // The write lock guarding the model has already been acquired. + OpenApiDocument.INSTANCE.reset(); + OpenApiDocument.INSTANCE.config(config); + OpenApiDocument.INSTANCE.modelFromReader(OpenApiProcessor.modelFromReader(config, contextClassLoader())); + if (staticFile != null) { + OpenApiDocument.INSTANCE.modelFromStaticFile(OpenApiParser.parse(PARSER_HELPER.get().types(), + staticFile.getContent())); + } + if (isAnnotationProcessingEnabled(config)) { + expandModelUsingAnnotations(config, filteredIndexViews); + } else { + LOGGER.log(System.Logger.Level.TRACE, "OpenAPI Annotation processing is disabled"); + } + OpenApiDocument.INSTANCE.filter(OpenApiProcessor.getFilter(config, contextClassLoader())); + OpenApiDocument.INSTANCE.initialize(); + OpenAPIImpl instance = OpenAPIImpl.class.cast(OpenApiDocument.INSTANCE.get()); + + // Create a copy, primarily to avoid problems during unit testing. + // The SmallRye MergeUtil omits the openapi value, so we need to set it explicitly. + return MergeUtil.merge(new OpenAPIImpl(), instance) + .openapi(instance.getOpenapi()); + } catch (IOException ex) { + throw new RuntimeException("Error initializing OpenAPI information", ex); + } + } + + + private static Format toFormat(OpenAPIMediaType openAPIMediaType) { + return openAPIMediaType.equals(OpenAPIMediaType.YAML) + ? Format.YAML + : Format.JSON; + } + + private boolean isAnnotationProcessingEnabled(OpenApiConfig config) { + return !config.scanDisable(); + } + + private void expandModelUsingAnnotations(OpenApiConfig config, List filteredIndexViews) { + if (filteredIndexViews.isEmpty() || config.scanDisable()) { + return; + } + + /* + * Conduct a SmallRye OpenAPI annotation scan for each filtered index view, merging the resulting OpenAPI models into one. + * The AtomicReference is effectively final so we can update the actual reference from inside the lambda. + */ + AtomicReference aggregateModelRef = new AtomicReference<>(new OpenAPIImpl()); // Start with skeletal model + filteredIndexViews.forEach(filteredIndexView -> { + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(config, filteredIndexView, + List.of(new HelidonAnnotationScannerExtension())); + OpenAPI modelForApp = scanner.scan(); + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + + LOGGER.log(System.Logger.Level.DEBUG, String.format("Intermediate model from filtered index view %s:%n%s", + filteredIndexView.getKnownClasses(), + openApiContent(OpenAPIMediaType.YAML, modelForApp))); + } + aggregateModelRef.set( + MergeUtil.merge(aggregateModelRef.get(), modelForApp) + .openapi(modelForApp.getOpenapi())); // SmallRye's merge skips openapi value. + + }); + OpenApiDocument.INSTANCE.modelFromAnnotations(aggregateModelRef.get()); + } + + private OpenAPI model() { + return access(() -> { + if (model == null) { + model = prepareModel(openApiConfig, toSmallRye(openApiStaticFile), filteredIndexViewsSupplier.get()); + } + return model; + }); + } + + private static OpenApiStaticFile toSmallRye(io.helidon.openapi.OpenApiStaticFile staticFile) { + + return staticFile == null + ? null + : new OpenApiStaticFile( + new BufferedInputStream( + new ByteArrayInputStream(staticFile.content() + .getBytes(Charset.defaultCharset()))), + toFormat(staticFile.openApiMediaType())); + } + + private T access(Supplier operation) { + modelAccess.lock(); + try { + return operation.get(); + } finally { + modelAccess.unlock(); + } + } +} diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiCdiExtension.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiCdiExtension.java index 77b18f470c1..b3b205526ae 100644 --- a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiCdiExtension.java +++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiCdiExtension.java @@ -15,39 +15,23 @@ */ package io.helidon.microprofile.openapi; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.PrintStream; -import java.lang.System.Logger.Level; -import java.net.URL; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Enumeration; import java.util.HashSet; -import java.util.List; -import java.util.Optional; import java.util.Set; +import java.util.function.Function; import io.helidon.config.Config; -import io.helidon.microprofile.cdi.RuntimeStart; -import io.helidon.microprofile.server.JaxRsApplication; -import io.helidon.microprofile.server.RoutingBuilders; -import io.helidon.nima.openapi.OpenApiService; +import io.helidon.microprofile.servicecommon.HelidonRestCdiExtension; +import io.helidon.openapi.OpenApiFeature; import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.Initialized; import jakarta.enterprise.event.Observes; -import jakarta.enterprise.inject.spi.Extension; import jakarta.enterprise.inject.spi.ProcessAnnotatedType; -import org.jboss.jandex.CompositeIndex; -import org.jboss.jandex.Index; -import org.jboss.jandex.IndexReader; -import org.jboss.jandex.IndexView; -import org.jboss.jandex.Indexer; +import jakarta.enterprise.inject.spi.ProcessManagedBean; +import org.eclipse.microprofile.config.ConfigProvider; -import static jakarta.interceptor.Interceptor.Priority.LIBRARY_BEFORE; import static jakarta.interceptor.Interceptor.Priority.PLATFORM_AFTER; /** @@ -55,20 +39,30 @@ * SmallRye OpenAPI) from CDI if no {@code META-INF/jandex.idx} file exists on * the class path. */ -public class OpenApiCdiExtension implements Extension { - - private static final String INDEX_PATH = "META-INF/jandex.idx"; +public class OpenApiCdiExtension extends HelidonRestCdiExtension { private static final System.Logger LOGGER = System.getLogger(OpenApiCdiExtension.class.getName()); - private final String[] indexPaths; - private final int indexURLCount; + /** + * Normal location of Jandex index files. + */ + static final String INDEX_PATH = "META-INF/jandex.idx"; - private final Set> annotatedTypes = new HashSet<>(); - private org.eclipse.microprofile.config.Config mpConfig; - private Config config; - private MPOpenAPISupport openApiSupport; + private static Function featureFactory(String... indexPaths) { + return (Config helidonConfig) -> { + + org.eclipse.microprofile.config.Config mpConfig = ConfigProvider.getConfig(); + + MPOpenAPIBuilder builder = MpOpenApiFeature.builder() + .config(helidonConfig) + .indexPaths(indexPaths) + .config(mpConfig); + return builder.build(); + }; + } + + private final Set> annotatedTypes = new HashSet<>(); /** * Creates a new instance of the index builder. @@ -80,156 +74,39 @@ public OpenApiCdiExtension() throws IOException { } OpenApiCdiExtension(String... indexPaths) throws IOException { - this.indexPaths = indexPaths; - List indexURLs = findIndexFiles(indexPaths); - indexURLCount = indexURLs.size(); - if (indexURLs.isEmpty()) { - LOGGER.log(Level.INFO, () -> String.format( - "OpenAPI support could not locate the Jandex index file %s " - + "so will build an in-memory index.%n" - + "This slows your app start-up and, depending on CDI configuration, " - + "might omit some type information needed for a complete OpenAPI document.%n" - + "Consider using the Jandex maven plug-in during your build " - + "to create the index and add it to your app.", - INDEX_PATH)); - } + super(LOGGER, featureFactory(indexPaths), OpenApiFeature.Builder.CONFIG_KEY); } - private void configure(@Observes @RuntimeStart Config config) { - this.mpConfig = (org.eclipse.microprofile.config.Config) config; - this.config = config; + @Override + protected void processManagedBean(ProcessManagedBean processManagedBean) { + // SmallRye handles annotation processing. We have this method because the abstract superclass requires it. } - void registerOpenApi(@Observes @Priority(LIBRARY_BEFORE + 10) @Initialized(ApplicationScoped.class) Object event) { - Config openapiNode = config.get(OpenApiService.Builder.CONFIG_KEY); - openApiSupport = new MPOpenAPIBuilder() - .config(mpConfig) - .singleIndexViewSupplier(this::indexView) - .config(openapiNode) - .build(); - - openApiSupport - .configureEndpoint(RoutingBuilders.create(openapiNode).routingBuilder()); - } // Must run after the server has created the Application instances. void buildModel(@Observes @Priority(PLATFORM_AFTER + 100 + 10) @Initialized(ApplicationScoped.class) Object event) { - openApiSupport.prepareModel(); + serviceSupport().prepareModel(); } - /** - * Records each type that is annotated unless Jandex index(es) were found on - * the classpath (in which case we do not need to build our own in memory). - * - * @param annotated type - * @param event {@code ProcessAnnotatedType} event - */ - private void processAnnotatedType(@Observes ProcessAnnotatedType event) { - if (indexURLCount == 0) { - Class c = event.getAnnotatedType() - .getJavaClass(); - annotatedTypes.add(c); - } + // For testing + MpOpenApiFeature feature() { + return serviceSupport(); } - /** - * Reports an {@link org.jboss.jandex.IndexView} for the Jandex index that describes - * annotated classes for endpoints. - * - * @return {@code IndexView} describing discovered classes - */ - public IndexView indexView() { - try { - return indexURLCount > 0 ? existingIndexFileReader() : indexFromHarvestedClasses(); - } catch (IOException e) { - // wrap so we can use this method in a reference - throw new RuntimeException(e); - } + + Set> annotatedTypes() { + return annotatedTypes; } /** - * Builds an {@code IndexView} from existing Jandex index file(s) on the classpath. + * Records each type that is annotated. * - * @return IndexView from all index files - * @throws IOException in case of error attempting to open an index file + * @param annotated type + * @param event {@code ProcessAnnotatedType} event */ - private IndexView existingIndexFileReader() throws IOException { - List indices = new ArrayList<>(); - /* - * Do not reuse the previously-computed indexURLs; those values will be incorrect with native images. - */ - for (URL indexURL : findIndexFiles(indexPaths)) { - try (InputStream indexIS = indexURL.openStream()) { - LOGGER.log(Level.DEBUG, "Adding Jandex index at {0}", indexURL.toString()); - indices.add(new IndexReader(indexIS).read()); - } catch (Exception ex) { - throw new IOException("Attempted to read from previously-located index file " - + indexURL + " but the index cannot be read", ex); - } - } - return indices.size() == 1 ? indices.get(0) : CompositeIndex.create(indices); - } - - private IndexView indexFromHarvestedClasses() throws IOException { - Indexer indexer = new Indexer(); - annotatedTypes.forEach(c -> addClassToIndexer(indexer, c)); - - /* - * Some apps might be added dynamically, not via annotation processing. Add those classes to the index if they are not - * already present. - */ - MPOpenAPIBuilder.jaxRsApplicationsToRun().stream() - .map(JaxRsApplication::applicationClass) - .filter(Optional::isPresent) - .forEach(appClassOpt -> addClassToIndexer(indexer, appClassOpt.get())); - - LOGGER.log(Level.DEBUG, "Using internal Jandex index created from CDI bean discovery"); - Index result = indexer.complete(); - dumpIndex(Level.TRACE, result); - return result; - } - - private void addClassToIndexer(Indexer indexer, Class c) { - try (InputStream is = contextClassLoader().getResourceAsStream(resourceNameForClass(c))) { - indexer.index(is); - } catch (IOException ex) { - throw new RuntimeException(String.format("Cannot load bytecode from class %s at %s for annotation processing", - c.getName(), resourceNameForClass(c)), ex); - } - } - - private List findIndexFiles(String... indexPaths) throws IOException { - List result = new ArrayList<>(); - for (String indexPath : indexPaths) { - Enumeration urls = contextClassLoader().getResources(indexPath); - while (urls.hasMoreElements()) { - result.add(urls.nextElement()); - } - } - return result; - } - - private static void dumpIndex(Level level, Index index) { - if (LOGGER.isLoggable(level)) { - LOGGER.log(level, "Dump of internal Jandex index:"); - PrintStream oldStdout = System.out; - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (PrintStream newPS = new PrintStream(baos, true, Charset.defaultCharset())) { - System.setOut(newPS); - index.printAnnotations(); - index.printSubclasses(); - LOGGER.log(level, baos.toString(Charset.defaultCharset())); - } finally { - System.setOut(oldStdout); - } - } - } - - private static ClassLoader contextClassLoader() { - return Thread.currentThread().getContextClassLoader(); - } - - private static String resourceNameForClass(Class c) { - return c.getName().replace('.', '/') + ".class"; + private void processAnnotatedType(@Observes ProcessAnnotatedType event) { + Class c = event.getAnnotatedType() + .getJavaClass(); + annotatedTypes.add(c); } } diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiParser.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiParser.java new file mode 100644 index 00000000000..98fc7c03d0f --- /dev/null +++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiParser.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.openapi; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import org.eclipse.microprofile.openapi.models.OpenAPI; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.TypeDescription; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.representer.Representer; + +/** + * Abstraction for SnakeYAML parsing of JSON and YAML. + */ +final class OpenApiParser { + + private OpenApiParser() { + } + + /** + * Parse open API. + * + * @param types types + * @param inputStream input stream to parse from + * @return parsed document + * @throws IOException in case of I/O problems + */ + static OpenAPI parse(Map, ExpandedTypeDescription> types, InputStream inputStream) throws IOException { + try (Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { + return parse(types, OpenAPI.class, reader); + } + } + + /** + * Parse YAML or JSON using the specified types, returning the specified type with input taken from the indicated reader. + * + * @param types type descriptions + * @param tClass POJO type to be parsed + * @param reader {@code Reader} containing the JSON or YAML input + * @return the parsed object + * @param the type to be returned + */ + static T parse(Map, ExpandedTypeDescription> types, Class tClass, Reader reader) { + return parse(types, tClass, reader, new Representer(new DumperOptions())); + } + + /** + * Parse YAML or JSON using the specified types, returning the indicated type with input from the specified reader. + * + * @param types type descriptions + * @param tClass POJO type to be parsed + * @param reader {@code Reader} containing the JSON or YAML input + * @param representer the {@code Representer} to use during parsing + * @return the parsed object + * @param the type to be returned + */ + static T parse(Map, ExpandedTypeDescription> types, Class tClass, Reader reader, Representer representer) { + TypeDescription td = types.get(tClass); + Constructor topConstructor = new CustomConstructor(td); + types.values() + .forEach(topConstructor::addTypeDescription); + + Yaml yaml = new Yaml(topConstructor, representer); + return yaml.loadAs(reader, tClass); + } +} diff --git a/nima/openapi/src/main/java/io/helidon/nima/openapi/OpenApiService.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiService.java.save similarity index 98% rename from nima/openapi/src/main/java/io/helidon/nima/openapi/OpenApiService.java rename to microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiService.java.save index 39babafdb22..c9dbae4daaf 100644 --- a/nima/openapi/src/main/java/io/helidon/nima/openapi/OpenApiService.java +++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiService.java.save @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.nima.openapi; +package io.helidon.microprofile.openapi; import java.io.BufferedInputStream; import java.io.IOException; @@ -52,11 +52,6 @@ import io.helidon.nima.webserver.http.HttpService; import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; -import io.helidon.openapi.ExpandedTypeDescription; -import io.helidon.openapi.OpenAPIMediaType; -import io.helidon.openapi.OpenAPIParser; -import io.helidon.openapi.ParserHelper; -import io.helidon.openapi.Serializer; import io.helidon.openapi.internal.OpenAPIConfigImpl; import io.smallrye.openapi.api.OpenApiConfig; @@ -77,7 +72,7 @@ import jakarta.json.JsonString; import jakarta.json.JsonValue; import org.eclipse.microprofile.openapi.models.OpenAPI; -import org.jboss.jandex.IndexView; +import io.smallrye.jandex.IndexView; import static io.helidon.nima.webserver.cors.CorsEnabledServiceHelper.CORS_CONFIG_KEY; @@ -513,7 +508,7 @@ public OpenApiService build() { } /** - * Base builder for OpenAPI service builders, extended by {@link io.helidon.nima.openapi.OpenApiService.Builder} + * Base builder for OpenAPI service builders, extended by {@link OpenApiService.Builder} * and MicroProfile implementation. * * @param type of the builder (subclass) @@ -553,7 +548,7 @@ public B config(Config config) { config.get("static-file") .asString() .ifPresent(this::staticFile); - config.get(CORS_CONFIG_KEY) + config.get(CorsEnabledServiceHelper.CORS_CONFIG_KEY) .as(CrossOriginConfig::create) .ifPresent(this::crossOriginConfig); return identity(); @@ -605,7 +600,7 @@ public B staticFile(String path) { * @param crossOriginConfig {@code CrossOriginConfig} containing CORS set-up * @return updated builder instance */ - @ConfiguredOption(key = CORS_CONFIG_KEY) + @ConfiguredOption(key = CorsEnabledServiceHelper.CORS_CONFIG_KEY) public B crossOriginConfig(CrossOriginConfig crossOriginConfig) { Objects.requireNonNull(crossOriginConfig, "CrossOriginConfig must be non-null"); this.crossOriginConfig = crossOriginConfig; diff --git a/openapi/src/main/java/io/helidon/openapi/ParserHelper.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/ParserHelper.java similarity index 95% rename from openapi/src/main/java/io/helidon/openapi/ParserHelper.java rename to microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/ParserHelper.java index babc8f6edb7..cd3107c76ee 100644 --- a/openapi/src/main/java/io/helidon/openapi/ParserHelper.java +++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/ParserHelper.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.openapi; +package io.helidon.microprofile.openapi; import java.util.List; import java.util.Map; @@ -29,9 +29,9 @@ import org.yaml.snakeyaml.TypeDescription; /** - * Wraps generated parser and uses {@link io.helidon.openapi.ExpandedTypeDescription} as its type. + * Wraps generated parser and uses {@link ExpandedTypeDescription} as its type. */ -public class ParserHelper { +class ParserHelper { // Temporary to suppress SnakeYAML warnings. // As a static we keep a reference to the logger, thereby making sure any changes we make are persistent. (JUL holds @@ -46,6 +46,7 @@ public class ParserHelper { private ParserHelper(SnakeYAMLParserHelper generatedHelper) { this.generatedHelper = generatedHelper; + adjustTypeDescriptions(generatedHelper.types()); } /** @@ -53,13 +54,12 @@ private ParserHelper(SnakeYAMLParserHelper generatedHel * * @return a new parser helper */ - public static ParserHelper create() { + static ParserHelper create() { boolean warningsEnabled = Boolean.getBoolean("openapi.parsing.warnings.enabled"); if (SNAKE_YAML_INTROSPECTOR_LOGGER.isLoggable(java.util.logging.Level.WARNING) && !warningsEnabled) { SNAKE_YAML_INTROSPECTOR_LOGGER.setLevel(java.util.logging.Level.SEVERE); } ParserHelper helper = new ParserHelper(SnakeYAMLParserHelper.create(ExpandedTypeDescription::create)); - adjustTypeDescriptions(helper.types()); return helper; } diff --git a/openapi/src/main/java/io/helidon/openapi/Serializer.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/Serializer.java similarity index 80% rename from openapi/src/main/java/io/helidon/openapi/Serializer.java rename to microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/Serializer.java index 514c3c2891d..051500bfedb 100644 --- a/openapi/src/main/java/io/helidon/openapi/Serializer.java +++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/Serializer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.openapi; +package io.helidon.microprofile.openapi; import java.io.PrintWriter; import java.io.Writer; @@ -26,8 +26,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import io.helidon.openapi.OpenApiFeature; + import io.smallrye.openapi.api.models.OpenAPIImpl; -import io.smallrye.openapi.runtime.io.Format; import org.eclipse.microprofile.openapi.models.Extensible; import org.eclipse.microprofile.openapi.models.OpenAPI; import org.eclipse.microprofile.openapi.models.Reference; @@ -76,15 +77,15 @@ private Serializer() { * @param types types * @param implsToTypes implementations to types * @param openAPI Open API document to serialize - * @param fmt format to use + * @param openAPIMediaType OpenAPI media type to use * @param writer writer to serialize to */ public static void serialize(Map, ExpandedTypeDescription> types, Map, ExpandedTypeDescription> implsToTypes, OpenAPI openAPI, - Format fmt, + OpenApiFeature.OpenAPIMediaType openAPIMediaType, Writer writer) { - if (fmt == Format.JSON) { + if (openAPIMediaType.equals(OpenApiFeature.OpenAPIMediaType.JSON)) { serialize(types, implsToTypes, openAPI, writer, JSON_DUMPER_OPTIONS, DumperOptions.ScalarStyle.DOUBLE_QUOTED); } else { serialize(types, implsToTypes, openAPI, writer, YAML_DUMPER_OPTIONS, DumperOptions.ScalarStyle.PLAIN); @@ -141,6 +142,22 @@ protected Node representSequence(Tag tag, Iterable sequence, DumperOptions.Fl return result; } + @Override + protected Node representMapping(Tag tag, Map mapping, DumperOptions.FlowStyle flowStyle) { + Node result = super.representMapping(tag, mapping, flowStyle); + if (mapping instanceof Extensible extensible) { + List tuples = ((MappingNode) result).getValue(); + if (extensible.getExtensions() != null) { + extensible.getExtensions().forEach((k, v) -> { + NodeTuple extensionTuple = new NodeTuple(new ScalarNode(Tag.STR, k, null, null, stringStyle), + represent(v)); + tuples.add(extensionTuple); + }); + } + } + return result; + } + private boolean isExemptedFromQuotes(Tag tag) { return tag.equals(Tag.BINARY) || tag.equals(Tag.BOOL) || tag.equals(Tag.FLOAT) || tag.equals(Tag.INT); @@ -206,8 +223,19 @@ private Object adjustPropertyValue(Object propertyValue) { @Override protected MappingNode representJavaBean(Set properties, Object javaBean) { + /* + * First, let SnakeYAML prepare the node normally. If the JavaBean is Extensible and has extension properties, the + * will contain a subnode called "extensions" which itself has one or more subnodes, one for each extension + * property assigned. + */ MappingNode result = super.representJavaBean(properties, javaBean); + + /* + * Now promote the individual subnodes for each extension property (if any) up one level so that they are peers of the + * other properties. Also remove the "extensions" node. + */ processExtensions(result, javaBean); + /* * Clearing representedObjects is an awkward but effective way of preventing SnakeYAML from using anchors and * aliases, which apparently the Jackson parser used in the TCK (as of this writing) does not handle properly. @@ -221,31 +249,45 @@ private void processExtensions(MappingNode node, Object javaBean) { return; } - List tuples = new ArrayList<>(node.getValue()); + List tuples = node.getValue(); if (tuples.isEmpty()) { return; } + List updatedTuples = processExtensions(tuples); + node.setValue(updatedTuples); + } + + /** + * Returns a (possibly) updated list of {@code NodeTuple} objects, promoting any node under the node {@code extensions} + * one level higher and removing the {@code extensions} node itself. + * + * @param tuples {@code NodeTuple} objects to process + * @return possibly updated list of node tuples + */ + private List processExtensions(List tuples) { List updatedTuples = new ArrayList<>(); tuples.forEach(tuple -> { - Node keyNode = tuple.getKeyNode(); - if (keyNode.getTag().equals(Tag.STR)) { - String key = ((ScalarNode) keyNode).getValue(); - if (key.equals(EXTENSIONS)) { - Node valueNode = tuple.getValueNode(); - if (valueNode.getNodeId().equals(NodeId.mapping)) { - MappingNode extensions = MappingNode.class.cast(valueNode); - updatedTuples.addAll(extensions.getValue()); - } - } else { - updatedTuples.add(tuple); - } - } else { + // Returns either null (if the tuple is not for "extensions") or a list of tuples for each extension property. + List extensionTuples = processExtensions(tuple); + if (extensionTuples == null) { updatedTuples.add(tuple); + } else { + updatedTuples.addAll(extensionTuples); } }); - node.setValue(updatedTuples); + return updatedTuples; + } + + private List processExtensions(NodeTuple tuple) { + Node keyNode = tuple.getKeyNode(); + return keyNode.getTag().equals(Tag.STR) + && ((ScalarNode) keyNode).getValue().equals(EXTENSIONS) + && tuple.getValueNode() instanceof MappingNode extensions + && extensions.getNodeId().equals(NodeId.mapping) + ? extensions.getValue() + : null; } /** diff --git a/microprofile/openapi/src/main/java/module-info.java b/microprofile/openapi/src/main/java/module-info.java index e2ae5e65ea0..aa68d39bb98 100644 --- a/microprofile/openapi/src/main/java/module-info.java +++ b/microprofile/openapi/src/main/java/module-info.java @@ -33,15 +33,22 @@ requires smallrye.open.api.core; + requires java.desktop; // for java.beans package + requires microprofile.config.api; requires io.helidon.microprofile.server; requires io.helidon.openapi; - requires io.helidon.nima.openapi; + requires org.jboss.jandex; + + requires org.yaml.snakeyaml; + requires transitive microprofile.openapi.api; - requires org.jboss.jandex; + // logging required for SnakeYAML logging workaround + requires java.logging; requires static io.helidon.config.metadata; + requires io.helidon.microprofile.servicecommon; exports io.helidon.microprofile.openapi; diff --git a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/BasicServerTest.java b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/BasicServerTest.java index e59943d7f4b..b70d186837b 100644 --- a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/BasicServerTest.java +++ b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/BasicServerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import io.helidon.common.http.Http; import io.helidon.microprofile.tests.junit5.AddBean; import io.helidon.microprofile.tests.junit5.HelidonTest; -import io.helidon.nima.openapi.OpenApiService; +import io.helidon.openapi.OpenApiFeature; import jakarta.inject.Inject; import jakarta.ws.rs.client.WebTarget; @@ -48,8 +48,8 @@ public class BasicServerTest { private static Map retrieveYaml(WebTarget webTarget) { try (Response response = webTarget - .path(OpenApiService.DEFAULT_WEB_CONTEXT) - .request(OpenApiService.DEFAULT_RESPONSE_MEDIA_TYPE.text()) + .path(OpenApiFeature.DEFAULT_CONTEXT) + .request(OpenApiFeature.DEFAULT_RESPONSE_MEDIA_TYPE.text()) .get()) { assertThat("Fetch of OpenAPI document from server status", response.getStatus(), is(equalTo(Http.Status.OK_200.code()))); diff --git a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestMultiJandex.java b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestMultiJandex.java index d61f9bcdf8f..1c92aaf8621 100644 --- a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestMultiJandex.java +++ b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestMultiJandex.java @@ -22,11 +22,13 @@ import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; +@Disabled public class TestMultiJandex { @Test @@ -36,8 +38,9 @@ public void testMultipleIndexFiles() throws IOException { * The pom builds two differently-named test Jandex files, as an approximation * to handling multiple same-named index files in the class path. */ - OpenApiCdiExtension builder = new OpenApiCdiExtension("META-INF/jandex.idx", "META-INF/other.idx"); - IndexView indexView = builder.indexView(); + OpenApiCdiExtension ext = new OpenApiCdiExtension("META-INF/jandex.idx", "META-INF/other.idx"); + IndexView indexView = ext.feature().indexView(); + DotName testAppName = DotName.createSimple(TestApp.class.getName()); DotName testApp2Name = DotName.createSimple(TestApp2.class.getName()); diff --git a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestUtil.java b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestUtil.java index 3864be36efa..514f03566ae 100644 --- a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestUtil.java +++ b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestUtil.java @@ -79,7 +79,7 @@ public static void cleanup(Server server, HttpURLConnection cnx) { /** * Returns a {@code HttpURLConnection} for the requested method and path and - * {code @MediaType} from the specified {@link io.helidon.reactive.webserver.WebServer}. + * {code @MediaType} from the specified location. * * @param port port to connect to * @param method HTTP method to use in building the connection diff --git a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityCdiExtension.java b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityCdiExtension.java index fa230b0bb77..4982bf5c1d7 100644 --- a/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityCdiExtension.java +++ b/microprofile/security/src/main/java/io/helidon/microprofile/security/SecurityCdiExtension.java @@ -17,8 +17,6 @@ import java.lang.System.Logger.Level; import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; import java.util.concurrent.atomic.AtomicReference; import io.helidon.common.context.Contexts; @@ -163,8 +161,7 @@ private void registerSecurity(@Observes @Priority(LIBRARY_BEFORE) @Initialized(A this.security.set(security); } - private CompletionStage failingAtnProvider(ProviderRequest request) { - return CompletableFuture - .completedFuture(AuthenticationResponse.failed("No provider configured")); + private AuthenticationResponse failingAtnProvider(ProviderRequest request) { + return AuthenticationResponse.failed("No provider configured"); } } diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsService.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsService.java index a6140a5063d..c22b27d0e83 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsService.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/JaxRsService.java @@ -62,6 +62,7 @@ import org.glassfish.jersey.server.ContainerRequest; import org.glassfish.jersey.server.ContainerResponse; import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.ServerProperties; import org.glassfish.jersey.server.spi.Container; import org.glassfish.jersey.server.spi.ContainerResponseWriter; @@ -97,6 +98,12 @@ static JaxRsService create(ResourceConfig resourceConfig, InjectionManager injec resourceConfig.property(PROVIDER_DEFAULT_DISABLE, "ALL"); resourceConfig.property(WADL_FEATURE_DISABLE, "true"); + // TODO - temporary until MP OpenAPI TCK bug fix released. https://github.com/eclipse/microprofile-open-api/issues/557 + if (System.getProperties().containsKey("io.helidon." + ServerProperties.RESOURCE_VALIDATION_IGNORE_ERRORS)) { + resourceConfig.property(ServerProperties.RESOURCE_VALIDATION_IGNORE_ERRORS, + Boolean.getBoolean("io.helidon." + ServerProperties.RESOURCE_VALIDATION_IGNORE_ERRORS)); + } + InjectionManager ij = injectionManager == null ? null : new InjectionManagerWrapper(injectionManager, resourceConfig); ApplicationHandler appHandler = new ApplicationHandler(resourceConfig, new WebServerBinder(), diff --git a/microprofile/telemetry/pom.xml b/microprofile/telemetry/pom.xml index 5c0ef80c6c3..fef4c9e663b 100644 --- a/microprofile/telemetry/pom.xml +++ b/microprofile/telemetry/pom.xml @@ -49,26 +49,10 @@ io.opentelemetry opentelemetry-context - - io.opentelemetry - opentelemetry-exporter-otlp - - - io.opentelemetry - opentelemetry-exporter-zipkin - - - io.opentelemetry - opentelemetry-exporter-jaeger - io.opentelemetry opentelemetry-sdk-extension-autoconfigure - - io.opentelemetry.instrumentation - opentelemetry-instrumentation-api - io.helidon.microprofile.server helidon-microprofile-server @@ -103,6 +87,22 @@ jakarta.activation-api provided + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.tests + helidon-microprofile-tests-junit5 + test + diff --git a/microprofile/telemetry/src/test/java/io/helidon/microprofile/telemetry/AgentDetectorTest.java b/microprofile/telemetry/src/test/java/io/helidon/microprofile/telemetry/AgentDetectorTest.java new file mode 100644 index 00000000000..17f3750783d --- /dev/null +++ b/microprofile/telemetry/src/test/java/io/helidon/microprofile/telemetry/AgentDetectorTest.java @@ -0,0 +1,65 @@ +package io.helidon.microprofile.telemetry;/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.helidon.config.Config; +import io.helidon.microprofile.server.ServerCdiExtension; +import io.helidon.microprofile.tests.junit5.AddConfig; +import io.helidon.microprofile.tests.junit5.AddExtension; +import io.helidon.microprofile.tests.junit5.HelidonTest; +import io.helidon.tracing.opentelemetry.HelidonOpenTelemetry; +import jakarta.enterprise.inject.spi.CDI; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + + +/** + * Check Agent Detector working correctly. + */ +@HelidonTest(resetPerTest = true) +@AddExtension(ServerCdiExtension.class) +class AgentDetectorTest { + + public static final String OTEL_AGENT_PRESENT = "otel.agent.present"; + public static final String IO_OPENTELEMETRY_JAVAAGENT = "io.opentelemetry.javaagent"; + + @Test + @AddConfig(key = OTEL_AGENT_PRESENT, value = "true") + void shouldBeNoOpTelemetry(){ + Config config = CDI.current().select(Config.class).get(); + boolean present = HelidonOpenTelemetry.AgentDetector.isAgentPresent(config); + assertThat(present, is(true)); + } + + @Test + @AddConfig(key = OTEL_AGENT_PRESENT, value = "false") + void shouldNotBeNoOpTelemetry(){ + Config config = CDI.current().select(Config.class).get(); + boolean present = HelidonOpenTelemetry.AgentDetector.isAgentPresent(config); + assertThat(present, is(false)); + } + + @Test + void checkEnvVariable(){ + System.setProperty(IO_OPENTELEMETRY_JAVAAGENT, "true"); + Config config = CDI.current().select(Config.class).get(); + boolean present = HelidonOpenTelemetry.AgentDetector.isAgentPresent(config); + assertThat(present, is(true)); + } +} diff --git a/microprofile/tests/tck/tck-graphql/pom.xml b/microprofile/tests/tck/tck-graphql/pom.xml index 5d45a9caccd..04061b66de5 100644 --- a/microprofile/tests/tck/tck-graphql/pom.xml +++ b/microprofile/tests/tck/tck-graphql/pom.xml @@ -91,7 +91,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/microprofile/tests/tck/tck-openapi/pom.xml b/microprofile/tests/tck/tck-openapi/pom.xml index f5cb6d53773..bf07387bb70 100644 --- a/microprofile/tests/tck/tck-openapi/pom.xml +++ b/microprofile/tests/tck/tck-openapi/pom.xml @@ -109,9 +109,14 @@ org.eclipse.microprofile.openapi:microprofile-openapi-tck false - + false + + + true diff --git a/microprofile/tests/tck/tck-telemetry/pom.xml b/microprofile/tests/tck/tck-telemetry/pom.xml index 20c4115fce7..76fe30f5bdc 100644 --- a/microprofile/tests/tck/tck-telemetry/pom.xml +++ b/microprofile/tests/tck/tck-telemetry/pom.xml @@ -40,6 +40,11 @@ arquillian-testng-container test + + io.opentelemetry + opentelemetry-exporter-otlp + test + io.helidon.microprofile.telemetry helidon-microprofile-telemetry diff --git a/nima/http/media/media/src/main/java/io/helidon/nima/http/media/FormParamsSupport.java b/nima/http/media/media/src/main/java/io/helidon/nima/http/media/FormParamsSupport.java index 63ed4719a83..9ef7eac3862 100644 --- a/nima/http/media/media/src/main/java/io/helidon/nima/http/media/FormParamsSupport.java +++ b/nima/http/media/media/src/main/java/io/helidon/nima/http/media/FormParamsSupport.java @@ -65,7 +65,7 @@ public static MediaSupport create() { @Override public ReaderResponse reader(GenericType type, Headers requestHeaders) { - if (!type.equals(Parameters.GENERIC_TYPE)) { + if (!Parameters.class.isAssignableFrom(type.rawType())) { return ReaderResponse.unsupported(); } @@ -89,7 +89,7 @@ public WriterResponse writer(GenericType type, Headers requestHeaders, WritableHeaders responseHeaders) { - if (!type.equals(Parameters.GENERIC_TYPE)) { + if (!Parameters.class.isAssignableFrom(type.rawType())) { return WriterResponse.unsupported(); } @@ -112,7 +112,7 @@ public WriterResponse writer(GenericType type, public ReaderResponse reader(GenericType type, Headers requestHeaders, Headers responseHeaders) { - if (!type.equals(Parameters.GENERIC_TYPE)) { + if (!Parameters.class.isAssignableFrom(type.rawType())) { return ReaderResponse.unsupported(); } @@ -133,7 +133,7 @@ public ReaderResponse reader(GenericType type, @Override public WriterResponse writer(GenericType type, WritableHeaders requestHeaders) { - if (!type.equals(Parameters.GENERIC_TYPE)) { + if (!Parameters.class.isAssignableFrom(type.rawType())) { return WriterResponse.unsupported(); } return requestHeaders.contentType() diff --git a/nima/http/media/multipart/src/main/java/io/helidon/nima/http/media/multipart/MultiPartImpl.java b/nima/http/media/multipart/src/main/java/io/helidon/nima/http/media/multipart/MultiPartImpl.java index 36fd23396dd..cedd0599837 100644 --- a/nima/http/media/multipart/src/main/java/io/helidon/nima/http/media/multipart/MultiPartImpl.java +++ b/nima/http/media/multipart/src/main/java/io/helidon/nima/http/media/multipart/MultiPartImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/nima/http/media/multipart/src/main/java/io/helidon/nima/http/media/multipart/MultiPartReader.java b/nima/http/media/multipart/src/main/java/io/helidon/nima/http/media/multipart/MultiPartReader.java index 4346d7fa6d0..179ab9109b2 100644 --- a/nima/http/media/multipart/src/main/java/io/helidon/nima/http/media/multipart/MultiPartReader.java +++ b/nima/http/media/multipart/src/main/java/io/helidon/nima/http/media/multipart/MultiPartReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/nima/http/processor/src/main/java/io/helidon/nima/http/processor/HttpMethodCreator.java b/nima/http/processor/src/main/java/io/helidon/nima/http/processor/HttpMethodCreator.java index 3fd8ccfde88..85dfa5132a4 100644 --- a/nima/http/processor/src/main/java/io/helidon/nima/http/processor/HttpMethodCreator.java +++ b/nima/http/processor/src/main/java/io/helidon/nima/http/processor/HttpMethodCreator.java @@ -29,7 +29,7 @@ import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNameDefault; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; import io.helidon.pico.tools.CustomAnnotationTemplateRequest; import io.helidon.pico.tools.CustomAnnotationTemplateResponse; import io.helidon.pico.tools.GenericTemplateCreator; @@ -100,7 +100,7 @@ public Optional create(CustomAnnotationTemplat } private Map addProperties(CustomAnnotationTemplateRequest request) { - TypedElementName targetElement = request.targetElement(); + TypedElementInfo targetElement = request.targetElement(); Map response = new HashMap<>(); HttpDef http = new HttpDef(); @@ -121,10 +121,10 @@ private Map addProperties(CustomAnnotationTemplateRequest reques // http.params (full string) List headerList = new LinkedList<>(); - List elementArgs = request.targetElementArgs(); + List elementArgs = request.targetElement().parameterArguments(); LinkedList parameters = new LinkedList<>(); int headerCount = 1; - for (TypedElementName elementArg : elementArgs) { + for (TypedElementInfo elementArg : elementArgs) { String type = elementArg.typeName().name(); switch (type) { @@ -171,7 +171,7 @@ private void processParameter(HttpDef httpDef, LinkedList parameters, List headerList, String type, - TypedElementName elementArg) { + TypedElementInfo elementArg) { // depending on annotations List annotations = elementArg.annotations(); if (annotations.size() == 0) { diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ConnectionWriter.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ConnectionWriter.java index 998a3febee8..fa8e4ec35bb 100755 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ConnectionWriter.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ConnectionWriter.java @@ -74,19 +74,62 @@ public int writeHeaders(Http2Headers headers, int streamId, Http2Flag.HeaderFlag // we must enforce parallelism of exactly 1, to make sure the dynamic table is updated // and then immediately written + int maxFrameSize = flowControl.maxFrameSize(); + return withStreamLock(() -> { int written = 0; headerBuffer.clear(); headers.write(outboundDynamicTable, responseHuffman, headerBuffer); - Http2FrameHeader frameHeader = Http2FrameHeader.create(headerBuffer.available(), - Http2FrameTypes.HEADERS, - flags, - streamId); + + // Fast path when headers fits within the SETTINGS_MAX_FRAME_SIZE + if (headerBuffer.available() <= maxFrameSize) { + Http2FrameHeader frameHeader = Http2FrameHeader.create(headerBuffer.available(), + Http2FrameTypes.HEADERS, + flags, + streamId); + written += frameHeader.length(); + written += Http2FrameHeader.LENGTH; + + noLockWrite(new Http2FrameData(frameHeader, headerBuffer)); + return written; + } + + // Split header frame to smaller continuation frames RFC 9113 §6.10 + BufferData[] fragments = Http2Headers.split(headerBuffer, maxFrameSize); + + // First header fragment + BufferData fragment = fragments[0]; + Http2FrameHeader frameHeader; + frameHeader = Http2FrameHeader.create(fragment.available(), + Http2FrameTypes.HEADERS, + Http2Flag.HeaderFlags.create(0), + streamId); written += frameHeader.length(); written += Http2FrameHeader.LENGTH; + noLockWrite(new Http2FrameData(frameHeader, fragment)); + + // Header continuation fragments in the middle + for (int i = 1; i < fragments.length; i++) { + fragment = fragments[i]; + frameHeader = Http2FrameHeader.create(fragment.available(), + Http2FrameTypes.CONTINUATION, + Http2Flag.ContinuationFlags.create(0), + streamId); + written += frameHeader.length(); + written += Http2FrameHeader.LENGTH; + noLockWrite(new Http2FrameData(frameHeader, fragment)); + } - noLockWrite(new Http2FrameData(frameHeader, headerBuffer)); - + // Last header continuation fragment + fragment = fragments[fragments.length - 1]; + frameHeader = Http2FrameHeader.create(fragment.available(), + Http2FrameTypes.CONTINUATION, + // Last fragment needs to indicate the end of headers + Http2Flag.ContinuationFlags.create(flags.value() | Http2Flag.END_OF_HEADERS), + streamId); + written += frameHeader.length(); + written += Http2FrameHeader.LENGTH; + noLockWrite(new Http2FrameData(frameHeader, fragment)); return written; }); } @@ -104,17 +147,8 @@ public int writeHeaders(Http2Headers headers, return withStreamLock(() -> { int bytesWritten = 0; - headerBuffer.clear(); - headers.write(outboundDynamicTable, responseHuffman, headerBuffer); - bytesWritten += headerBuffer.available(); - - Http2FrameHeader frameHeader = Http2FrameHeader.create(headerBuffer.available(), - Http2FrameTypes.HEADERS, - flags, - streamId); - bytesWritten += Http2FrameHeader.LENGTH; + bytesWritten += writeHeaders(headers, streamId, flags, flowControl); - noLockWrite(new Http2FrameData(frameHeader, headerBuffer)); writeData(dataFrame, flowControl); bytesWritten += Http2FrameHeader.LENGTH; bytesWritten += dataFrame.header().length(); @@ -151,6 +185,8 @@ private T withStreamLock(Callable callable) { } finally { streamLock.unlock(); } + } catch (RuntimeException e) { + throw e; } catch (Exception e) { throw new RuntimeException(e); } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ErrorCode.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ErrorCode.java index 17051735c3c..59888524995 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ErrorCode.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ErrorCode.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -96,7 +96,12 @@ public enum Http2ErrorCode { * The endpoint requires that HTTP/1.1 be used * instead of HTTP/2. */ - HTTP_1_1_REQUIRED(0xd); + HTTP_1_1_REQUIRED(0xd), + /** + * Request header fields are too large. + * RFC6585 + */ + REQUEST_HEADER_FIELDS_TOO_LARGE(431); private static final Map BY_CODE; diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Headers.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Headers.java index e9ed7ac5513..4e057140f2f 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Headers.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Headers.java @@ -440,6 +440,27 @@ public void write(DynamicTable table, Http2HuffmanEncoder huffman, BufferData gr } } + static BufferData[] split(BufferData bufferData, int size) { + int length = bufferData.available(); + if (length <= size) { + return new BufferData[]{bufferData}; + } + + int lastFragmentSize = length % size; + // Avoid creating 0 length last fragment + int allFrames = (length / size) + (lastFragmentSize != 0 ? 1 : 0); + BufferData[] result = new BufferData[allFrames]; + + for (int i = 0; i < allFrames; i++) { + boolean lastFrame = (allFrames == i + 1); + byte[] frag = new byte[lastFrame ? (lastFragmentSize != 0 ? lastFragmentSize : size) : size]; + bufferData.read(frag); + result[i] = BufferData.create(frag); + } + + return result; + } + private static Http2Headers create(ServerRequestHeaders httpHeaders, PseudoHeaders pseudoHeaders) { return new Http2Headers(httpHeaders, pseudoHeaders); } diff --git a/nima/http2/http2/src/test/java/io/helidon/nima/http2/MaxFrameSizeSplitTest.java b/nima/http2/http2/src/test/java/io/helidon/nima/http2/MaxFrameSizeSplitTest.java index badecf1fa0a..acc08d096b9 100644 --- a/nima/http2/http2/src/test/java/io/helidon/nima/http2/MaxFrameSizeSplitTest.java +++ b/nima/http2/http2/src/test/java/io/helidon/nima/http2/MaxFrameSizeSplitTest.java @@ -24,6 +24,7 @@ import io.helidon.logging.common.LogConfig; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -65,6 +66,15 @@ private static Stream splitMultiple() { ); } + @Test + void splitHeaders() { + BufferData bf = BufferData.create("This is so long text!"); + BufferData[] split = Http2Headers.split(bf, 12); + assertThat(split.length, is(2)); + assertThat(split[0].available(), is(12)); + assertThat(split[1].available(), is(9)); + } + @ParameterizedTest @MethodSource void splitMultiple(SplitTest args) { diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java index 91f76a98fb8..0ac29291ddc 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java @@ -57,12 +57,15 @@ class ClientRequestImpl implements Http2ClientRequest { private final int maxFrameSize; private final long maxHeaderListSize; private final int connectionPrefetch; + private final Map properties; - private WritableHeaders explicitHeaders = WritableHeaders.create(); + private WritableHeaders explicitHeaders; private Tls tls; private int priority; private boolean priorKnowledge; + private boolean followRedirects; private int requestPrefetch = 0; + private int maxRedirects; private ClientConnection explicitConnection; private Duration flowControlTimeout = Duration.ofMillis(100); private Duration timeout = Duration.ofSeconds(10); @@ -83,8 +86,12 @@ class ClientRequestImpl implements Http2ClientRequest { this.maxFrameSize = client.maxFrameSize(); this.maxHeaderListSize = client.maxHeaderListSize(); this.connectionPrefetch = client.prefetch(); + this.properties = client.properties(); this.tls = tls == null || !tls.enabled() ? null : tls; this.query = query; + this.followRedirects = client.followRedirects(); + this.maxRedirects = client.maxRedirects(); + this.explicitHeaders = WritableHeaders.create(client.defaultHeaders()); } @Override @@ -194,6 +201,18 @@ public Http2ClientRequest connection(ClientConnection connection) { return this; } + @Override + public Http2ClientRequest skipUriEncoding() { + this.uri.skipUriEncoding(true); + return this; + } + + @Override + public Http2ClientRequest property(String propertyName, String propertyValue) { + properties.put(propertyName, propertyValue); + return this; + } + @Override public Http2ClientRequest priority(int priority) { if (priority < 1 || priority > 256) { @@ -233,6 +252,20 @@ public Http2ClientRequest fragment(String fragment) { return this; } + @Override + public Http2ClientRequest followRedirects(boolean followRedirects) { +// this.followRedirects = followRedirects; +// return this; + throw new UnsupportedOperationException("Not supported in HTTP2 yet"); + } + + @Override + public Http2ClientRequest maxRedirects(int maxRedirects) { +// this.maxRedirects = maxRedirects; +// return this; + throw new UnsupportedOperationException("Not supported in HTTP2 yet"); + } + UriHelper uriHelper() { return uri; } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2Client.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2Client.java index 91f999ca3fd..2cfbf93d1cf 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2Client.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2Client.java @@ -129,7 +129,7 @@ public Http2ClientBuilder prefetch(int prefetch) { } @Override - public Http2Client build() { + public Http2Client doBuild() { return new Http2ClientImpl(this); } diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java index 64044f5e65f..6dcffadf3d3 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java @@ -42,11 +42,11 @@ public interface Http2Config { /** * The maximum field section size that the sender is prepared to accept in bytes. * See RFC 9113 section 6.5.2 for details. - * Default is maximal unsigned int. + * Default is 8192. * * @return maximal header list size in bytes */ - @ConfiguredOption("0xFFFFFFFFL") + @ConfiguredOption("8192") long maxHeaderListSize(); /** diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java index 8e8ab3922fa..01191a830e9 100755 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java @@ -16,6 +16,8 @@ package io.helidon.nima.http2.webserver; +import java.io.UncheckedIOException; +import java.net.SocketException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -165,7 +167,9 @@ public void handle() throws InterruptedException { sendErrorDetails ? e.getMessage() : ""); connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create())); state = State.FINISHED; - } catch (CloseConnectionException | InterruptedException e) { + } catch (CloseConnectionException + | InterruptedException + | UncheckedIOException e) { throw e; } catch (Throwable e) { if (state == State.FINISHED) { @@ -400,11 +404,10 @@ private void readFrame() { private void doContinuation() { Http2Flag.ContinuationFlags flags = frameHeader.flags(Http2FrameTypes.CONTINUATION); - List continuationData = stream(frameHeader.streamId()).contData(); - if (continuationData.isEmpty()) { - throw new Http2Exception(Http2ErrorCode.PROTOCOL, "Received continuation without headers."); - } - continuationData.add(new Http2FrameData(frameHeader, inProgressFrame())); + + stream(frameHeader.streamId()) + .addContinuation(new Http2FrameData(frameHeader, inProgressFrame())); + if (flags.endOfHeaders()) { state = State.HEADERS; } else { @@ -489,7 +492,7 @@ private void ackSettings() { stream.headers(upgradeHeaders, !hasEntity); upgradeHeaders = null; ctx.executor() - .submit(new StreamRunnable(streams, stream, stream.streamId())); + .submit(new StreamRunnable(streams, stream, Thread.currentThread())); } } @@ -544,9 +547,7 @@ private void doHeaders() { // first frame, expecting continuation if (frameHeader.type() == Http2FrameType.HEADERS && !frameHeader.flags(Http2FrameTypes.HEADERS).endOfHeaders()) { // this needs to retain the data until we receive last continuation, cannot use the same data - streamContext.contData().clear(); - streamContext.contData().add(new Http2FrameData(frameHeader, inProgressFrame().copy())); - streamContext.continuationHeader = frameHeader; + streamContext.addHeadersToBeContinued(frameHeader, inProgressFrame().copy()); this.continuationExpectedStreamId = streamId; this.state = State.READ_FRAME; return; @@ -559,14 +560,12 @@ private void doHeaders() { if (frameHeader.type() == Http2FrameType.CONTINUATION) { // end of continuations with header frames - List frames = streamContext.contData(); headers = Http2Headers.create(stream, requestDynamicTable, requestHuffman, - frames.toArray(new Http2FrameData[0])); - endOfStream = streamContext.continuationHeader.flags(Http2FrameTypes.HEADERS).endOfStream(); - frames.clear(); - streamContext.continuationHeader = null; + streamContext.contData()); + endOfStream = streamContext.contHeader().flags(Http2FrameTypes.HEADERS).endOfStream(); + streamContext.clearContinuations(); continuationExpectedStreamId = 0; } else { endOfStream = frameHeader.flags(Http2FrameTypes.HEADERS).endOfStream(); @@ -592,7 +591,7 @@ private void doHeaders() { // we now have all information needed to execute ctx.executor() - .submit(new StreamRunnable(streams, stream, stream.streamId())); + .submit(new StreamRunnable(streams, stream, Thread.currentThread())); } private void pingFrame() { @@ -716,6 +715,7 @@ private StreamContext stream(int streamId) { } streamContext = new StreamContext(streamId, + http2Config.maxHeaderListSize(), new Http2Stream(ctx, routing, http2Config, @@ -768,33 +768,41 @@ private enum State { UNKNOWN } - private static final class StreamRunnable implements Runnable { - private final Map streams; - private final Http2Stream stream; - private final int streamId; - - private StreamRunnable(Map streams, Http2Stream stream, int streamId) { - this.streams = streams; - this.stream = stream; - this.streamId = streamId; - } + private record StreamRunnable(Map streams, + Http2Stream stream, + Thread handlerThread) implements Runnable { @Override public void run() { - stream.run(); - streams.remove(stream.streamId()); + try { + stream.run(); + } catch (UncheckedIOException e) { + // Broken connection + if (e.getCause() instanceof SocketException) { + // Interrupt handler thread + handlerThread.interrupt(); + LOGGER.log(DEBUG, "Socket error on writer thread", e); + } else { + throw e; + } + } finally { + streams.remove(stream.streamId()); + } } } private static class StreamContext { private final List continuationData = new ArrayList<>(); + private final long maxHeaderListSize; private final int streamId; private final Http2Stream stream; + private long headerListSize = 0; private Http2FrameHeader continuationHeader; - StreamContext(int streamId, Http2Stream stream) { + StreamContext(int streamId, long maxHeaderListSize, Http2Stream stream) { this.streamId = streamId; + this.maxHeaderListSize = maxHeaderListSize; this.stream = stream; } @@ -802,12 +810,41 @@ public Http2Stream stream() { return stream; } - public Http2FrameHeader contHeader() { + Http2FrameData[] contData() { + return continuationData.toArray(new Http2FrameData[0]); + } + + Http2FrameHeader contHeader() { return continuationHeader; } - public List contData() { - return continuationData; + void addContinuation(Http2FrameData frameData) { + if (continuationData.isEmpty()) { + throw new Http2Exception(Http2ErrorCode.PROTOCOL, "Received continuation without headers."); + } + this.continuationData.add(frameData); + addAndValidateHeaderListSize(frameData.header().length()); + } + + void addHeadersToBeContinued(Http2FrameHeader frameHeader, BufferData bufferData) { + clearContinuations(); + continuationHeader = frameHeader; + this.continuationData.add(new Http2FrameData(frameHeader, bufferData)); + addAndValidateHeaderListSize(frameHeader.length()); + } + + private void addAndValidateHeaderListSize(int headerSizeIncrement){ + // Check MAX_HEADER_LIST_SIZE + headerListSize += headerSizeIncrement; + if (headerListSize > maxHeaderListSize){ + throw new Http2Exception(Http2ErrorCode.REQUEST_HEADER_FIELDS_TOO_LARGE, + "Request Header Fields Too Large"); + } + } + + private void clearContinuations() { + continuationData.clear(); + headerListSize = 0; } } } diff --git a/nima/openapi/etc/spotbugs/exclude.xml b/nima/openapi/etc/spotbugs/exclude.xml index a8fa29ce0e4..39fd88b1de3 100644 --- a/nima/openapi/etc/spotbugs/exclude.xml +++ b/nima/openapi/etc/spotbugs/exclude.xml @@ -1,7 +1,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + true + + + org.apache.maven.plugins maven-compiler-plugin diff --git a/nima/openapi/src/main/java/io/helidon/nima/openapi/SeOpenApiFeature.java b/nima/openapi/src/main/java/io/helidon/nima/openapi/SeOpenApiFeature.java new file mode 100644 index 00000000000..fa1f17b6d35 --- /dev/null +++ b/nima/openapi/src/main/java/io/helidon/nima/openapi/SeOpenApiFeature.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.nima.openapi; + +import io.helidon.openapi.OpenApiFeature; + +/** + * SE implementation of {@link OpenApiFeature}. + */ +public class SeOpenApiFeature extends OpenApiFeature { + + private static final System.Logger LOGGER = System.getLogger(SeOpenApiFeature.class.getName()); + + /** + * Create a new builder for the feature. + * + * @return the new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a new instance. + * + * @param builder builder for the SE OpenAPI feature + */ + protected SeOpenApiFeature(Builder builder) { + super(LOGGER, builder); + } + + @Override + protected String openApiContent(io.helidon.openapi.OpenApiFeature.OpenAPIMediaType openApiMediaType) { + // TODO temporarily supports only static files + if (staticContent().isPresent()) { + return staticContent().get().content(); + } + return null; + } + + /** + * Builder class for the SE OpenAPI feature. + */ + public static class Builder extends OpenApiFeature.Builder { + + private static final System.Logger LOGGER = System.getLogger(Builder.class.getName()); + + @Override + public SeOpenApiFeature build() { + return new SeOpenApiFeature(this); + } + + @Override + protected System.Logger logger() { + return LOGGER; + } + } +} diff --git a/nima/openapi/src/main/java/module-info.java b/nima/openapi/src/main/java/module-info.java index 1175dcd76e6..b94beb168ce 100644 --- a/nima/openapi/src/main/java/module-info.java +++ b/nima/openapi/src/main/java/module-info.java @@ -36,10 +36,6 @@ requires io.helidon.cors; requires io.helidon.openapi; - requires smallrye.open.api.core; - requires org.jboss.jandex; - requires org.yaml.snakeyaml; - requires static io.helidon.config.metadata; requires io.helidon.nima.webserver; requires jakarta.json; diff --git a/nima/openapi/src/test/java/io/helidon/nima/openapi/ServerModelReaderTest.java b/nima/openapi/src/test/java/io/helidon/nima/openapi/ServerModelReaderTest.java deleted file mode 100644 index 903cd606bb2..00000000000 --- a/nima/openapi/src/test/java/io/helidon/nima/openapi/ServerModelReaderTest.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.nima.openapi; - -import java.net.HttpURLConnection; - -import io.helidon.common.media.type.MediaTypes; -import io.helidon.config.Config; -import io.helidon.config.ConfigSources; -import io.helidon.nima.openapi.test.MyModelReader; -import io.helidon.nima.webserver.WebServer; - -import jakarta.json.JsonException; -import jakarta.json.JsonString; -import jakarta.json.JsonStructure; -import jakarta.json.JsonValue; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Makes sure that the app-supplied model reader participates in constructing - * the OpenAPI model. - */ -public class ServerModelReaderTest { - - private static final String SIMPLE_PROPS_PATH = "/openapi"; - - private static final OpenApiService.Builder OPENAPI_SUPPORT_BUILDER = - OpenApiService.builder() - .config(Config.create(ConfigSources.classpath("simple.properties")).get(OpenApiService.Builder.CONFIG_KEY)); - - private static WebServer webServer; - - @BeforeAll - public static void startup() { - webServer = TestUtil.startServer(OPENAPI_SUPPORT_BUILDER); - } - - @AfterAll - public static void shutdown() { - TestUtil.shutdownServer(webServer); - } - - @Test - @Disabled - public void checkCustomModelReader() throws Exception { - HttpURLConnection cnx = TestUtil.getURLConnection( - webServer.port(), - "GET", - SIMPLE_PROPS_PATH, - MediaTypes.APPLICATION_OPENAPI_JSON); - TestUtil.validateResponseMediaType(cnx, MediaTypes.APPLICATION_OPENAPI_JSON); - JsonStructure json = TestUtil.jsonFromResponse(cnx); - // The model reader adds the following key/value (among others) to the model. - JsonValue v = json.getValue(String.format("/paths/%s/get/summary", - TestUtil.escapeForJsonPointer(MyModelReader.MODEL_READER_PATH))); - if (v.getValueType().equals(JsonValue.ValueType.STRING)) { - JsonString s = (JsonString) v; - assertEquals(MyModelReader.SUMMARY, s.getString(), - "Unexpected summary value as added by model reader"); - } - } - - @Test - public void makeSureFilteredPathIsMissing() throws Exception { - HttpURLConnection cnx = TestUtil.getURLConnection( - webServer.port(), - "GET", - SIMPLE_PROPS_PATH, - MediaTypes.APPLICATION_OPENAPI_JSON); - TestUtil.validateResponseMediaType(cnx, MediaTypes.APPLICATION_OPENAPI_JSON); - JsonStructure json = TestUtil.jsonFromResponse(cnx); - /* - * Although the model reader adds this path, the filter should have - * removed it. - */ - final JsonException ex = assertThrows( - JsonException.class, - () -> { - JsonValue v = json.getValue(String.format("/paths/%s/get/summary", - TestUtil.escapeForJsonPointer(MyModelReader.DOOMED_PATH))); - }); - assertTrue(ex.getMessage().contains( - String.format("contains no mapping for the name '%s'", MyModelReader.DOOMED_PATH))); - } -} diff --git a/nima/openapi/src/test/java/io/helidon/nima/openapi/ServerTest.java b/nima/openapi/src/test/java/io/helidon/nima/openapi/ServerTest.java deleted file mode 100644 index 390817ba9c4..00000000000 --- a/nima/openapi/src/test/java/io/helidon/nima/openapi/ServerTest.java +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.nima.openapi; - -import java.net.HttpURLConnection; -import java.util.ArrayList; -import java.util.Map; -import java.util.function.Consumer; - -import io.helidon.common.media.type.MediaType; -import io.helidon.common.media.type.MediaTypes; -import io.helidon.config.Config; -import io.helidon.config.ConfigSources; -import io.helidon.nima.webserver.WebServer; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Starts a server with the default OpenAPI endpoint to test a static OpenAPI - * document file in various ways. - */ -class ServerTest { - - private static WebServer greetingWebServer; - private static WebServer timeWebServer; - - private static final String GREETING_PATH = "/openapi-greeting"; - private static final String TIME_PATH = "/openapi-time"; - - private static final Config OPENAPI_CONFIG_DISABLED_CORS = Config.create( - ConfigSources.classpath("serverNoCORS.properties").build()).get(OpenApiService.Builder.CONFIG_KEY); - - private static final Config OPENAPI_CONFIG_RESTRICTED_CORS = Config.create( - ConfigSources.classpath("serverCORSRestricted.yaml").build()).get(OpenApiService.Builder.CONFIG_KEY); - - static final OpenApiService.Builder GREETING_OPENAPI_SUPPORT_BUILDER - = OpenApiService.builder() - .staticFile("src/test/resources/openapi-greeting.yml") - .webContext(GREETING_PATH) - .config(OPENAPI_CONFIG_DISABLED_CORS); - - static final OpenApiService.Builder TIME_OPENAPI_SUPPORT_BUILDER - = OpenApiService.builder() - .staticFile("src/test/resources/openapi-time-server.yml") - .webContext(TIME_PATH) - .config(OPENAPI_CONFIG_RESTRICTED_CORS); - - public ServerTest() { - } - - @BeforeAll - static void startup() { - greetingWebServer = TestUtil.startServer(GREETING_OPENAPI_SUPPORT_BUILDER); - timeWebServer = TestUtil.startServer(TIME_OPENAPI_SUPPORT_BUILDER); - } - - @AfterAll - static void shutdown() { - TestUtil.shutdownServer(greetingWebServer); - TestUtil.shutdownServer(timeWebServer); - } - - - /** - * Accesses the OpenAPI endpoint, requesting a YAML response payload, and - * makes sure that navigating among the YAML yields what we expect. - * - * @throws Exception in case of errors sending the request or reading the - * response - */ - @SuppressWarnings("unchecked") - @Test - void testGreetingAsYAML() throws Exception { - HttpURLConnection cnx = TestUtil.getURLConnection( - greetingWebServer.port(), - "GET", - GREETING_PATH, - MediaTypes.APPLICATION_OPENAPI_YAML); - Map openAPIDocument = TestUtil.yamlFromResponse(cnx); - - ArrayList> servers = TestUtil.as( - ArrayList.class, openAPIDocument.get("servers")); - Map server = servers.get(0); - assertEquals("http://localhost:8000", server.get("url"), "unexpected URL"); - assertEquals("Local test server", server.get("description"), "unexpected description"); - - Map paths = TestUtil.as(Map.class, openAPIDocument.get("paths")); - Map setGreetingPath = TestUtil.as(Map.class, paths.get("/greet/greeting")); - Map put = TestUtil.as(Map.class, setGreetingPath.get("put")); - assertEquals("Sets the greeting prefix", put.get("summary")); - Map requestBody = TestUtil.as(Map.class, put.get("requestBody")); - assertTrue(Boolean.class.cast(requestBody.get("required"))); - Map content = TestUtil.as(Map.class, requestBody.get("content")); - Map applicationJson = TestUtil.as(Map.class, content.get("application/json")); - Map schema = TestUtil.as(Map.class, applicationJson.get("schema")); - - assertEquals("object", schema.get("type")); - } - - /** - * Tests the OpenAPI support by converting the response payload as YAML and - * then creating a {@code Config} instance from that YAML for ease of - * accessing its values in the test. - * - * @throws Exception in case of errors sending the request or receiving the - * response - */ - @Test - void testGreetingAsConfig() throws Exception { - HttpURLConnection cnx = TestUtil.getURLConnection( - greetingWebServer.port(), - "GET", - GREETING_PATH, - MediaTypes.APPLICATION_OPENAPI_YAML); - Config c = TestUtil.configFromResponse(cnx); - assertEquals("Sets the greeting prefix", - TestUtil.fromConfig(c, "paths./greet/greeting.put.summary")); - assertEquals("string", - TestUtil.fromConfig(c, - "paths./greet/greeting.put.requestBody.content." - + "application/json.schema.properties.greeting.type")); - } - - /** - * Makes sure that the response content type is consistent with the Accept - * media type. - * - * @throws Exception in case of errors sending the request or receiving the - * response - */ - @Test - void checkExplicitResponseMediaTypeViaHeaders() throws Exception { - connectAndConsumePayload(MediaTypes.APPLICATION_OPENAPI_YAML); - connectAndConsumePayload(MediaTypes.APPLICATION_YAML); - connectAndConsumePayload(MediaTypes.APPLICATION_OPENAPI_JSON); - connectAndConsumePayload(MediaTypes.APPLICATION_JSON); - } - - @Test - void checkExplicitResponseMediaTypeViaQueryParameter() throws Exception { - TestUtil.connectAndConsumePayload(greetingWebServer.port(), - GREETING_PATH, - "format=JSON", - MediaTypes.APPLICATION_JSON); - - TestUtil.connectAndConsumePayload(greetingWebServer.port(), - GREETING_PATH, - "format=YAML", - MediaTypes.APPLICATION_OPENAPI_YAML); - } - - /** - * Makes sure that the response is correct if the request specified no - * explicit Accept. - * - * @throws Exception error sending the request or receiving the response - */ - @Test - void checkDefaultResponseMediaType() throws Exception { - connectAndConsumePayload(null); - } - - @Test - void testTimeAsConfig() throws Exception { - commonTestTimeAsConfig(null); - } - - @Test - void testTimeUnrestrictedCors() throws Exception { - commonTestTimeAsConfig(cnx -> { - - cnx.setRequestProperty("Origin", "http://foo.bar"); - cnx.setRequestProperty("Host", "localhost"); - }); - - } - - private void commonTestTimeAsConfig(Consumer headerSetter) throws Exception { - HttpURLConnection cnx = TestUtil.getURLConnection( - timeWebServer.port(), - "GET", - TIME_PATH, - MediaTypes.APPLICATION_OPENAPI_YAML); - if (headerSetter != null) { - headerSetter.accept(cnx); - } - Config c = TestUtil.configFromResponse(cnx); - assertEquals("Returns the current time", - TestUtil.fromConfig(c, "paths./timecheck.get.summary")); - assertEquals("string", - TestUtil.fromConfig(c, - "paths./timecheck.get.responses.200.content." - + "application/json.schema.properties.message.type")); - } - - @Test - void ensureNoCrosstalkAmongPorts() throws Exception { - HttpURLConnection timeCnx = TestUtil.getURLConnection( - timeWebServer.port(), - "GET", - TIME_PATH, - MediaTypes.APPLICATION_OPENAPI_YAML); - HttpURLConnection greetingCnx = TestUtil.getURLConnection( - greetingWebServer.port(), - "GET", - GREETING_PATH, - MediaTypes.APPLICATION_OPENAPI_YAML); - Config greetingConfig = TestUtil.configFromResponse(greetingCnx); - Config timeConfig = TestUtil.configFromResponse(timeCnx); - assertFalse(timeConfig.get("paths./greet/greeting.put.summary").exists(), - "Incorrectly found greeting-related item in time OpenAPI document"); - assertFalse(greetingConfig.get("paths./timecheck.get.summary").exists(), - "Incorrectly found time-related item in greeting OpenAPI document"); - } - - private static void connectAndConsumePayload(MediaType mt) throws Exception { - TestUtil.connectAndConsumePayload(greetingWebServer.port(), GREETING_PATH, mt); - } -} diff --git a/nima/openapi/src/test/java/io/helidon/nima/openapi/TestCors.java b/nima/openapi/src/test/java/io/helidon/nima/openapi/TestCors.java deleted file mode 100644 index e190849fa42..00000000000 --- a/nima/openapi/src/test/java/io/helidon/nima/openapi/TestCors.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.nima.openapi; - -import java.net.HttpURLConnection; - -import io.helidon.common.http.Http; -import io.helidon.common.media.type.MediaTypes; -import io.helidon.config.Config; -import io.helidon.nima.webserver.WebServer; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import static io.helidon.nima.openapi.ServerTest.GREETING_OPENAPI_SUPPORT_BUILDER; -import static io.helidon.nima.openapi.ServerTest.TIME_OPENAPI_SUPPORT_BUILDER; -import static org.junit.jupiter.api.Assertions.assertEquals; - -@Disabled -class TestCors { - - private static WebServer greetingWebServer; - private static WebServer timeWebServer; - - private static final String GREETING_PATH = "/openapi-greeting"; - private static final String TIME_PATH = "/openapi-time"; - - @BeforeAll - public static void startup() { - greetingWebServer = TestUtil.startServer(GREETING_OPENAPI_SUPPORT_BUILDER); - timeWebServer = TestUtil.startServer(TIME_OPENAPI_SUPPORT_BUILDER); - } - @Test - public void testCrossOriginGreetingWithoutCors() throws Exception { - HttpURLConnection cnx = TestUtil.getURLConnection( - greetingWebServer.port(), - "GET", - GREETING_PATH, - MediaTypes.APPLICATION_OPENAPI_YAML); - cnx.setRequestProperty("Origin", "http://foo.bar"); - cnx.setRequestProperty("Host", "localhost"); - - Config c = TestUtil.configFromResponse(cnx); - - assertEquals(Http.Status.OK_200.code(), cnx.getResponseCode()); - } - - @Test - public void testTimeRestrictedCorsValidOrigin() throws Exception { - HttpURLConnection cnx = TestUtil.getURLConnection( - timeWebServer.port(), - "GET", - TIME_PATH, - MediaTypes.APPLICATION_OPENAPI_YAML); - cnx.setRequestProperty("Origin", "http://foo.bar"); - cnx.setRequestProperty("Host", "localhost"); - - assertEquals(Http.Status.OK_200.code(), cnx.getResponseCode()); - } - - @Test - public void testTimeRestrictedCorsInvalidOrigin() throws Exception { - HttpURLConnection cnx = TestUtil.getURLConnection( - timeWebServer.port(), - "GET", - TIME_PATH, - MediaTypes.APPLICATION_OPENAPI_YAML); - cnx.setRequestProperty("Origin", "http://other.com"); - cnx.setRequestProperty("Host", "localhost"); - - assertEquals(Http.Status.FORBIDDEN_403.code(), cnx.getResponseCode()); - } -} diff --git a/nima/openapi/src/test/java/io/helidon/nima/openapi/TestUtil.java b/nima/openapi/src/test/java/io/helidon/nima/openapi/TestUtil.java deleted file mode 100644 index 0a973b9b298..00000000000 --- a/nima/openapi/src/test/java/io/helidon/nima/openapi/TestUtil.java +++ /dev/null @@ -1,385 +0,0 @@ -/* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.nima.openapi; - -import java.io.IOException; -import java.io.InputStreamReader; -import java.lang.System.Logger.Level; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.CharBuffer; -import java.nio.charset.Charset; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; - -import io.helidon.common.http.Http; -import io.helidon.common.http.HttpMediaType; -import io.helidon.common.media.type.MediaType; -import io.helidon.common.media.type.MediaTypes; -import io.helidon.config.Config; -import io.helidon.config.ConfigSources; -import io.helidon.nima.webserver.WebServer; - -import jakarta.json.Json; -import jakarta.json.JsonReader; -import jakarta.json.JsonReaderFactory; -import jakarta.json.JsonStructure; -import org.yaml.snakeyaml.Yaml; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Various utility methods used by OpenAPI tests. - */ -public class TestUtil { - - private static final JsonReaderFactory JSON_READER_FACTORY - = Json.createReaderFactory(Collections.emptyMap()); - - private static final System.Logger LOGGER = System.getLogger(TestUtil.class.getName()); - - /** - * Starts the web server at an available port and sets up OpenAPI using the - * supplied builder. - * - * @param builder the {@code OpenAPISupport.Builder} to set up for the - * server. - * @return the {@code WebServer} set up with OpenAPI support - */ - public static WebServer startServer(OpenApiService.Builder builder) { - try { - return startServer(0, builder); - } catch (InterruptedException | ExecutionException | TimeoutException ex) { - throw new RuntimeException("Error starting server for test", ex); - } - } - - /** - * Represents the HTTP response payload as a String. - * - * @param cnx the HttpURLConnection from which to get the response payload - * @return String representation of the OpenAPI document as a String - * @throws IOException in case of errors reading the HTTP response payload - */ - public static String stringYAMLFromResponse(HttpURLConnection cnx) throws IOException { - HttpMediaType returnedMediaType = mediaTypeFromResponse(cnx); - assertTrue(HttpMediaType.create(MediaTypes.APPLICATION_OPENAPI_YAML).test(returnedMediaType), - "Unexpected returned media type"); - return stringFromResponse(cnx, returnedMediaType); - } - - /** - * Connects to localhost at the specified port, sends a request using the - * specified method, and consumes the response payload as the indicated - * media type, returning the actual media type reported in the response. - * - * @param port port with which to create the connection - * @param path URL path to access on the web server - * @param expectedMediaType the {@code MediaType} with which the response - * must be consistent - * @return actual {@code MediaType} - * @throws Exception in case of errors sending the request or receiving the - * response - */ - public static MediaType connectAndConsumePayload( - int port, String path, MediaType expectedMediaType) throws Exception { - HttpURLConnection cnx = getURLConnection(port, "GET", path, expectedMediaType); - HttpMediaType actualMT = validateResponseMediaType(cnx, expectedMediaType); - if (actualMT.test(MediaTypes.APPLICATION_OPENAPI_YAML) || actualMT.test(MediaTypes.APPLICATION_YAML)) { - yamlFromResponse(cnx); - } else if (actualMT.test(MediaTypes.APPLICATION_OPENAPI_JSON) - || actualMT.test(MediaTypes.APPLICATION_JSON)) { - jsonFromResponse(cnx); - } else { - throw new IllegalArgumentException( - "Expected either JSON or YAML response but received " + actualMT.toString()); - } - return actualMT; - } - - /** - * Returns the {@code MediaType} instance conforming to the HTTP response - * content type. - * - * @param cnx the HttpURLConnection from which to get the content type - * @return the MediaType corresponding to the content type in the response - */ - public static HttpMediaType mediaTypeFromResponse(HttpURLConnection cnx) { - HttpMediaType returnedMediaType = HttpMediaType.create(cnx.getContentType()); - if (returnedMediaType.charset().isEmpty()) { - returnedMediaType = returnedMediaType.withCharset(Charset.defaultCharset().name()); - } - return returnedMediaType; - } - - /** - * Represents an OpenAPI document HTTP response as a {@code Config} instance - * to simplify access to deeply-nested values. - * - * @param cnx the HttpURLConnection which already has the response to - * process - * @return Config representing the OpenAPI document content - * @throws IOException in case of errors reading the returned payload as - * config - */ - public static Config configFromResponse(HttpURLConnection cnx) throws IOException { - HttpMediaType mt = mediaTypeFromResponse(cnx); - MediaType configMT = HttpMediaType.create(MediaTypes.APPLICATION_OPENAPI_YAML).test(mt) - ? MediaTypes.APPLICATION_X_YAML - : MediaTypes.APPLICATION_JSON; - String yaml = stringYAMLFromResponse(cnx); - return Config.create(ConfigSources.create(yaml, configMT)); - } - - /** - * Returns the response payload from the specified connection as a snakeyaml - * {@code Yaml} object. - * - * @param cnx the {@code HttpURLConnection} containing the response - * @return the YAML {@code Map} (created by snakeyaml) from - * the HTTP response payload - * @throws IOException in case of errors reading the response - */ - @SuppressWarnings(value = "unchecked") - public static Map yamlFromResponse(HttpURLConnection cnx) throws IOException { - HttpMediaType returnedMediaType = mediaTypeFromResponse(cnx); - Yaml yaml = new Yaml(); - Charset cs = Charset.defaultCharset(); - if (returnedMediaType.charset().isPresent()) { - cs = Charset.forName(returnedMediaType.charset().get()); - } - return (Map) yaml.load(new InputStreamReader(cnx.getInputStream(), cs)); - } - - /** - * Shuts down the specified web server. - * - * @param ws the {@code WebServer} instance to stop - */ - public static void shutdownServer(WebServer ws) { - if (ws != null) { - try { - stopServer(ws); - } catch (InterruptedException | ExecutionException | TimeoutException ex) { - throw new RuntimeException("Error shutting down server for test", ex); - } - } - } - - /** - * Returns the string values from the specified key in the {@code Config}, - * ensuring that the key exists first. - * - * @param c the {@code Config} object to query - * @param key the key to access in the {@code Config} object - * @return the {@code String} value from the {@code Config} value - */ - public static String fromConfig(Config c, String key) { - Config v = c.get(key); - if (!v.exists()) { - throw new IllegalArgumentException("Requested key not found: " + key); - } - return v.asString().get(); - } - - /** - * Returns the response payload in the specified connection as a - * {@code JsonStructure} instance. - * - * @param cnx the {@code HttpURLConnection} containing the response - * @return {@code JsonStructure} representing the response payload - * @throws IOException in case of errors reading the response - */ - public static JsonStructure jsonFromResponse(HttpURLConnection cnx) throws IOException { - JsonReader reader = JSON_READER_FACTORY.createReader(cnx.getInputStream()); - JsonStructure result = reader.read(); - reader.close(); - return result; - } - - /** - * Converts a JSON pointer possibly containing slashes and tildes into a - * JSON pointer with such characters properly escaped. - * - * @param pointer original JSON pointer expression - * @return escaped (if needed) JSON pointer - */ - public static String escapeForJsonPointer(String pointer) { - return pointer.replaceAll("\\~", "~0").replaceAll("\\/", "~1"); - } - - /** - * Makes sure that the response is 200 and that the content type MediaType - * is consistent with the expected one, returning the actual MediaType from - * the response and leaving the payload ready for consumption. - * - * @param cnx {@code HttpURLConnection} with the response to validate - * @param expectedMediaType {@code MediaType} with which the actual one - * should be consistent - * @return actual media type - * @throws Exception in case of errors reading the content type from the - * response - */ - public static HttpMediaType validateResponseMediaType( - HttpURLConnection cnx, - MediaType expectedMediaType) throws Exception { - assertEquals(Http.Status.OK_200.code(), cnx.getResponseCode(), - "Unexpected response code"); - MediaType expectedMT = expectedMediaType != null - ? expectedMediaType - : OpenApiService.DEFAULT_RESPONSE_MEDIA_TYPE; - HttpMediaType actualMT = mediaTypeFromResponse(cnx); - assertTrue(HttpMediaType.create(expectedMT).test(actualMT), - "Expected response media type " - + expectedMT.toString() - + " but received " - + actualMT.toString()); - return actualMT; - } - - /** - * Returns a {@code HttpURLConnection} for the requested method and path and - * {code @MediaType} from the specified {@link WebServer}. - * - * @param port port to connect to - * @param method HTTP method to use in building the connection - * @param path path to the resource in the web server - * @param mediaType {@code MediaType} to be Accepted - * @return the connection to the server and path - * @throws Exception in case of errors creating the connection - */ - public static HttpURLConnection getURLConnection( - int port, - String method, - String path, - MediaType mediaType) throws Exception { - URL url = new URL("http://localhost:" + port + path); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod(method); - if (mediaType != null) { - conn.setRequestProperty("Accept", mediaType.text()); - } - System.out.println("Connecting: " + method + " " + url); - return conn; - } - - /** - * Stop the web server. - * - * @param server the {@code WebServer} to stop - * @throws InterruptedException if the stop operation was interrupted - * @throws ExecutionException if the stop operation failed as it ran - * @throws TimeoutException if the stop operation timed out - */ - public static void stopServer(WebServer server) throws - InterruptedException, ExecutionException, TimeoutException { - if (server != null) { - server.stop(); - } - } - - /** - * Start the Web Server - * - * @param port the port on which to start the server; if less than 1, the - * port is dynamically selected - * @param openAPIBuilders OpenAPISupport.Builder instances to use in - * starting the server - * @return {@code WebServer} that has been started - * @throws java.lang.InterruptedException if the start was interrupted - * @throws java.util.concurrent.ExecutionException if the start failed - * @throws java.util.concurrent.TimeoutException if the start timed out - */ - public static WebServer startServer( - int port, - OpenApiService.Builder... openAPIBuilders) throws - InterruptedException, ExecutionException, TimeoutException { - WebServer result = WebServer.builder() - .routing(it -> it.register(openAPIBuilders) - .build()) - .port(port) - .build() - .start(); - LOGGER.log(Level.INFO, "Started server at: https://localhost:{0}", result.port()); - return result; - } - - /** - * Returns a {@code String} resulting from interpreting the response payload - * in the specified connection according to the expected {@code MediaType}. - * - * @param cnx {@code HttpURLConnection} with the response - * @param mediaType {@code MediaType} to use in interpreting the response - * payload - * @return {@code String} of the payload interpreted according to the - * specified {@code MediaType} - * @throws IOException in case of errors reading the response payload - */ - public static String stringFromResponse(HttpURLConnection cnx, HttpMediaType mediaType) throws IOException { - try (final InputStreamReader isr = new InputStreamReader( - cnx.getInputStream(), mediaType.charset().get())) { - StringBuilder sb = new StringBuilder(); - CharBuffer cb = CharBuffer.allocate(1024); - while (isr.read(cb) != -1) { - cb.flip(); - sb.append(cb); - } - return sb.toString(); - } - } - - /** - * Returns an instance of the requested type given the input object. - * - * @param expected type - * @param c the {@code Class} for the expected type - * @param o the {@code Object} to be cast to the expected type - * @return the object, cast to {@code T} - */ - public static T as(Class c, Object o) { - return c.cast(o); - } - - static MediaType connectAndConsumePayload( - int port, String path, String queryParameter, MediaType expectedMediaType) throws Exception { - HttpURLConnection cnx = getURLConnection(port, "GET", path, queryParameter); - HttpMediaType actualMT = validateResponseMediaType(cnx, expectedMediaType); - if (actualMT.test(MediaTypes.APPLICATION_OPENAPI_YAML) || actualMT.test(MediaTypes.APPLICATION_YAML)) { - yamlFromResponse(cnx); - } else if (actualMT.test(MediaTypes.APPLICATION_OPENAPI_JSON) - || actualMT.test(MediaTypes.APPLICATION_JSON)) { - jsonFromResponse(cnx); - } else { - throw new IllegalArgumentException( - "Expected either JSON or YAML response but received " + actualMT.toString()); - } - return actualMT; - } - - static HttpURLConnection getURLConnection( - int port, - String method, - String path, - String queryParameter) throws Exception { - URL url = new URL("http://localhost:" + port + path + "?" + queryParameter); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod(method); - return conn; - } -} diff --git a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/HeadersTest.java b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/HeadersTest.java index 1d93aa5d603..dbe7f9c63be 100644 --- a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/HeadersTest.java +++ b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/HeadersTest.java @@ -16,30 +16,36 @@ package io.helidon.nima.tests.integration.http2.client; -import java.time.Duration; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeoutException; - import io.helidon.common.http.Headers; import io.helidon.common.http.Http; import io.helidon.logging.common.LogConfig; import io.helidon.nima.http2.webclient.Http2; import io.helidon.nima.http2.webclient.Http2ClientResponse; import io.helidon.nima.webclient.WebClient; - +import io.vertx.core.MultiMap; import io.vertx.core.Vertx; +import io.vertx.core.http.Http2Settings; import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.HttpServerResponse; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import static java.util.concurrent.TimeUnit.MILLISECONDS; +import java.time.Duration; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeoutException; +import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; class HeadersTest { @@ -54,24 +60,39 @@ class HeadersTest { @BeforeAll static void beforeAll() throws ExecutionException, InterruptedException, TimeoutException { LogConfig.configureRuntime(); - server = vertx.createHttpServer() + server = vertx.createHttpServer(new HttpServerOptions() + .setInitialSettings(new Http2Settings() + .setMaxHeaderListSize(Integer.MAX_VALUE) + ) + ) .requestHandler(req -> { HttpServerResponse res = req.response(); switch (req.path()) { - case "/trailer" -> { - res.putHeader("test", "before"); - res.write(DATA); - res.putTrailer("Trailer-header", "trailer-test"); - res.end(); - } - case "/cont" -> { - for (int i = 0; i < 500; i++) { - res.headers().add("test-header-" + i, DATA); + case "/trailer" -> { + res.putHeader("test", "before"); + res.write(DATA); + res.putTrailer("Trailer-header", "trailer-test"); + res.end(); } - res.write(DATA); - res.end(); - } - default -> res.setStatusCode(404).end(); + case "/cont-in" -> { + for (int i = 0; i < 500; i++) { + res.headers().add("test-header-" + i, DATA); + } + res.write(DATA); + res.end(); + } + case "/cont-out" -> { + MultiMap headers = req.headers(); + StringBuilder sb = new StringBuilder(); + for (Map.Entry header : headers) { + if (!header.getKey().startsWith("test-header-")) continue; + sb.append(header.getKey() + "=" + header.getValue() + "\n"); + } + + res.write(sb.toString()); + res.end(); + } + default -> res.setStatusCode(404).end(); } }) .listen(0) @@ -97,7 +118,7 @@ static void afterAll() { } @Test - //FIXME: trailer headers are not implemented yet + //FIXME: #6544 trailer headers are not implemented yet @Disabled void trailerHeader() { try (Http2ClientResponse res = WebClient.builder(Http2.PROTOCOL) @@ -115,12 +136,12 @@ void trailerHeader() { } @Test - void continuation() { + void continuationInbound() { try (Http2ClientResponse res = WebClient.builder(Http2.PROTOCOL) .baseUri("http://localhost:" + port + "/") .build() .method(Http.Method.GET) - .path("/cont") + .path("/cont-in") .priorKnowledge(true) .request()) { @@ -133,4 +154,48 @@ void continuation() { assertThat(res.as(String.class), is(DATA)); } } + + @Test + void continuationOutbound() { + Set expected = new HashSet<>(500); + try (Http2ClientResponse res = WebClient.builder(Http2.PROTOCOL) + .baseUri("http://localhost:" + port + "/") + .build() + .method(Http.Method.GET) + .path("/cont-out") + .priorKnowledge(true) + .headers(hv -> { + for (int i = 0; i < 500; i++) { + hv.add(Http.Header.createCached("test-header-" + i, DATA + i)); + expected.add("test-header-" + i + "=" + DATA + i); + } + return hv; + }) + .request()) { + String actual = res.as(String.class); + assertThat(List.of(actual.split("\\n")), containsInAnyOrder(expected.toArray(new String[0]))); + } + } + + @Test + void continuationOutboundPost() { + Set expected = new HashSet<>(500); + try (Http2ClientResponse res = WebClient.builder(Http2.PROTOCOL) + .baseUri("http://localhost:" + port + "/") + .build() + .method(Http.Method.POST) + .path("/cont-out") + .priorKnowledge(true) + .headers(hv -> { + for (int i = 0; i < 500; i++) { + hv.add(Http.Header.createCached("test-header-" + i, DATA + i)); + expected.add("test-header-" + i + "=" + DATA + i); + } + return hv; + }) + .submit(DATA)) { + String actual = res.as(String.class); + assertThat(List.of(actual.split("\\n")), containsInAnyOrder(expected.toArray(new String[0]))); + } + } } diff --git a/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/CutConnectionTest.java b/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/CutConnectionTest.java new file mode 100644 index 00000000000..b71af81b910 --- /dev/null +++ b/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/CutConnectionTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.nima.tests.integration.http2.webserver; + +import io.helidon.common.http.Http; +import io.helidon.logging.common.LogConfig; +import io.helidon.nima.http2.webserver.Http2Route; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http.ServerRequest; +import io.helidon.nima.webserver.http.ServerResponse; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.SocketException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class CutConnectionTest { + private static final AssertingHandler ASSERTING_HANDLER = new AssertingHandler(); + private static final Logger NIMA_LOGGER = Logger.getLogger("io.helidon.nima"); + private static final int TIME_OUT_SEC = 10; + + static { + LogConfig.configureRuntime(); + } + + private final HttpClient client; + + public CutConnectionTest() { + client = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .connectTimeout(Duration.ofSeconds(20)) + .build(); + } + + + private static void stream(ServerRequest req, ServerResponse res) throws InterruptedException, IOException { + try (OutputStream os = res.outputStream()) { + for (int i = 0; i < 1000; i++) { + Thread.sleep(1); + os.write("TEST".getBytes()); + os.flush(); + } + } + } + + @Test + void testStringRoute() throws Exception { + CompletableFuture receivedFirstChunk = new CompletableFuture<>(); + ExecutorService exec = Executors.newSingleThreadExecutor(); + Level originalLevel = Level.INFO; + try { + originalLevel = NIMA_LOGGER.getLevel(); + NIMA_LOGGER.setLevel(Level.FINE); + NIMA_LOGGER.addHandler(ASSERTING_HANDLER); + WebServer server = WebServer.builder() + .host("localhost") + .routing(r -> r.route(Http2Route.route(Http.Method.GET, "/stream", CutConnectionTest::stream))) + .build(); + server.start(); + + URI uri = new URI("http://localhost:" + server.port()).resolve("/stream"); + + exec.submit(() -> { + try { + HttpResponse response = client.send(HttpRequest.newBuilder() + .timeout(Duration.ofSeconds(20)) + .uri(uri) + .GET() + .build(), HttpResponse.BodyHandlers.ofInputStream()); + try (InputStream is = response.body()) { + byte[] chunk; + for (int read = 0; read != -1; read = is.read(chunk)) { + receivedFirstChunk.complete(null); + chunk = new byte[4]; + } + } + } catch (IOException | InterruptedException e) { + // ignored + } + }); + receivedFirstChunk.get(TIME_OUT_SEC, TimeUnit.SECONDS); + exec.shutdownNow(); + assertThat(exec.awaitTermination(TIME_OUT_SEC, TimeUnit.SECONDS), is(true)); + server.stop(); + SocketClosedLog log = ASSERTING_HANDLER.socketClosedLog.get(TIME_OUT_SEC, TimeUnit.SECONDS); + assertThat(log.record.getLevel(), is(Level.FINE)); + } finally { + NIMA_LOGGER.removeHandler(ASSERTING_HANDLER); + NIMA_LOGGER.setLevel(originalLevel); + } + } + + + private record SocketClosedLog(LogRecord record, SocketException e) { + + } + + /** + * DEBUG level logging for attempts to write to closed socket is expected: + *

    {@code
    +     * 023.05.23 14:51:53 FINE io.helidon.nima.http2.webserver.Http2Connection !thread!: Socket error on writer thread
    +     * java.io.UncheckedIOException: java.net.SocketException: Socket closed
    +     * 	at io.helidon.common.buffers.FixedBufferData.writeTo(FixedBufferData.java:74)
    +     * 	at io.helidon.common.buffers.CompositeArrayBufferData.writeTo(CompositeArrayBufferData.java:41)
    +     * 	at io.helidon.common.socket.PlainSocket.write(PlainSocket.java:127)
    +     *  ...
    +     * Caused by: java.net.SocketException: Socket closed
    +     * 	at java.base/sun.nio.ch.NioSocketImpl.ensureOpenAndConnected(NioSocketImpl.java:163)
    +     *  ...
    +     * 	at java.base/java.net.Socket$SocketOutputStream.write(Socket.java:1120)
    +     * 	at io.helidon.common.buffers.FixedBufferData.writeTo(FixedBufferData.java:71)
    +     * 	...
    +     * 	}
    + */ + + private static class AssertingHandler extends Handler { + + CompletableFuture socketClosedLog = new CompletableFuture<>(); + + @Override + public void publish(LogRecord record) { + Throwable t = record.getThrown(); + if (t == null) return; + while (t.getCause() != null) { + t = t.getCause(); + } + if (t instanceof SocketException e) { + socketClosedLog.complete(new SocketClosedLog(record, e)); + } + } + + @Override + public void flush() { + + } + + @Override + public void close() throws SecurityException { + + } + } +} diff --git a/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/HeadersTest.java b/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/HeadersTest.java new file mode 100644 index 00000000000..5a0a1e1ac54 --- /dev/null +++ b/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/HeadersTest.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.nima.tests.integration.http2.webserver; + +import io.helidon.nima.http2.webserver.Http2ConfigDefault; +import io.helidon.nima.http2.webserver.Http2ConnectionProvider; +import io.helidon.nima.http2.webserver.Http2Route; +import io.helidon.nima.http2.webserver.Http2UpgradeProvider; +import io.helidon.nima.testing.junit5.webserver.ServerTest; +import io.helidon.nima.testing.junit5.webserver.SetUpRoute; +import io.helidon.nima.testing.junit5.webserver.SetUpServer; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http.HttpRouting; +import io.helidon.nima.webserver.http1.Http1ConnectionProvider; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static io.helidon.common.http.Http.Method.GET; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@ServerTest +public class HeadersTest { + + private static final Duration TIMEOUT = Duration.ofSeconds(10); + private static final String DATA = "Helidon!!!".repeat(10); + + @SetUpServer + static void setUpServer(WebServer.Builder serverBuilder) { + serverBuilder.port(-1) + // HTTP/2 prior knowledge config + .addConnectionProvider(Http2ConnectionProvider.builder() + .http2Config(Http2ConfigDefault.builder() + .sendErrorDetails(true) + .maxHeaderListSize(128_000)) + .build()) + // HTTP/1.1 -> HTTP/2 upgrade config + .addConnectionProvider(Http1ConnectionProvider.builder() + .addUpgradeProvider(Http2UpgradeProvider.builder() + .http2Config(Http2ConfigDefault.builder() + .sendErrorDetails(true) + .maxHeaderListSize(128_000)) + .build()) + .build()); + } + + @SetUpRoute + static void router(HttpRouting.Builder router) { + router.route(Http2Route.route(GET, "/ping", (req, res) -> res.send("pong"))); + router.route(Http2Route.route(GET, "/cont-out", + (req, res) -> { + for (int i = 0; i < 500; i++) { + res.header("test-header-" + i, DATA + i); + } + res.send(); + } + )); + router.route(Http2Route.route(GET, "/cont-in", + (req, res) -> { + String joinedHeaders = req.headers() + .stream() + .filter(h -> h.name().startsWith("test-header-")) + .map(h -> h.name() + "=" + h.value()) + .collect(Collectors.joining("\n")); + res.send(joinedHeaders); + } + )); + } + + @Test + void serverOutbound(WebServer server) throws IOException, InterruptedException { + URI base = URI.create("http://localhost:" + server.port()); + HttpClient client = http2Client(base); + + Set expected = new HashSet<>(500); + for (int i = 0; i < 500; i++) { + expected.add("test-header-" + i + "=" + DATA + i); + } + + HttpResponse res = client.send(HttpRequest.newBuilder() + .timeout(TIMEOUT) + .uri(base.resolve("/cont-out")) + .GET() + .build(), HttpResponse.BodyHandlers.ofString()); + + List actual = res.headers() + .map() + .entrySet() + .stream() + .filter(e -> e.getKey().startsWith("test-header-")) + .map(e -> e.getKey() + "=" + String.join("", e.getValue())) + .toList(); + + assertThat(res.statusCode(), is(200)); + assertThat(actual, Matchers.containsInAnyOrder(expected.toArray(new String[0]))); + } + + @Test + void serverInbound(WebServer server) throws IOException, InterruptedException { + URI base = URI.create("http://localhost:" + server.port()); + HttpClient client = http2Client(base); + + HttpRequest.Builder req = HttpRequest.newBuilder() + .timeout(TIMEOUT) + .GET(); + + Set expected = new HashSet<>(500); + for (int i = 0; i < 800; i++) { + req.setHeader("test-header-" + i, DATA + i); + expected.add("test-header-" + i + "=" + DATA + i); + } + + HttpResponse res = client.send(req.uri(base.resolve("/cont-in")).build(), + HttpResponse.BodyHandlers.ofString()); + + assertThat(res.statusCode(), is(200)); + assertThat(List.of(res.body().split("\n")), Matchers.containsInAnyOrder(expected.toArray(new String[0]))); + } + + @Test + void serverInboundTooLarge(WebServer server) throws IOException, InterruptedException { + URI base = URI.create("http://localhost:" + server.port()); + HttpClient client = http2Client(base); + + HttpRequest.Builder req = HttpRequest.newBuilder() + .timeout(TIMEOUT) + .GET(); + + for (int i = 0; i < 5200; i++) { + req.setHeader("test-header-" + i, DATA + i); + } + + // There is no way how to access GO_AWAY status code and additional data with JDK Http client + Assertions.assertThrows(IOException.class, + () -> client.send(req.uri(base.resolve("/cont-in")).build(), + HttpResponse.BodyHandlers.ofString())); + } + + private HttpClient http2Client(URI base) throws IOException, InterruptedException { + HttpClient client = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .connectTimeout(TIMEOUT) + .build(); + + HttpRequest req = HttpRequest.newBuilder() + .timeout(TIMEOUT) + .GET() + .uri(base.resolve("/ping")) + .build(); + + // Java client can't do the prior knowledge + client.send(req, HttpResponse.BodyHandlers.ofString()); + return client; + } + +} diff --git a/nima/tests/integration/webclient/webclient/pom.xml b/nima/tests/integration/webclient/webclient/pom.xml index 072345e16e6..fc2f119b3c7 100644 --- a/nima/tests/integration/webclient/webclient/pom.xml +++ b/nima/tests/integration/webclient/webclient/pom.xml @@ -32,6 +32,11 @@ io.helidon.nima.webclient helidon-nima-webclient + + io.helidon.nima.webserver + helidon-nima-webserver + test + io.helidon.nima.testing.junit5 helidon-nima-testing-junit5-webserver diff --git a/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java index c2f0c5484cd..936beb0a3f7 100644 --- a/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java +++ b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java @@ -57,6 +57,7 @@ class ClientRequestImplTest { private static final Http.HeaderValue REQ_EXPECT_100_HEADER_NAME = Http.Header.createCached( Http.Header.create("X-Req-Expect100"), "true"); private static final Http.HeaderName REQ_CONTENT_LENGTH_HEADER_NAME = Http.Header.create("X-Req-ContentLength"); + private static final String EXPECTED_GET_AFTER_REDIRECT_STRING = "GET after redirect endpoint reached"; private static final long NO_CONTENT_LENGTH = -1L; private final String baseURI; @@ -70,6 +71,10 @@ class ClientRequestImplTest { @SetUpRoute static void routing(HttpRules rules) { rules.put("/test", ClientRequestImplTest::responseHandler); + rules.put("/redirectKeepMethod", ClientRequestImplTest::redirectKeepMethod); + rules.put("/redirect", ClientRequestImplTest::redirect); + rules.get("/afterRedirect", ClientRequestImplTest::afterRedirectGet); + rules.put("/afterRedirect", ClientRequestImplTest::afterRedirectPut); rules.put("/chunkresponse", ClientRequestImplTest::chunkResponseHandler); } @@ -287,6 +292,35 @@ void testConnectionQueueSizeLimit() { assertThat(connectionNow, is(connection)); } + @Test + void testRedirect() { + try (Http1ClientResponse response = injectedHttp1client.put("/redirect") + .followRedirects(false) + .submit("Test entity")) { + assertThat(response.status(), is(Http.Status.FOUND_302)); + } + + try (Http1ClientResponse response = injectedHttp1client.put("/redirect") + .submit("Test entity")) { + assertThat(response.status(), is(Http.Status.OK_200)); + assertThat(response.as(String.class), is(EXPECTED_GET_AFTER_REDIRECT_STRING)); + } + } + + @Test + void testRedirectKeepMethod() { + try (Http1ClientResponse response = injectedHttp1client.put("/redirectKeepMethod") + .followRedirects(false) + .submit("Test entity")) { + assertThat(response.status(), is(Http.Status.TEMPORARY_REDIRECT_307)); + } + + try (Http1ClientResponse response = injectedHttp1client.put("/redirectKeepMethod") + .submit("Test entity")) { + assertThat(response.status(), is(Http.Status.NO_CONTENT_204)); + } + } + private static void validateSuccessfulResponse(Http1Client client) { String requestEntity = "Sending Something"; Http1ClientRequest request = client.put("/test"); @@ -319,6 +353,38 @@ private static void validateChunkTransfer(Http1ClientResponse response, boolean assertThat(responseEntity, is(entity)); } + private static void redirect(ServerRequest req, ServerResponse res) { + res.status(Http.Status.FOUND_302) + .header(Http.Header.LOCATION, "/afterRedirect") + .send(); + } + + private static void redirectKeepMethod(ServerRequest req, ServerResponse res) { + res.status(Http.Status.TEMPORARY_REDIRECT_307) + .header(Http.Header.LOCATION, "/afterRedirect") + .send(); + } + + private static void afterRedirectGet(ServerRequest req, ServerResponse res) { + if (req.content().hasEntity()) { + res.status(Http.Status.BAD_REQUEST_400) + .send("GET after redirect endpoint reached with entity"); + return; + } + res.send(EXPECTED_GET_AFTER_REDIRECT_STRING); + } + + private static void afterRedirectPut(ServerRequest req, ServerResponse res) { + String entity = req.content().as(String.class); + if (!entity.equals("Test entity")) { + res.status(Http.Status.BAD_REQUEST_400) + .send("Entity was not kept the same after the redirect"); + return; + } + res.status(Http.Status.NO_CONTENT_204) + .send(); + } + private static void responseHandler(ServerRequest req, ServerResponse res) throws IOException { customHandler(req, res, false); } diff --git a/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/HeadersTest.java b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/HeadersTest.java new file mode 100644 index 00000000000..c09b086cfe5 --- /dev/null +++ b/nima/tests/integration/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/HeadersTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.nima.webclient.http1; + +import java.util.Optional; + +import io.helidon.common.http.Headers; +import io.helidon.common.http.Http; +import io.helidon.common.http.HttpMediaType; +import io.helidon.common.media.type.ParserMode; +import io.helidon.nima.webclient.ClientResponse; +import io.helidon.nima.webclient.HttpClient; +import io.helidon.nima.webclient.WebClient; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http.HttpRules; +import io.helidon.nima.webserver.http.HttpService; +import io.helidon.nima.webserver.http.ServerRequest; +import io.helidon.nima.webserver.http.ServerResponse; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class HeadersTest { + + private static WebServer server; + + @BeforeAll + static void beforeAll() { + server = WebServer.builder() + .routing(routing -> routing.register("/test", new TestService()).build()) + .start(); + } + + @AfterAll + static void afterAll() { + server.stop(); + } + + // Verify that invalid content type is present in response headers and is accesible + @Test + public void testInvalidContentType() { + HttpClient client = WebClient.builder() + .baseUri("http://localhost:" + server.port() + "/test") + .build(); + try (ClientResponse res = client.method(Http.Method.GET) + .path("/invalidContentType") + .request()) { + Headers h = res.headers(); + Http.HeaderValue contentType = h.get(Http.Header.CONTENT_TYPE); + assertThat(res.status(), is(Http.Status.OK_200)); + assertThat(contentType.value(), is(TestService.INVALID_CONTENT_TYPE_VALUE)); + } + } + + // Verify that "Content-Type: text" header parsing fails in strict mode + @Test + public void testInvalidTextContentTypeStrict() { + HttpClient client = WebClient.builder() + .baseUri("http://localhost:" + server.port() + "/test") + .build(); + ClientResponse res = client.method(Http.Method.GET) + .path("/invalidTextContentType") + .request(); + assertThat(res.status(), is(Http.Status.OK_200)); + Headers h = res.headers(); + // Raw protocol data value + Http.HeaderValue rawContentType = h.get(Http.Header.CONTENT_TYPE); + assertThat(rawContentType.value(), is(TestService.INVALID_CONTENT_TYPE_TEXT)); + // Media type parsed value is invalid, IllegalArgumentException shall be thrown + try { + h.contentType(); + Assertions.fail("Content-Type: text parsing must throw an exception in strict mode"); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), is("Cannot parse media type: text")); + } + } + + // Verify that "Content-Type: text" header parsing returns text/plain in relaxed mode + @Test + public void testInvalidTextContentTypeRelaxed() { + HttpClient client = WebClient.builder() + .baseUri("http://localhost:" + server.port() + "/test") + .mediaTypeParserMode(ParserMode.RELAXED) + .build(); + ClientResponse res = client.method(Http.Method.GET) + .path("/invalidTextContentType") + .request(); + assertThat(res.status(), is(Http.Status.OK_200)); + Headers h = res.headers(); + // Raw protocol data value + Http.HeaderValue rawContentType = h.get(Http.Header.CONTENT_TYPE); + assertThat(rawContentType.value(), is(TestService.INVALID_CONTENT_TYPE_TEXT)); + // Media type parsed value + Optional contentType = h.contentType(); + assertThat(contentType.isPresent(), is(true)); + assertThat(contentType.get().text(), is(TestService.RELAXED_CONTENT_TYPE_TEXT)); + } + + static final class TestService implements HttpService { + + TestService() { + } + + @Override + public void routing(HttpRules rules) { + rules + .get("/invalidContentType", this::invalidContentType) + .get("/invalidTextContentType", this::invalidTextContentType); + } + + private static final String INVALID_CONTENT_TYPE_VALUE = "invalid header value"; + + private void invalidContentType(ServerRequest request, ServerResponse response) { + response.header(Http.Header.CONTENT_TYPE, INVALID_CONTENT_TYPE_VALUE) + .send(); + } + + private static final String INVALID_CONTENT_TYPE_TEXT = "text"; + private static final String RELAXED_CONTENT_TYPE_TEXT = "text/plain"; + + // Returns Content-Type: text instead of text/plain + private void invalidTextContentType(ServerRequest request, ServerResponse response) { + response.header(Http.Header.CONTENT_TYPE, INVALID_CONTENT_TYPE_TEXT) + .send(); + } + + } + +} diff --git a/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/ExceptionMessageTest.java b/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/ExceptionMessageTest.java new file mode 100644 index 00000000000..37e34f47f18 --- /dev/null +++ b/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/ExceptionMessageTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.nima.tests.integration.server; + +import java.util.Collections; + +import io.helidon.common.http.Http; +import io.helidon.common.testing.http.junit5.SocketHttpClient; +import io.helidon.nima.testing.junit5.webserver.ServerTest; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Test that no unwanted request data is leaked back (reflected) in response to a + * bad request. There are no routes defined for this test. + */ +@ServerTest +class ExceptionMessageTest { + + private final SocketHttpClient socketClient; + + ExceptionMessageTest(SocketHttpClient socketClient) { + this.socketClient = socketClient; + } + + @Test + void testNoUrlReflect() { + String response = socketClient.sendAndReceive("/anyjavascript%3a/*%3c/script%3e%3cimg/onerror%3d'\\''" + + "-/%22/-/%20onmouseover%d1/-/[%60*/[]/[(new(Image)).src%3d(/%3b/%2b/255t6qeelp23xlr08hn1uv" + + "vnkeqae02stgk87yvnX%3b.oastifycom/).replace(/.%3b/g%2c[])]//'\\''src%3d%3e", + Http.Method.GET, + ""); + Http.Status status = SocketHttpClient.statusFromResponse(response); + String entity = SocketHttpClient.entityFromResponse(response, false); + assertThat(status, is(Http.Status.BAD_REQUEST_400)); + assertThat(entity, containsString("see server log")); + assertThat(entity, not(containsString("javascript"))); + } + + @Test + void testNoHeaderReflect() { + String response = socketClient.sendAndReceive("/", + Http.Method.GET, + "", + Collections.singletonList(": ")); + Http.Status status = SocketHttpClient.statusFromResponse(response); + String entity = SocketHttpClient.entityFromResponse(response, false); + assertThat(status, is(Http.Status.BAD_REQUEST_400)); + assertThat(entity, not(containsString("javascript"))); + } +} diff --git a/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/MultiPortTest.java b/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/MultiPortTest.java index 1c6226bda2e..51f1d74fa3c 100644 --- a/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/MultiPortTest.java +++ b/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/MultiPortTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2022 Oracle and/or its affiliates. + * Copyright (c) 2017, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,7 @@ */ class MultiPortTest { private static final Http1Client CLIENT = WebClient.builder() + .followRedirect(false) .build(); private Handler commonHandler; private WebServer server; diff --git a/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/StatusCodeTest.java b/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/StatusCodeTest.java new file mode 100644 index 00000000000..f74d203611f --- /dev/null +++ b/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/StatusCodeTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.nima.tests.integration.server; + +import io.helidon.common.http.Http; +import io.helidon.nima.testing.junit5.webserver.ServerTest; +import io.helidon.nima.testing.junit5.webserver.SetUpRoute; +import io.helidon.nima.webclient.ClientResponse; +import io.helidon.nima.webclient.http1.Http1Client; +import io.helidon.nima.webserver.http.HttpRules; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Tests status codes as integers. + */ +@ServerTest +class StatusCodeTest { + + private final Http1Client client; + + StatusCodeTest(Http1Client client) { + this.client = client; + } + + @SetUpRoute + static void routing(HttpRules rules) { + rules.get("/", (req, res) -> res.status(204).send()); + } + + @Test + void testCode() { + try (ClientResponse response = client.method(Http.Method.GET).request()) { + assertThat(response.status(), is(Http.Status.NO_CONTENT_204)); + } + } +} diff --git a/nima/webclient/webclient/pom.xml b/nima/webclient/webclient/pom.xml index 55ae1815d36..2d2ed7f2f7e 100644 --- a/nima/webclient/webclient/pom.xml +++ b/nima/webclient/webclient/pom.xml @@ -93,6 +93,11 @@ junit-jupiter-api test + + junit-jupiter-params + org.junit.jupiter + test + org.hamcrest hamcrest-all diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientConfig.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientConfig.java index b00c06e376d..cb9b34af958 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientConfig.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientConfig.java @@ -19,6 +19,8 @@ import java.net.URI; import java.util.Optional; +import io.helidon.common.http.Headers; +import io.helidon.common.media.type.ParserMode; import io.helidon.common.socket.SocketOptions; import io.helidon.config.metadata.ConfiguredOption; import io.helidon.nima.common.tls.Tls; @@ -66,4 +68,36 @@ public interface ClientConfig { */ DnsAddressLookup dnsAddressLookup(); + /** + * Whether to follow redirects. + * + * @return follow redirects + */ + @ConfiguredOption("true") + boolean followRedirects(); + + /** + * Maximum number of redirects allowed. + * + * @return allowed number of redirects + */ + @ConfiguredOption("5") + int maxRedirects(); + + /** + * Custom client headers. + * + * @return client headers + */ + Headers defaultHeaders(); + + /** + * Client {@code Content-Type} header matching mode. + * Supported values are {@code ParserMode.STRICT} and {@code ParserMode.RELAXED}. + * + * @return {@code Content-Type} header matching mode + */ + @ConfiguredOption("STRICT") + ParserMode mediaTypeParserMode(); + } diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientRequest.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientRequest.java index f06085e1f6d..73a2a7768d9 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientRequest.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ClientRequest.java @@ -131,6 +131,22 @@ default B header(Http.HeaderName name, String value) { */ B fragment(String fragment); + /** + * Whether to follow redirects. + * + * @param followRedirects follow redirects + * @return updated request + */ + B followRedirects(boolean followRedirects); + + /** + * Max number of the followed redirects. + * + * @param maxRedirects max followed redirects + * @return updated request + */ + B maxRedirects(int maxRedirects); + /** * Request without an entity. * @@ -181,6 +197,22 @@ default T request(Class type) { */ B connection(ClientConnection connection); + /** + * Disable uri encoding. + * + * @return updated client request + */ + B skipUriEncoding(); + + /** + * Add a property to be used by this request. + * + * @param propertyName property name + * @param propertyValue property value + * @return updated builder instance + */ + B property(String propertyName, String propertyValue); + /** * Handle output stream. */ diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/LoomClient.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/LoomClient.java index e1b8d284f1e..fb0f035b8b4 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/LoomClient.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/LoomClient.java @@ -17,10 +17,13 @@ package io.helidon.nima.webclient; import java.net.URI; +import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import io.helidon.common.LazyValue; +import io.helidon.common.http.Headers; +import io.helidon.common.media.type.ParserMode; import io.helidon.common.socket.SocketOptions; import io.helidon.nima.common.tls.Tls; import io.helidon.nima.webclient.spi.DnsResolver; @@ -41,6 +44,11 @@ public class LoomClient implements WebClient { private final SocketOptions channelOptions; private final DnsResolver dnsResolver; private final DnsAddressLookup dnsAddressLookup; + private final int maxRedirects; + private final boolean followRedirects; + private final Headers defaultHeaders; + private final ParserMode mediaTypeParserMode; + private final Map properties; /** * Construct this instance from a subclass of builder. @@ -53,6 +61,11 @@ protected LoomClient(WebClient.Builder builder) { this.channelOptions = builder.channelOptions() == null ? EMPTY_OPTIONS : builder.channelOptions(); this.dnsResolver = builder.dnsResolver(); this.dnsAddressLookup = builder.dnsAddressLookup(); + this.maxRedirects = builder.maxRedirect(); + this.followRedirects = builder.followRedirect(); + this.defaultHeaders = builder.defaultHeaders(); + this.mediaTypeParserMode = builder.mediaTypeParserMode(); + this.properties = builder.properties(); } /** @@ -101,11 +114,57 @@ public DnsResolver dnsResolver() { } /** - * + * DNS address lookup instance to be used for this client. * * @return DNS address lookup instance type */ public DnsAddressLookup dnsAddressLookup() { return dnsAddressLookup; } + + /** + * Whether to follow redirects. + * + * @return follow redirects + */ + public boolean followRedirects() { + return followRedirects; + } + + /** + * Maximum number of redirects allowed. + * + * @return allowed number of redirects + */ + public int maxRedirects() { + return maxRedirects; + } + + /** + * Default headers to be used in every request performed by this client. + * + * @return default headers + */ + public Headers defaultHeaders() { + return defaultHeaders; + } + + /** + * Properties configured for this client. + * + * @return properties + */ + public Map properties() { + return properties; + } + + /** + * Media type parsing mode for HTTP {@code Content-Type} header. + * + * @return media type parsing mode + */ + protected ParserMode mediaTypeParserMode() { + return mediaTypeParserMode; + } + } diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/UriHelper.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/UriHelper.java index 2de5cd59670..269ad56d055 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/UriHelper.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/UriHelper.java @@ -44,6 +44,7 @@ public class UriHelper { private String path; private String host; private int port; + private boolean skipUriEncoding = false; private UriHelper() { this.baseScheme = null; @@ -109,6 +110,7 @@ public void scheme(String scheme) { */ public void host(String host) { this.host = host; + authority(host, port); } /** @@ -118,6 +120,7 @@ public void host(String host) { */ public void port(int port) { this.port = port; + authority(host, port); } /** @@ -130,6 +133,15 @@ public void path(String path, UriQueryWriteable query) { this.path = extractQuery(path, query); } + /** + * Whether to skip uri encoding. + * + * @param skipUriEncoding skip uri encoding + */ + public void skipUriEncoding(boolean skipUriEncoding) { + this.skipUriEncoding = skipUriEncoding; + } + /** * Resolve the provided URI against this URI and extract query from it. * @@ -229,7 +241,7 @@ public String path() { * @return string containing encoded path with query */ public String pathWithQueryAndFragment(UriQuery query, UriFragment fragment) { - String queryString = query.rawValue(); + String queryString = skipUriEncoding ? query.value() : query.rawValue(); boolean hasQuery = !queryString.isEmpty(); @@ -237,14 +249,15 @@ public String pathWithQueryAndFragment(UriQuery query, UriFragment fragment) { if (this.path.equals("")) { path = "/"; } else { - path = UriEncoding.encodeUri(this.path); + path = skipUriEncoding ? this.path : UriEncoding.encodeUri(this.path); } if (hasQuery) { path = path + '?' + queryString; } if (fragment.hasValue()) { - path = path + '#' + fragment.rawValue(); + String fragmentValue = skipUriEncoding ? fragment.value() : fragment.rawValue(); + path = path + '#' + fragmentValue; } return path; diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/WebClient.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/WebClient.java index 30d56dde476..4dcbf0dfaa0 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/WebClient.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/WebClient.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,19 @@ package io.helidon.nima.webclient; import java.net.URI; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; +import io.helidon.common.http.ClientRequestHeaders; +import io.helidon.common.http.Http; +import io.helidon.common.http.WritableHeaders; +import io.helidon.common.media.type.ParserMode; import io.helidon.common.socket.SocketOptions; import io.helidon.nima.common.tls.Tls; import io.helidon.nima.webclient.http1.Http1; @@ -62,8 +73,14 @@ abstract class Builder, C extends WebClient> implements private URI baseUri; private Tls tls; private SocketOptions channelOptions; + private final SocketOptions.Builder channelOptionsBuilder = SocketOptions.builder(); private DnsResolver dnsResolver; private DnsAddressLookup dnsAddressLookup; + private boolean followRedirect; + private int maxRedirect; + private WritableHeaders defaultHeaders = WritableHeaders.create(); + private ParserMode mediaTypeParserMode = ParserMode.STRICT; + private Map properties = new HashMap<>(); /** * Common builder base for all the client builder. @@ -71,6 +88,21 @@ abstract class Builder, C extends WebClient> implements protected Builder() { } + /** + * Actual {@link #build()} implementation for {@link WebClient} subclasses. + * + * @return new client + */ + protected abstract C doBuild(); + + @Override + public C build() { + if (channelOptions == null) { + channelOptions = channelOptionsBuilder.build(); + } + return doBuild(); + } + /** * Base uri used by the client in all requests. * @@ -89,7 +121,7 @@ public B baseUri(String baseUri) { */ public B baseUri(URI baseUri) { this.baseUri = baseUri; - return (B) this; + return identity(); } /** @@ -102,7 +134,7 @@ public B baseUri(URI baseUri) { */ public B tls(Tls tls) { this.tls = tls; - return (B) this; + return identity(); } /** @@ -115,18 +147,37 @@ public B tls(Tls tls) { */ public B tls(Supplier tls) { this.tls = tls.get(); - return (B) this; + return identity(); } /** * Socket options for connections opened by this client. + * Note that using this method will trump the default {@link SocketOptions.Builder}. + * Thus, all methods that operate on the default {@link SocketOptions.Builder} are ineffective: + *
      + *
    • {@link #channelOptions(Consumer)}
    • + *
    • {@link #readTimeout(Duration)}
    • + *
    • {@link #connectTimeout(Duration)} (Duration)}
    • + *
    • {@link #keepAlive(boolean)}
    • + *
    * * @param channelOptions options * @return updated builder */ public B channelOptions(SocketOptions channelOptions) { this.channelOptions = channelOptions; - return (B) this; + return identity(); + } + + /** + * Configure the socket options for connections opened by this client. + * + * @param consumer {@link SocketOptions.Builder} consumer + * @return updated builder + */ + public B channelOptions(Consumer consumer) { + consumer.accept(channelOptionsBuilder); + return identity(); } /** @@ -137,7 +188,7 @@ public B channelOptions(SocketOptions channelOptions) { */ public B dnsResolver(DnsResolver dnsResolver) { this.dnsResolver = dnsResolver; - return (B) this; + return identity(); } /** @@ -148,7 +199,141 @@ public B dnsResolver(DnsResolver dnsResolver) { */ public B dnsAddressLookup(DnsAddressLookup dnsAddressLookup) { this.dnsAddressLookup = dnsAddressLookup; - return (B) this; + return identity(); + } + + /** + * Whether to follow redirects. + * + * @param followRedirect whether to follow redirects + * @return updated builder + */ + public B followRedirect(boolean followRedirect) { + this.followRedirect = followRedirect; + return identity(); + } + + /** + * Max number of followed redirects. + * This is ignored if followRedirect option is false. + * + * @param maxRedirect max number of followed redirects + * @return updated builder + */ + public B maxRedirects(int maxRedirect) { + this.maxRedirect = maxRedirect; + return identity(); + } + + /** + * Connect timeout. + * This method operates on the default socket options builder and provides a shortcut for + * {@link SocketOptions.Builder#connectTimeout(Duration)}. + * + * @param connectTimeout connect timeout + * @return updated builder + */ + public B connectTimeout(Duration connectTimeout) { + channelOptionsBuilder.connectTimeout(connectTimeout); + return identity(); + } + + /** + * Sets the socket read timeout. + * This method operates on the default socket options builder and provides a shortcut for + * {@link SocketOptions.Builder#readTimeout(Duration)}. + * + * @param readTimeout read timeout + * @return updated builder + */ + public B readTimeout(Duration readTimeout) { + channelOptionsBuilder.readTimeout(readTimeout); + return identity(); + } + + /** + * Configure socket keep alive. + * This method operates on the default socket options builder and provides a shortcut for + * {@link SocketOptions.Builder#socketKeepAlive(boolean)}. + * + * @param keepAlive keep alive + * @return updated builder + * @see java.net.StandardSocketOptions#SO_KEEPALIVE + */ + public B keepAlive(boolean keepAlive) { + channelOptionsBuilder.socketKeepAlive(keepAlive); + return identity(); + } + + /** + * Configure a custom header to be sent. Some headers cannot be modified. + * + * @param header header to add + * @return updated builder instance + */ + public B header(Http.HeaderValue header) { + Objects.requireNonNull(header); + this.defaultHeaders.set(header); + return identity(); + } + + /** + * Set header with multiple values. Some headers cannot be modified. + * + * @param name header name + * @param values header values + * @return updated builder instance + */ + public B header(Http.HeaderName name, List values) { + Objects.requireNonNull(name); + this.defaultHeaders.set(name, values); + return identity(); + } + + /** + * Update headers. + * + * @param headersConsumer consumer of client headers + * @return updated builder instance + */ + public B headers(Function> headersConsumer) { + this.defaultHeaders = headersConsumer.apply(ClientRequestHeaders.create(defaultHeaders)); + return identity(); + } + + /** + * Properties configured by user when creating this client. + * + * @param properties that were configured (mutable) + * @return updated builder instance + */ + public B properties(Map properties) { + Objects.requireNonNull(properties); + this.properties = properties; + return identity(); + } + + /** + * Remove header with the selected name from the default headers. + * + * @param name header name + * @return updated builder instance + */ + protected B removeHeader(Http.HeaderName name) { + Objects.requireNonNull(name); + this.defaultHeaders.remove(name); + return identity(); + } + + /** + * Configure media type parsing mode for HTTP {@code Content-Type} header. + * + * @param mode media type parsing mode + * @return updated builder instance + */ + public B mediaTypeParserMode(ParserMode mode) { + this.mediaTypeParserMode = mode; + return identity(); } /** @@ -156,24 +341,32 @@ public B dnsAddressLookup(DnsAddressLookup dnsAddressLookup) { * * @return socket options */ - SocketOptions channelOptions() { + protected SocketOptions channelOptions() { return channelOptions; } /** - * Configured TLS. + * Default headers to be used in every request. * - * @return TLS if configured, null otherwise + * @return default headers */ - Tls tls() { - return tls; + protected WritableHeaders defaultHeaders() { + return defaultHeaders; } /** - * Base request uri. + * Media type parsing mode for HTTP {@code Content-Type} header. * - * @return client request base uri + * @return media type parsing mode */ + protected ParserMode mediaTypeParserMode() { + return this.mediaTypeParserMode; + } + + Tls tls() { + return tls; + } + URI baseUri() { return baseUri; } @@ -186,5 +379,16 @@ DnsAddressLookup dnsAddressLookup() { return dnsAddressLookup; } + boolean followRedirect() { + return followRedirect; + } + + int maxRedirect() { + return maxRedirect; + } + + Map properties() { + return properties; + } } } diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java index 80ce6580843..d8a6b2e31a8 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java @@ -27,14 +27,18 @@ import io.helidon.common.buffers.BufferData; import io.helidon.common.context.Context; +import io.helidon.common.context.Contexts; import io.helidon.common.http.ClientRequestHeaders; import io.helidon.common.http.Http; import io.helidon.common.http.Http.HeaderValue; import io.helidon.common.http.WritableHeaders; import io.helidon.common.uri.UriEncoding; import io.helidon.common.uri.UriFragment; +import io.helidon.common.uri.UriPath; +import io.helidon.common.uri.UriQuery; import io.helidon.common.uri.UriQueryWriteable; import io.helidon.nima.common.tls.Tls; +import io.helidon.nima.http.media.MediaContext; import io.helidon.nima.webclient.ClientConnection; import io.helidon.nima.webclient.Proxy; import io.helidon.nima.webclient.UriHelper; @@ -51,26 +55,50 @@ class ClientRequestImpl implements Http1ClientRequest { private final UriHelper uri; private final String requestId; private final Http1ClientConfig clientConfig; + private final MediaContext mediaContext; + private final Map properties; private WritableHeaders explicitHeaders = WritableHeaders.create(); + private boolean followRedirects; + private int maxRedirects; private Tls tls; private String uriTemplate; private ClientConnection connection; - private UriFragment fragment; + private UriFragment fragment = UriFragment.empty(); private Proxy proxy; + private boolean skipUriEncoding = false; ClientRequestImpl(Http1ClientConfig clientConfig, Http.Method method, UriHelper helper, - UriQueryWriteable query) { + UriQueryWriteable query, + Map properties) { this.method = method; this.uri = helper; + this.properties = properties; this.clientConfig = clientConfig; + this.mediaContext = clientConfig.mediaContext(); + this.followRedirects = clientConfig.followRedirects(); + this.maxRedirects = clientConfig.maxRedirects(); this.tls = clientConfig.tls().orElse(null); this.query = query; this.requestId = "http1-client-" + COUNTER.getAndIncrement(); + this.explicitHeaders = WritableHeaders.create(clientConfig.defaultHeaders()); + } + + //Copy constructor for redirection purposes + private ClientRequestImpl(ClientRequestImpl request, + Http.Method method, + UriHelper helper, + UriQueryWriteable query, + Map properties) { + this(request.clientConfig, method, helper, query, properties); + this.followRedirects = request.followRedirects; + this.maxRedirects = request.maxRedirects; + this.tls = request.tls; + this.connection = request.connection; } @Override @@ -78,7 +106,11 @@ public Http1ClientRequest uri(String uri) { if (uri.indexOf('{') > -1) { this.uriTemplate = uri; } else { - uri(URI.create(UriEncoding.encodeUri(uri))); + if (skipUriEncoding) { + uri(URI.create(uri)); + } else { + uri(URI.create(UriEncoding.encodeUri(uri))); + } } return this; @@ -127,6 +159,18 @@ public Http1ClientRequest fragment(String fragment) { return this; } + @Override + public Http1ClientRequest followRedirects(boolean followRedirects) { + this.followRedirects = followRedirects; + return this; + } + + @Override + public Http1ClientRequest maxRedirects(int maxRedirects) { + this.maxRedirects = maxRedirects; + return this; + } + @Override public Http1ClientResponse request() { return submit(BufferData.EMPTY_BYTES); @@ -137,24 +181,15 @@ public Http1ClientResponse submit(Object entity) { if (entity != BufferData.EMPTY_BYTES) { rejectHeadWithEntity(); } - - CompletableFuture whenSent = new CompletableFuture<>(); - CompletableFuture whenComplete = new CompletableFuture<>(); - WebClientService.Chain callChain = new HttpCallEntityChain(clientConfig, - connection, - tls, - proxy, - whenSent, - whenComplete, - entity); - - return invokeServices(callChain, whenSent, whenComplete); + if (followRedirects) { + return invokeWithFollowRedirectsEntity(entity); + } + return invokeRequestWithEntity(entity); } @Override public Http1ClientResponse outputStream(OutputStreamHandler streamHandler) { rejectHeadWithEntity(); - CompletableFuture whenSent = new CompletableFuture<>(); CompletableFuture whenComplete = new CompletableFuture<>(); WebClientService.Chain callChain = new HttpCallOutputStreamChain(clientConfig, @@ -172,7 +207,11 @@ public Http1ClientResponse outputStream(OutputStreamHandler streamHandler) { public URI resolvedUri() { if (uriTemplate != null) { String resolved = resolvePathParams(uriTemplate); - this.uri.resolve(URI.create(UriEncoding.encodeUri(resolved)), query); + if (skipUriEncoding) { + this.uri.resolve(URI.create(resolved), query); + } else { + this.uri.resolve(URI.create(UriEncoding.encodeUri(resolved)), query); + } } return URI.create(this.uri.scheme() + "://" + uri.authority() @@ -186,6 +225,19 @@ public Http1ClientRequest connection(ClientConnection connection) { return this; } + @Override + public Http1ClientRequest skipUriEncoding() { + this.skipUriEncoding = true; + this.uri.skipUriEncoding(true); + return this; + } + + @Override + public Http1ClientRequest property(String propertyName, String propertyValue) { + this.properties.put(propertyName, propertyValue); + return this; + } + Http1ClientConfig clientConfig() { return clientConfig; } @@ -198,25 +250,108 @@ ClientRequestHeaders headers() { return ClientRequestHeaders.create(explicitHeaders); } + private ClientResponseImpl invokeWithFollowRedirectsEntity(Object entity) { + //Request object which should be used for invoking the next request. This will change in case of any redirection. + ClientRequestImpl clientRequest = this; + //Entity to be sent with the request. Will be changed when redirect happens to prevent entity sending. + Object entityToBeSent = entity; + for (int i = 0; i < maxRedirects; i++) { + ClientResponseImpl clientResponse = clientRequest.invokeRequestWithEntity(entityToBeSent); + int code = clientResponse.status().code(); + if (code < 300 || code >= 400) { + return clientResponse; + } else if (!clientResponse.headers().contains(Http.Header.LOCATION)) { + throw new IllegalStateException("There is no " + Http.Header.LOCATION + " header present in the response! " + + "It is not clear where to redirect."); + } + String redirectedUri = clientResponse.headers().get(Http.Header.LOCATION).value(); + URI newUri = URI.create(redirectedUri); + UriQueryWriteable newQuery = UriQueryWriteable.create(); + UriHelper redirectUri = UriHelper.create(newUri, newQuery); + String uriQuery = newUri.getQuery(); + if (uriQuery != null) { + newQuery.fromQueryString(uriQuery); + } + if (newUri.getHost() == null) { + //To keep the information about the latest host, we need to use uri from the last performed request + //Example: + //request -> my-test.com -> response redirect -> my-example.com + //new request -> my-example.com -> response redirect -> /login + //with using the last request uri host etc, we prevent my-test.com/login from happening + redirectUri.scheme(clientRequest.uri.scheme()); + redirectUri.host(clientRequest.uri.host()); + redirectUri.port(clientRequest.uri.port()); + } + //Method and entity is required to be the same as with original request with 307 and 308 requests + if (clientResponse.status() == Http.Status.TEMPORARY_REDIRECT_307 + || clientResponse.status() == Http.Status.PERMANENT_REDIRECT_308) { + clientRequest = new ClientRequestImpl(this, method, redirectUri, newQuery, properties); + } else { + //It is possible to change to GET and send no entity with all other redirect codes + entityToBeSent = BufferData.EMPTY_BYTES; //We do not want to send entity after this redirect + clientRequest = new ClientRequestImpl(this, Http.Method.GET, redirectUri, newQuery, properties); + } + } + throw new IllegalStateException("Maximum number of request redirections (" + + clientConfig.maxRedirects() + ") reached."); + } + + private ClientResponseImpl invokeRequestWithEntity(Object entity) { + CompletableFuture whenSent = new CompletableFuture<>(); + CompletableFuture whenComplete = new CompletableFuture<>(); + WebClientService.Chain callChain = new HttpCallEntityChain(clientConfig, + connection, + tls, + proxy, + whenSent, + whenComplete, + entity); + + return invokeServices(callChain, whenSent, whenComplete); + } + + @Override + public Http.Method httpMethod(){ + return method; + } + + @Override + public UriPath uriPath(){ + return UriPath.create(uri.path()); + } + + @Override + public UriQuery uriQuery(){ + return UriQuery.create(resolvedUri()); + } + + @Override + public Http1ClientRequest proxy(Proxy proxy) { + this.proxy = proxy; + return this; + } + private ClientResponseImpl invokeServices(WebClientService.Chain callChain, CompletableFuture whenSent, CompletableFuture whenComplete) { if (uriTemplate != null) { String resolved = resolvePathParams(uriTemplate); - this.uri.resolve(URI.create(UriEncoding.encodeUri(resolved)), query); + if (skipUriEncoding) { + this.uri.resolve(URI.create(resolved), query); + } else { + this.uri.resolve(URI.create(UriEncoding.encodeUri(resolved)), query); + } } ClientRequestHeaders headers = ClientRequestHeaders.create(explicitHeaders); - Map properties = new HashMap<>(); - WebClientServiceRequest serviceRequest = new ServiceRequestImpl(uri, method, Http.Version.V1_1, query, UriFragment.empty(), headers, - Context.create(), + Contexts.context().orElseGet(Context::create), requestId, whenComplete, whenSent, @@ -244,6 +379,8 @@ private ClientResponseImpl invokeServices(WebClientService.Chain callChain, serviceResponse.headers(), serviceResponse.connection(), serviceResponse.reader(), + mediaContext, + clientConfig.mediaTypeParserMode(), complete); } @@ -269,10 +406,4 @@ private void rejectHeadWithEntity() { throw new IllegalArgumentException("Payload in method '" + Http.Method.HEAD + "' has no defined semantics"); } } - - @Override - public Http1ClientRequest proxy(Proxy proxy) { - this.proxy = proxy; - return this; - } } diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientResponseImpl.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientResponseImpl.java index a4958883f74..62e8ef01d98 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientResponseImpl.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientResponseImpl.java @@ -34,6 +34,7 @@ import io.helidon.common.http.Http.HeaderValues; import io.helidon.common.http.Http1HeadersParser; import io.helidon.common.http.WritableHeaders; +import io.helidon.common.media.type.ParserMode; import io.helidon.nima.http.encoding.ContentDecoder; import io.helidon.nima.http.encoding.ContentEncodingContext; import io.helidon.nima.http.media.MediaContext; @@ -62,11 +63,13 @@ class ClientResponseImpl implements Http1ClientResponse { private final DataReader reader; // todo configurable private final ContentEncodingContext encodingSupport = ContentEncodingContext.create(); - private final MediaContext mediaContext = MediaContext.create(); + private final MediaContext mediaContext; private final String channelId; private final CompletableFuture whenComplete; private final boolean hasTrailers; private final List trailerNames; + // Media type parsing mode configured on client. + private final ParserMode parserMode; private ClientConnection connection; private long entityLength; @@ -78,12 +81,16 @@ class ClientResponseImpl implements Http1ClientResponse { ClientResponseHeaders responseHeaders, ClientConnection connection, DataReader reader, + MediaContext mediaContext, + ParserMode parserMode, CompletableFuture whenComplete) { this.responseStatus = responseStatus; this.requestHeaders = requestHeaders; this.responseHeaders = responseHeaders; this.connection = connection; this.reader = reader; + this.mediaContext = mediaContext; + this.parserMode = parserMode; this.channelId = connection.channelId(); this.whenComplete = whenComplete; diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1Client.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1Client.java index 949b2685605..e126b544e7f 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1Client.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1Client.java @@ -24,9 +24,11 @@ import io.helidon.common.HelidonServiceLoader; import io.helidon.common.LazyValue; +import io.helidon.common.media.type.ParserMode; import io.helidon.common.socket.SocketOptions; import io.helidon.nima.common.tls.Tls; import io.helidon.nima.http.media.MediaContext; +import io.helidon.nima.http.media.MediaSupport; import io.helidon.nima.webclient.DefaultDnsResolverProvider; import io.helidon.nima.webclient.DnsAddressLookup; import io.helidon.nima.webclient.HttpClient; @@ -67,6 +69,8 @@ class Http1ClientBuilder extends WebClient.Builder services(); } diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientImpl.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientImpl.java index 0328769fbee..1eb19a9ef39 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientImpl.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientImpl.java @@ -55,7 +55,7 @@ public Http1ClientRequest method(Http.Method method) { UriQueryWriteable query = UriQueryWriteable.create(); UriHelper helper = (uri() == null) ? UriHelper.create() : UriHelper.create(uri(), query); - return new ClientRequestImpl(clientConfig, method, helper, query); + return new ClientRequestImpl(clientConfig, method, helper, query, properties()); } Http1ClientConfig clientConfig() { diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientRequest.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientRequest.java index f1d071e8094..6e1898b2383 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientRequest.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package io.helidon.nima.webclient.http1; +import io.helidon.common.http.Http; +import io.helidon.common.uri.UriPath; +import io.helidon.common.uri.UriQuery; import io.helidon.nima.webclient.ClientRequest; /** @@ -23,4 +26,24 @@ */ public interface Http1ClientRequest extends ClientRequest { + /** + * HTTP Method. + * + * @return {@link Http.Method} the http method + */ + Http.Method httpMethod(); + + /** + * URI Path. + * + * @return {@link UriPath} + */ + UriPath uriPath(); + + /** + * URI Query. + * + * @return {@link UriQuery} + */ + UriQuery uriQuery(); } diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/HttpCallChainBase.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/HttpCallChainBase.java index 357da752f9e..35d63f477f8 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/HttpCallChainBase.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/HttpCallChainBase.java @@ -83,8 +83,13 @@ abstract WebClientServiceResponse doProceed(ClientConnection connection, BufferData writeBuffer); void prologue(BufferData nonEntityData, WebClientServiceRequest request, UriHelper uri) { + // TODO When proxy is implemented, change default value of Http1ClientConfig.relativeUris to false + // and below conditional statement to: + // proxy == Proxy.noProxy() || proxy.noProxyPredicate().apply(finalUri) || clientConfig.relativeUris + String schemeHostPort = clientConfig.relativeUris() ? "" : uri.scheme() + "://" + uri.host() + ":" + uri.port(); nonEntityData.writeAscii(request.method().text() + " " + + schemeHostPort + uri.pathWithQueryAndFragment(request.query(), request.fragment()) + " HTTP/1.1\r\n"); } @@ -94,7 +99,7 @@ ClientResponseHeaders readHeaders(DataReader reader) { clientConfig.maxHeaderSize(), clientConfig.validateHeaders()); - return ClientResponseHeaders.create(writable); + return ClientResponseHeaders.create(writable, clientConfig.mediaTypeParserMode()); } private ClientConnection obtainConnection(WebClientServiceRequest request) { diff --git a/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/HttpClientTest.java b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/HttpClientTest.java index cc66f045e25..25f1c1e428d 100644 --- a/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/HttpClientTest.java +++ b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/HttpClientTest.java @@ -113,6 +113,16 @@ public FakeHttpClientRequest fragment(String fragment) { return this; } + @Override + public FakeHttpClientRequest followRedirects(boolean followRedirects) { + return this; + } + + @Override + public FakeHttpClientRequest maxRedirects(int maxRedirects) { + return this; + } + @Override public FakeHttpClientRequest header(Http.HeaderValue header) { return null; @@ -162,5 +172,15 @@ public FakeHttpClientRequest connection(ClientConnection connection) { public FakeHttpClientRequest proxy(Proxy proxy) { return null; } + + @Override + public FakeHttpClientRequest skipUriEncoding() { + return null; + } + + @Override + public FakeHttpClientRequest property(String propertyName, String propertyValue) { + return null; + } } } diff --git a/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java index 1552a0736d5..27adf7c7675 100644 --- a/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java +++ b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/http1/ClientRequestImplTest.java @@ -17,10 +17,13 @@ package io.helidon.nima.webclient.http1; import java.io.OutputStream; +import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.StringTokenizer; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.stream.Stream; import io.helidon.common.GenericType; import io.helidon.common.buffers.BufferData; @@ -38,13 +41,18 @@ import io.helidon.nima.webclient.WebClient; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import static io.helidon.common.testing.http.junit5.HttpHeaderMatcher.hasHeader; import static io.helidon.common.testing.http.junit5.HttpHeaderMatcher.noHeader; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.params.provider.Arguments.arguments; class ClientRequestImplTest { private static final Http.HeaderValue REQ_CHUNKED_HEADER = Http.Header.createCached( @@ -175,6 +183,45 @@ void testHeadMethod() { http1ClientConnection.close(); } + @Test + void testSkipUrlEncoding() { + //Fill with chars which should be encoded + Http1ClientRequest request = getHttp1ClientRequest(Http.Method.PUT, "/ěščžř") + .queryParam("specialChar+", "someValue,").fragment("someFragment,"); + URI uri = request.resolvedUri(); + assertThat(uri.getRawPath(), is("/%C4%9B%C5%A1%C4%8D%C5%BE%C5%99")); + assertThat(uri.getRawQuery(), is("specialChar%2B=someValue%2C")); + assertThat(uri.getRawFragment(), is("someFragment%2C")); + + request = request.skipUriEncoding(); + uri = request.resolvedUri(); + assertThat(uri.getRawPath(), is("/ěščžř")); + assertThat(uri.getRawQuery(), is("specialChar+=someValue,")); + assertThat(uri.getRawFragment(), is("someFragment,")); + } + + @ParameterizedTest + @MethodSource("relativeUris") + void testRelativeUris(boolean relativeUris, boolean outputStream, String requestUri, String expectedUriStart) { + Http1Client client = WebClient.builder().relativeUris(relativeUris).build(); + FakeHttp1ClientConnection connection = new FakeHttp1ClientConnection(); + Http1ClientRequest request = client.put(requestUri); + request.connection(connection); + Http1ClientResponse response; + if (outputStream) { + response = getHttp1ClientResponseFromOutputStream(request, new String[] {"Sending Something"}); + } else { + response = request.submit("Sending Something"); + } + + assertThat(response.status(), is(Http.Status.OK_200)); + StringTokenizer st = new StringTokenizer(connection.getPrologue(), " "); + // skip method part + st.nextToken(); + // Validate URI part + assertThat(st.nextToken(), startsWith(expectedUriStart)); + } + private static void validateSuccessfulResponse(Http1Client client, ClientConnection connection) { String requestEntity = "Sending Something"; Http1ClientRequest request = client.put("http://localhost:" + dummyPort + "/test"); @@ -230,6 +277,28 @@ private static Http1ClientResponse getHttp1ClientResponseFromOutputStream(Http1C return response; } + private static Stream relativeUris() { + return Stream.of( + // OutputStream (chunk request) + arguments(false, true, "http://www.dummy.com/test", "http://www.dummy.com:80/"), + arguments(false, true, "http://www.dummy.com:1111/test", "http://www.dummy.com:1111/"), + arguments(false, true, "https://www.dummy.com/test", "https://www.dummy.com:443/"), + arguments(false, true, "https://www.dummy.com:1111/test", "https://www.dummy.com:1111/"), + arguments(true, true, "http://www.dummy.com/test", "/test"), + arguments(true, true, "http://www.dummy.com:1111/test", "/test"), + arguments(true, true, "https://www.dummy.com/test", "/test"), + arguments(true, true, "https://www.dummy.com:1111/test", "/test"), + // non-OutputStream (single entity request) + arguments(false, false, "http://www.dummy.com/test", "http://www.dummy.com:80/"), + arguments(false, false, "http://www.dummy.com:1111/test", "http://www.dummy.com:1111/"), + arguments(false, false, "https://www.dummy.com/test", "https://www.dummy.com:443/"), + arguments(false, false, "https://www.dummy.com:1111/test", "https://www.dummy.com:1111/"), + arguments(true, false, "http://www.dummy.com/test", "/test"), + arguments(true, false, "http://www.dummy.com:1111/test", "/test"), + arguments(true, false, "https://www.dummy.com/test", "/test"), + arguments(true, false, "https://www.dummy.com:1111/test", "/test")); + } + private static class FakeHttp1ClientConnection implements ClientConnection { private final DataReader clientReader; private final DataWriter clientWriter; @@ -237,6 +306,7 @@ private static class FakeHttp1ClientConnection implements ClientConnection { private final DataWriter serverWriter; private Throwable serverException; private ExecutorService webServerEmulator; + private String prologue; FakeHttp1ClientConnection() { ArrayBlockingQueue serverToClient = new ArrayBlockingQueue<>(1024); @@ -272,6 +342,11 @@ public String channelId() { return null; } + // This will be used for testing the element of Prologue + String getPrologue() { + return prologue; + } + private DataWriter writer(ArrayBlockingQueue queue) { return new DataWriter() { @Override @@ -343,8 +418,8 @@ private void webServerHandle() { // Read prologue int lineLength = serverReader.findNewLine(16384); if (lineLength > 0) { - //String prologue = serverReader.readAsciiString(lineLength); - serverReader.skip(lineLength + 2); // skip Prologue + CRLF + prologue = serverReader.readAsciiString(lineLength); + serverReader.skip(2); // skip CRLF } // Read Headers @@ -432,8 +507,9 @@ public void write(GenericType type, Headers requestHeaders, WritableHeaders responseHeaders) { if (object instanceof String) { - String maxLen5 = ((String) object).substring(0, 5); - impl.write(type, (T) maxLen5, outputStream, requestHeaders, responseHeaders); + @SuppressWarnings("unchecked") + final T maxLen5 = (T)((String) object).substring(0, 5); + impl.write(type, maxLen5, outputStream, requestHeaders, responseHeaders); } else { impl.write(type, object, outputStream, requestHeaders, responseHeaders); } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/ServerResponse.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/ServerResponse.java index d791d098dfa..ae562e8cf56 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/ServerResponse.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http/ServerResponse.java @@ -39,6 +39,16 @@ public interface ServerResponse { */ ServerResponse status(Http.Status status); + /** + * Status of the response. + * + * @param status HTTP status as integer + * @return this instance + */ + default ServerResponse status(int status) { + return status(Http.Status.create(status)); + } + /** * Configured HTTP status, if not configured, returns {@link Http.Status#OK_200}. * diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java index 66312047bf7..3e9dbfb2147 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java @@ -364,7 +364,8 @@ private void handleRequestException(RequestException e) { e.eventType(), e.status(), e.responseHeaders(), - e); + e, + LOGGER); BufferData buffer = BufferData.growing(128); ServerResponseHeaders headers = response.headers(); diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Headers.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Headers.java index e0e75fc05b3..d59701aee24 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Headers.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Headers.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Prologue.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Prologue.java index f64b45640e6..7e0fcfd7a9e 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Prologue.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Prologue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,6 +74,7 @@ private static RequestException badRequest(String message, String method, String .type(DirectHandler.EventType.BAD_REQUEST) .request(DirectTransportRequest.create(protocolAndVersion, method, path)) .message(message) + .safeMessage(false) .build(); } diff --git a/nima/websocket/client/src/main/java/io/helidon/nima/websocket/client/WsClient.java b/nima/websocket/client/src/main/java/io/helidon/nima/websocket/client/WsClient.java index 0c5ed21e0bf..6e1d1c91cde 100644 --- a/nima/websocket/client/src/main/java/io/helidon/nima/websocket/client/WsClient.java +++ b/nima/websocket/client/src/main/java/io/helidon/nima/websocket/client/WsClient.java @@ -22,9 +22,7 @@ import java.util.List; import java.util.Objects; -import io.helidon.common.http.Headers; import io.helidon.common.http.Http; -import io.helidon.common.http.WritableHeaders; import io.helidon.nima.webclient.DefaultDnsResolverProvider; import io.helidon.nima.webclient.DnsAddressLookup; import io.helidon.nima.webclient.WebClient; @@ -76,7 +74,6 @@ class Builder extends WebClient.Builder { private static final Http.HeaderValue HEADER_WS_VERSION = Http.Header.createCached(Http.Header.create( "Sec-WebSocket-Version"), SUPPORTED_VERSION); private final List subprotocols = new ArrayList<>(); - private final WritableHeaders headers = WritableHeaders.create(); private Builder() { // until we use the same parent for HTTP/1 and websocket, we need to have these defined as defaults @@ -85,15 +82,15 @@ private Builder() { } @Override - public WsClient build() { + public WsClient doBuild() { // these headers cannot be modified by user - headers.set(HEADER_UPGRADE_WS); - headers.set(HEADER_WS_VERSION); - headers.set(Http.HeaderValues.CONTENT_LENGTH_ZERO); + header(HEADER_UPGRADE_WS); + header(HEADER_WS_VERSION); + header(Http.HeaderValues.CONTENT_LENGTH_ZERO); if (subprotocols.isEmpty()) { - headers.remove(HEADER_WS_PROTOCOL); + removeHeader(HEADER_WS_PROTOCOL); } else { - headers.set(HEADER_WS_PROTOCOL, subprotocols); + header(HEADER_WS_PROTOCOL, subprotocols); } return new WsClientImpl(this); @@ -124,21 +121,5 @@ public Builder subProtocols(String... preferred) { Collections.addAll(subprotocols, preferred); return this; } - - /** - * Configure a custom header to be sent. Some headers cannot be modified (Upgrade, WebSocket version, Content Length). - * - * @param header header to add - * @return updated builder instance - */ - public Builder header(Http.HeaderValue header) { - Objects.requireNonNull(header); - headers.set(header); - return this; - } - - Headers headers() { - return headers; - } } } diff --git a/nima/websocket/client/src/main/java/io/helidon/nima/websocket/client/WsClientImpl.java b/nima/websocket/client/src/main/java/io/helidon/nima/websocket/client/WsClientImpl.java index fee5c75cd6d..79d4ce3bc8d 100644 --- a/nima/websocket/client/src/main/java/io/helidon/nima/websocket/client/WsClientImpl.java +++ b/nima/websocket/client/src/main/java/io/helidon/nima/websocket/client/WsClientImpl.java @@ -41,7 +41,6 @@ import io.helidon.common.buffers.Bytes; import io.helidon.common.buffers.DataReader; import io.helidon.common.buffers.DataWriter; -import io.helidon.common.http.Headers; import io.helidon.common.http.Http; import io.helidon.common.http.Http.Header; import io.helidon.common.http.Http1HeadersParser; @@ -68,11 +67,8 @@ class WsClientImpl extends LoomClient implements WsClient { private static final int KEY_SUFFIX_LENGTH = KEY_SUFFIX.length; private static final Base64.Encoder B64_ENCODER = Base64.getEncoder(); - private final Headers headers; - protected WsClientImpl(WsClient.Builder builder) { super(builder); - this.headers = WritableHeaders.create(builder.headers()); } @Override @@ -176,7 +172,7 @@ private void finishConnect(String channelId, HelidonSocket helidonSocket, SSLSoc /* Prepare headers */ - WritableHeaders headers = WritableHeaders.create(this.headers); + WritableHeaders headers = WritableHeaders.create(defaultHeaders()); byte[] nonce = new byte[16]; RANDOM.get().nextBytes(nonce); String secWsKey = B64_ENCODER.encodeToString(nonce); diff --git a/openapi/etc/spotbugs/exclude.xml b/openapi/etc/spotbugs/exclude.xml new file mode 100644 index 00000000000..e670cfe664d --- /dev/null +++ b/openapi/etc/spotbugs/exclude.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/openapi/pom.xml b/openapi/pom.xml index 0a236d65111..8421dd62359 100644 --- a/openapi/pom.xml +++ b/openapi/pom.xml @@ -36,32 +36,13 @@ jar - ${project.build.directory}/extracted-sources/openapi-interfaces - ${project.build.directory}/extracted-sources/openapi-impls + etc/spotbugs/exclude.xml - io.smallrye - smallrye-open-api-core - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.dataformat - jackson-dataformat-yaml - - - - - io.smallrye - smallrye-open-api-jaxrs + io.helidon.nima.service-common + helidon-nima-service-common jakarta.json @@ -84,6 +65,10 @@ io.helidon.common helidon-common-media-type + + io.helidon.nima.webserver + helidon-nima-webserver + io.helidon.config helidon-config-metadata @@ -104,18 +89,6 @@ parsson runtime - - org.yaml - snakeyaml - - - org.jboss - jandex - - - org.eclipse.microprofile.openapi - microprofile-openapi-api - org.junit.jupiter junit-jupiter-api @@ -131,70 +104,20 @@ helidon-config-yaml test + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-params + test + - - org.apache.maven.plugins - maven-dependency-plugin - - - unpack-openapi-interfaces - - unpack-dependencies - - generate-sources - - sources - true - ${openapi-interfaces-dir} - org.eclipse.microprofile.openapi - microprofile-openapi-api - org/eclipse/microprofile/openapi/models/**/*.java - - - - unpack-openapi-impls - - unpack-dependencies - - generate-sources - - sources - true - ${openapi-impls-dir} - io.smallrye - smallrye-open-api-core - io/smallrye/openapi/api/models/**/*.java - - - - - - io.helidon.build-tools - snakeyaml-codegen-maven-plugin - - - generate-snakeyaml-parsing-helper - - generate - - generate-sources - - io.helidon.openapi.SnakeYAMLParserHelper - - ${openapi-interfaces-dir} - - - ${openapi-impls-dir} - - io.smallrye - org.eclipse.microprofile.openapi - - - - org.apache.maven.plugins maven-surefire-plugin diff --git a/openapi/src/main/java/io/helidon/openapi/OpenAPIMediaType.java b/openapi/src/main/java/io/helidon/openapi/OpenAPIMediaType.java deleted file mode 100644 index 84bb6cbef06..00000000000 --- a/openapi/src/main/java/io/helidon/openapi/OpenAPIMediaType.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.openapi; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import io.helidon.common.media.type.MediaType; -import io.helidon.common.media.type.MediaTypes; - -import io.smallrye.openapi.runtime.io.Format; - -/** - * Abstraction of the different representations of a static OpenAPI document - * file and the file type(s) they correspond to. - *

    - * Each {@code OpenAPIMediaType} stands for a single format (e.g., yaml, - * json). That said, each can map to multiple file types (e.g., yml and - * yaml) and multiple actual media types (the proposed OpenAPI media type - * vnd.oai.openapi and various other YAML types proposed or in use). - */ -public enum OpenAPIMediaType { - /** - * JSON media type. - */ - JSON(Format.JSON, - new MediaType[] {MediaTypes.APPLICATION_OPENAPI_JSON, - MediaTypes.APPLICATION_JSON}, - "json"), - /** - * YAML media type. - */ - YAML(Format.YAML, - new MediaType[] {MediaTypes.APPLICATION_OPENAPI_YAML, - MediaTypes.APPLICATION_X_YAML, - MediaTypes.APPLICATION_YAML, - MediaTypes.TEXT_PLAIN, - MediaTypes.TEXT_X_YAML, - MediaTypes.TEXT_YAML}, - "yaml", "yml"); - - /** - * Default media type (YAML). - */ - public static final OpenAPIMediaType DEFAULT_TYPE = YAML; - - static final String TYPE_LIST = "json|yaml|yml"; // must be a true constant so it can be used in an annotation - - private final Format format; - private final List fileTypes; - private final List mediaTypes; - - OpenAPIMediaType(Format format, MediaType[] mediaTypes, String... fileTypes) { - this.format = format; - this.mediaTypes = Arrays.asList(mediaTypes); - this.fileTypes = new ArrayList<>(Arrays.asList(fileTypes)); - } - - /** - * Format associated with this media type. - * @return format - */ - public Format format() { - return format; - } - - /** - * File types matching this media type. - * @return file types - */ - public List matchingTypes() { - return fileTypes; - } - - /** - * Find media type by file suffix. - * - * @param fileType file suffix - * @return media type or empty if not supported - */ - public static Optional byFileType(String fileType) { - for (OpenAPIMediaType candidateType : values()) { - if (candidateType.matchingTypes().contains(fileType)) { - return Optional.of(candidateType); - } - } - return Optional.empty(); - } - - /** - * Find OpenAPI media type by media type. - * @param mt media type - * @return OpenAPI media type or empty if not supported - */ - public static Optional byMediaType(MediaType mt) { - for (OpenAPIMediaType candidateType : values()) { - if (candidateType.mediaTypes.contains(mt)) { - return Optional.of(candidateType); - } - } - return Optional.empty(); - } - - /** - * List of all supported file types. - * - * @return file types - */ - public static List recognizedFileTypes() { - final List result = new ArrayList<>(); - for (OpenAPIMediaType type : values()) { - result.addAll(type.fileTypes); - } - return result; - } - - /** - * Media types we recognize as OpenAPI, in order of preference. - * - * @return MediaTypes in order that we recognize them as OpenAPI - * content. - */ - public static MediaType[] preferredOrdering() { - return new MediaType[] { - MediaTypes.APPLICATION_OPENAPI_YAML, - MediaTypes.APPLICATION_X_YAML, - MediaTypes.APPLICATION_YAML, - MediaTypes.APPLICATION_OPENAPI_JSON, - MediaTypes.APPLICATION_JSON, - MediaTypes.TEXT_X_YAML, - MediaTypes.TEXT_YAML, - MediaTypes.TEXT_PLAIN - }; - } -} diff --git a/openapi/src/main/java/io/helidon/openapi/OpenAPIParser.java b/openapi/src/main/java/io/helidon/openapi/OpenAPIParser.java deleted file mode 100644 index 398cc3ea083..00000000000 --- a/openapi/src/main/java/io/helidon/openapi/OpenAPIParser.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.openapi; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.nio.charset.StandardCharsets; -import java.util.Map; - -import org.eclipse.microprofile.openapi.models.OpenAPI; -import org.yaml.snakeyaml.TypeDescription; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.Constructor; - -/** - * Abstraction for SnakeYAML parsing of JSON and YAML. - */ -public final class OpenAPIParser { - - private OpenAPIParser() { - } - - /** - * Parse open API. - * - * @param types types - * @param inputStream input stream to parse from - * @return parsed document - * @throws IOException in case of I/O problems - */ - public static OpenAPI parse(Map, ExpandedTypeDescription> types, InputStream inputStream) throws IOException { - try (Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { - return parse(types, reader); - } - } - - static OpenAPI parse(Map, ExpandedTypeDescription> types, Reader reader) { - TypeDescription openAPITD = types.get(OpenAPI.class); - Constructor topConstructor = new CustomConstructor(openAPITD); - - types.values() - .forEach(topConstructor::addTypeDescription); - - Yaml yaml = new Yaml(topConstructor); - OpenAPI result = yaml.loadAs(reader, OpenAPI.class); - return result; - } -} diff --git a/openapi/src/main/java/io/helidon/openapi/OpenApiFeature.java b/openapi/src/main/java/io/helidon/openapi/OpenApiFeature.java new file mode 100644 index 00000000000..073bbaa2b88 --- /dev/null +++ b/openapi/src/main/java/io/helidon/openapi/OpenApiFeature.java @@ -0,0 +1,541 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.openapi; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +import io.helidon.common.http.Http; +import io.helidon.common.http.HttpMediaType; +import io.helidon.common.http.ServerRequestHeaders; +import io.helidon.common.http.WritableHeaders; +import io.helidon.common.media.type.MediaType; +import io.helidon.common.media.type.MediaTypes; +import io.helidon.config.Config; +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.nima.servicecommon.HelidonFeatureSupport; +import io.helidon.nima.webserver.http.HttpRules; +import io.helidon.nima.webserver.http.HttpService; +import io.helidon.nima.webserver.http.ServerRequest; +import io.helidon.nima.webserver.http.ServerResponse; + +/** + * Behavior shared between the SE and MP OpenAPI feature implementations. + */ +public abstract class OpenApiFeature extends HelidonFeatureSupport { + + /** + * Default media type used in responses in absence of incoming Accept + * header. + */ + public static final MediaType DEFAULT_RESPONSE_MEDIA_TYPE = MediaTypes.APPLICATION_OPENAPI_YAML; + + /** + * Feature name for OpenAPI. + */ + public static final String FEATURE_NAME = "OpenAPI"; + + /** + * Default web context for the endpoint. + */ + public static final String DEFAULT_CONTEXT = "/openapi"; + + /** + * URL query parameter for specifying the requested format when retrieving the OpenAPI document. + */ + static final String OPENAPI_ENDPOINT_FORMAT_QUERY_PARAMETER = "format"; + + /** + * Abstraction of the different representations of a static OpenAPI document + * file and the file type(s) they correspond to. + *

    + * Each {@code OpenAPIMediaType} stands for a single format (e.g., yaml, + * json). That said, each can map to multiple file types (e.g., yml and + * yaml) and multiple actual media types (the proposed OpenAPI media type + * vnd.oai.openapi and various other YAML types proposed or in use). + */ + public enum OpenAPIMediaType { + /** + * JSON media type. + */ + JSON(new MediaType[] {MediaTypes.APPLICATION_OPENAPI_JSON, + MediaTypes.APPLICATION_JSON}, + "json"), + /** + * YAML media type. + */ + YAML(new MediaType[] {MediaTypes.APPLICATION_OPENAPI_YAML, + MediaTypes.APPLICATION_X_YAML, + MediaTypes.APPLICATION_YAML, + MediaTypes.TEXT_PLAIN, + MediaTypes.TEXT_X_YAML, + MediaTypes.TEXT_YAML}, + "yaml", "yml"); + + /** + * Default media type (YAML). + */ + public static final OpenAPIMediaType DEFAULT_TYPE = YAML; + + static final String TYPE_LIST = "json|yaml|yml"; // must be a true constant so it can be used in an annotation + + private final List fileTypes; + private final List mediaTypes; + + OpenAPIMediaType(MediaType[] mediaTypes, String... fileTypes) { + this.mediaTypes = Arrays.asList(mediaTypes); + this.fileTypes = new ArrayList<>(Arrays.asList(fileTypes)); + } + + /** + * File types matching this media type. + * @return file types + */ + public List matchingTypes() { + return fileTypes; + } + + /** + * Find media type by file suffix. + * + * @param fileType file suffix + * @return media type or empty if not supported + */ + public static Optional byFileType(String fileType) { + for (OpenAPIMediaType candidateType : values()) { + if (candidateType.matchingTypes().contains(fileType)) { + return Optional.of(candidateType); + } + } + return Optional.empty(); + } + + /** + * Find OpenAPI media type by media type. + * @param mt media type + * @return OpenAPI media type or empty if not supported + */ + public static Optional byMediaType(MediaType mt) { + for (OpenAPIMediaType candidateType : values()) { + if (candidateType.mediaTypes.contains(mt)) { + return Optional.of(candidateType); + } + } + return Optional.empty(); + } + + /** + * List of all supported file types. + * + * @return file types + */ + public static List recognizedFileTypes() { + final List result = new ArrayList<>(); + for (OpenAPIMediaType type : values()) { + result.addAll(type.fileTypes); + } + return result; + } + + /** + * Media types we recognize as OpenAPI, in order of preference. + * + * @return MediaTypes in order that we recognize them as OpenAPI + * content. + */ + public static MediaType[] preferredOrdering() { + return new MediaType[] { + MediaTypes.APPLICATION_OPENAPI_YAML, + MediaTypes.APPLICATION_X_YAML, + MediaTypes.APPLICATION_YAML, + MediaTypes.APPLICATION_OPENAPI_JSON, + MediaTypes.APPLICATION_JSON, + MediaTypes.TEXT_X_YAML, + MediaTypes.TEXT_YAML, + MediaTypes.TEXT_PLAIN + }; + } + } + + /** + * Some logic related to the possible format values as requested in the query + * parameter {@value OPENAPI_ENDPOINT_FORMAT_QUERY_PARAMETER}. + */ + enum QueryParameterRequestedFormat { + JSON(MediaTypes.APPLICATION_JSON), YAML(MediaTypes.APPLICATION_OPENAPI_YAML); + + static QueryParameterRequestedFormat chooseFormat(String format) { + return QueryParameterRequestedFormat.valueOf(format); + } + + private final MediaType mt; + + QueryParameterRequestedFormat(MediaType mt) { + this.mt = mt; + } + + MediaType mediaType() { + return mt; + } + } + + private static final String DEFAULT_STATIC_FILE_PATH_PREFIX = "META-INF/openapi."; + private static final String OPENAPI_EXPLICIT_STATIC_FILE_LOG_MESSAGE_FORMAT = "Using specified OpenAPI static file %s"; + private static final String OPENAPI_DEFAULTED_STATIC_FILE_LOG_MESSAGE_FORMAT = "Using default OpenAPI static file %s"; + + private final OpenApiStaticFile openApiStaticFile; + private final OpenApiUi ui; + private final MediaType[] preferredMediaTypeOrdering; + private final MediaType[] mediaTypesSupportedByUi; + private final ConcurrentMap cachedDocuments = new ConcurrentHashMap<>(); + + /** + * Constructor for the feature. + * + * @param logger logger to use for the feature + * @param builder builder to use for initializing the feature + */ + protected OpenApiFeature(System.Logger logger, Builder builder) { + super(logger, builder, FEATURE_NAME); + openApiStaticFile = builder.staticFile(); + ui = prepareUi(builder); + mediaTypesSupportedByUi = ui.supportedMediaTypes(); + preferredMediaTypeOrdering = preparePreferredMediaTypeOrdering(mediaTypesSupportedByUi); + } + + @Override + public Optional service() { + return enabled() + ? Optional.of(this::configureRoutes) + : Optional.empty(); + } + + /** + * Returns the OpenAPI document content in {@code String} form given the requested media type. + * + * @param openApiMediaType which OpenAPI media type to use for formatting + * @return {@code String} containing the formatted OpenAPI document + */ + protected abstract String openApiContent(OpenAPIMediaType openApiMediaType); + + /** + * Returns the explicitly-assigned or default static content (if any). + *

    + * Most likely invoked by the concrete implementations of {@link #openApiContent(OpenAPIMediaType)} as needed + * to find static content as needed. + *

    + * + * @return an {@code Optional} of the static content + */ + protected Optional staticContent() { + return Optional.ofNullable(openApiStaticFile); + } + + private OpenApiUi prepareUi(Builder builder) { + return builder.uiBuilder.build(this::prepareDocument, context()); + } + + private static MediaType[] preparePreferredMediaTypeOrdering(MediaType[] uiTypesSupported) { + int nonTextLength = OpenAPIMediaType.preferredOrdering().length; + + MediaType[] result = Arrays.copyOf(OpenAPIMediaType.preferredOrdering(), + nonTextLength + uiTypesSupported.length); + System.arraycopy(uiTypesSupported, 0, result, nonTextLength, uiTypesSupported.length); + return result; + } + + private void configureRoutes(HttpRules rules) { + rules.get("/", this::prepareResponse); + } + + private static ClassLoader getContextClassLoader() { + return Thread.currentThread().getContextClassLoader(); + } + + private static String typeFromPath(String staticFileNamePath) { + if (staticFileNamePath == null) { + throw new IllegalArgumentException("File path does not seem to have a file name value but one is expected"); + } + return staticFileNamePath.substring(staticFileNamePath.lastIndexOf(".") + 1); + } + + private void prepareResponse(ServerRequest req, ServerResponse resp) { + + try { + Optional requestedMediaType = chooseResponseMediaType(req); + + // Give the UI a chance to respond first if it claims to support the chosen media type. + if (requestedMediaType.isPresent() + && uiSupportsMediaType(requestedMediaType.get())) { + if (ui.prepareTextResponseFromMainEndpoint(req, resp)) { + return; + } + } + + if (requestedMediaType.isEmpty()) { + logger().log(System.Logger.Level.TRACE, + () -> String.format("Did not recognize requested media type %s; passing the request on", + req.headers().acceptedTypes())); + return; + } + + MediaType resultMediaType = requestedMediaType.get(); + final String openAPIDocument = prepareDocument(resultMediaType); + resp.status(Http.Status.OK_200); + resp.headers().contentType(resultMediaType); + resp.send(openAPIDocument); + } catch (Exception ex) { + resp.status(Http.Status.INTERNAL_SERVER_ERROR_500); + resp.send("Error serializing OpenAPI document; " + ex.getMessage()); + logger().log(System.Logger.Level.ERROR, "Error serializing OpenAPI document", ex); + } + } + + private boolean uiSupportsMediaType(MediaType mediaType) { + HttpMediaType httpMediaType = HttpMediaType.create(mediaType); + // The UI supports a very short list of media types, hence the sequential search. + for (MediaType uiSupportedMediaType : mediaTypesSupportedByUi) { + if (httpMediaType.test(uiSupportedMediaType)) { + return true; + } + } + return false; + } + + /** + * Returns the OpenAPI document in the requested format. + * + * @param resultMediaType requested media type + * @return String containing the formatted OpenAPI document + * from its underlying data + */ + private String prepareDocument(MediaType resultMediaType) { + OpenAPIMediaType matchingOpenApiMediaType + = OpenAPIMediaType.byMediaType(resultMediaType) + .orElseGet(() -> { + logger().log(System.Logger.Level.TRACE, + () -> String.format( + "Requested media type %s not supported; using default", + resultMediaType.toString())); + return OpenAPIMediaType.DEFAULT_TYPE; + }); + + + return cachedDocuments.computeIfAbsent(matchingOpenApiMediaType, + fmt -> { + String r = openApiContent(fmt); + logger().log(System.Logger.Level.TRACE, + "Created and cached OpenAPI document in {0} format", + fmt.toString()); + return r; + }); + } + + private Optional chooseResponseMediaType(ServerRequest req) { + /* + * Response media type default is application/vnd.oai.openapi (YAML) + * unless otherwise specified. + */ + Optional queryParameterFormat = req.query() + .first(OPENAPI_ENDPOINT_FORMAT_QUERY_PARAMETER); + if (queryParameterFormat.isPresent()) { + String queryParameterFormatValue = queryParameterFormat.get(); + try { + return Optional.of(QueryParameterRequestedFormat.chooseFormat(queryParameterFormatValue).mediaType()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "Query parameter 'format' had value '" + + queryParameterFormatValue + + "' but expected " + Arrays.toString(QueryParameterRequestedFormat.values())); + } + } + + ServerRequestHeaders headersToCheck = req.headers(); + if (headersToCheck.acceptedTypes().isEmpty()) { + WritableHeaders writableHeaders = WritableHeaders.create(headersToCheck); + writableHeaders.add(Http.Header.ACCEPT, DEFAULT_RESPONSE_MEDIA_TYPE.toString()); + headersToCheck = ServerRequestHeaders.create(writableHeaders); + } + return headersToCheck + .bestAccepted(preferredMediaTypeOrdering); + } + + + /** + * Behavior shared between the SE and MP OpenAPI feature builders. + * + * @param specific concrete type of the builder + * @param specific concrete type of {@link OpenApiFeature} the builder creates + */ + public abstract static class Builder, T extends OpenApiFeature> + extends HelidonFeatureSupport.Builder { + + /** + * Config key for the OpenAPI section. + */ + public static final String CONFIG_KEY = "openapi"; + + private OpenApiStaticFile staticFile; + + private OpenApiUi.Builder uiBuilder = OpenApiUi.builder(); + + /** + * Constructor for the builder. + */ + protected Builder() { + super(DEFAULT_CONTEXT); + } + + /** + * Returns the logger for the OpenAPI feature instance. + * + * @return logger + */ + protected abstract System.Logger logger(); + + /** + * Apply configuration settings to the builder. + * + * @param config the Helidon config instance + * @return updated builder + */ + public B config(Config config) { + super.config(config); + config.get("static-file") + .asString() + .ifPresent(this::staticFile); + config.get(OpenApiUi.Builder.OPENAPI_UI_CONFIG_KEY) + .ifExists(uiBuilder::config); + return identity(); + } + + /** + * Sets the path of the static OpenAPI document file. Default types are `json`, `yaml`, and `yml`. + * + * @param path non-null location of the static OpenAPI document file + * @return updated builder instance + */ + @ConfiguredOption(value = DEFAULT_STATIC_FILE_PATH_PREFIX + "*") + public B staticFile(String path) { + Objects.requireNonNull(path, "path to static file must be non-null"); + OpenAPIMediaType openApiMediaType = OpenAPIMediaType.byFileType(typeFromPath(path)) + .orElseThrow(() -> new IllegalArgumentException("Static file " + path + " not recognized as YAML or JSON")); + + staticFile = OpenApiStaticFile.create(openApiMediaType, explicitStaticFileContentFromPath(path)); + + return identity(); + } + + /** + * Assigns the OpenAPI UI builder the {@code OpenAPISupport} service should use in preparing the UI. + * + * @param uiBuilder the {@link OpenApiUi.Builder} + * @return updated builder instance + */ + @ConfiguredOption(type = OpenApiUi.class) + public B ui(OpenApiUi.Builder uiBuilder) { + Objects.requireNonNull(uiBuilder, "UI must be non-null"); + this.uiBuilder = uiBuilder; + return identity(); + } + + /** + * Returns the path to a static OpenAPI document file (if any exists), + * either as explicitly set using {@link #staticFile(java.lang.String) } + * or one of the default files. + * + * @return the OpenAPI static file instance for the static file if such + * a file exists, null otherwise + */ + public OpenApiStaticFile staticFile() { + return staticFile == null + ? getDefaultStaticFile() + : staticFile; + } + + private OpenApiStaticFile getDefaultStaticFile() { + OpenApiStaticFile result = null; + final List candidatePaths = logger().isLoggable(System.Logger.Level.TRACE) ? new ArrayList<>() : null; + for (OpenAPIMediaType candidate : OpenAPIMediaType.values()) { + for (String type : candidate.matchingTypes()) { + String candidatePath = DEFAULT_STATIC_FILE_PATH_PREFIX + type; + if (candidatePaths != null) { + candidatePaths.add(candidatePath); + } + String content = defaultStaticFileContentFromPath(candidatePath); + if (content != null) { + result = OpenApiStaticFile.create(candidate, content); + } + } + } + if (candidatePaths != null) { + logger().log(System.Logger.Level.TRACE, + candidatePaths.stream() + .collect(Collectors.joining( + ",", + "No default static OpenAPI description file found; checked [", + "]"))); + } + return result; + } + + private String defaultStaticFileContentFromPath(String candidatePath) { + return staticFileContentFromPath(candidatePath, OPENAPI_DEFAULTED_STATIC_FILE_LOG_MESSAGE_FORMAT); + } + + private String explicitStaticFileContentFromPath(String candidatePath) { + return staticFileContentFromPath(candidatePath, OPENAPI_EXPLICIT_STATIC_FILE_LOG_MESSAGE_FORMAT); + } + + private String staticFileContentFromPath(String candidatePath, String logMessage) { + InputStream is = getContextClassLoader().getResourceAsStream(candidatePath); + if (is != null) { + try (Reader reader = new BufferedReader(new InputStreamReader(is, Charset.defaultCharset()))) { + Path path = Paths.get(candidatePath); + logger().log(System.Logger.Level.TRACE, () -> String.format( + logMessage, + path.toAbsolutePath())); + StringBuilder result = new StringBuilder(); + CharBuffer charBuffer = CharBuffer.allocate(512); + while (reader.read(charBuffer) != -1) { + charBuffer.flip(); + result.append(charBuffer); + } + return result.toString(); + } catch (IOException ex) { + throw new IllegalArgumentException("Error preparing to read from path " + candidatePath, ex); + } + } else { + return null; + } + } + } +} diff --git a/openapi/src/main/java/io/helidon/openapi/OpenApiStaticFile.java b/openapi/src/main/java/io/helidon/openapi/OpenApiStaticFile.java new file mode 100644 index 00000000000..318d8da2db1 --- /dev/null +++ b/openapi/src/main/java/io/helidon/openapi/OpenApiStaticFile.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.openapi; + +import java.util.Objects; + +/** + * Information about a static OpenAPI file bundled with the application. + *

    + * There can be up to one of these for each {@link io.helidon.openapi.OpenApiFeature.OpenAPIMediaType} (YAML and JSON). + *

    + */ +public class OpenApiStaticFile { + + /** + * Creates a new static file instance using the given OpenAPI media type and content. + * + * @param openApiMediaType OpenAPI media type + * @param content text content + * @return static file instance + */ + static OpenApiStaticFile create(OpenApiFeature.OpenAPIMediaType openApiMediaType, String content) { + return new Builder().openApiMediaType(openApiMediaType).content(content).build(); + } + + private OpenApiFeature.OpenAPIMediaType openApiMediaType; + private String content; + + private OpenApiStaticFile(Builder builder) { + this.content = builder.content; + this.openApiMediaType = builder.openApiMediaType; + } + + /** + * Returns the OpenAPI media type of the static content. + * + * @return the OpenAPI media type of the static content + */ + public OpenApiFeature.OpenAPIMediaType openApiMediaType() { + return openApiMediaType; + } + + /** + * Returns the text content of the static file. + * + * @return text static content + */ + public String content() { + return content; + } + + static class Builder implements io.helidon.common.Builder { + + private OpenApiFeature.OpenAPIMediaType openApiMediaType; + private String content; + + @Override + public OpenApiStaticFile build() { + Objects.requireNonNull(openApiMediaType, "openApiMediaType"); + Objects.requireNonNull(content, "content"); + return new OpenApiStaticFile(this); + } + + Builder openApiMediaType(OpenApiFeature.OpenAPIMediaType openApiMediaType) { + this.openApiMediaType = openApiMediaType; + return this; + } + + Builder content(String content) { + this.content = content; + return this; + } + } + + void content(String content) { + this.content = content; + } +} diff --git a/openapi/src/main/java/io/helidon/openapi/OpenApiUi.java b/openapi/src/main/java/io/helidon/openapi/OpenApiUi.java new file mode 100644 index 00000000000..799d441475c --- /dev/null +++ b/openapi/src/main/java/io/helidon/openapi/OpenApiUi.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.openapi; + +import java.util.Map; +import java.util.function.Function; + +import io.helidon.common.http.HttpMediaType; +import io.helidon.common.media.type.MediaType; +import io.helidon.config.Config; +import io.helidon.config.metadata.Configured; +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.nima.webserver.http.HttpService; +import io.helidon.nima.webserver.http.ServerRequest; +import io.helidon.nima.webserver.http.ServerResponse; + +/** + * Behavior for OpenAPI UI implementations. + */ +public interface OpenApiUi extends HttpService { + + /** + * Default subcontext within the {@link OpenApiFeature} instance's web context + * (which itself defaults to {@value OpenApiFeature#DEFAULT_CONTEXT}. + */ + String UI_WEB_SUBCONTEXT = "/ui"; + + /** + * Creates a builder for a new {@code OpenApiUi} instance. + * + * @return new builder + */ + static Builder builder() { + return OpenApiUiBase.builder(); + } + + /** + * Indicates the media types the UI implementation itself supports. + * + * @return the media types the + * {@link #prepareTextResponseFromMainEndpoint(io.helidon.nima.webserver.http.ServerRequest, io.helidon.nima.webserver.http.ServerResponse)} + * method responds to + */ + HttpMediaType[] supportedMediaTypes(); + + /** + * Gives the UI an opportunity to respond to a request arriving at the {@code OpenAPISupport} endpoint for which the + * best-accepted {@link MediaType} was {@code text/html}. + *

    + * An implementation should return {@code true} if it is responsible for a particular media type + * whether it handled the request itself or delegated the request to the next handler. + * For example, even if the implementation is disabled it should still return {@code true} for the HTML media type. + *

    + * + * @param request the request for HTML content + * @param response the response which could be prepared and sent + * @return whether the UI did respond to the request + */ + boolean prepareTextResponseFromMainEndpoint(ServerRequest request, ServerResponse response); + + /** + * Builder for an {@code OpenApiUi}. + * + * @param type of the {@code OpenApiUi} to be build + * @param type of the builder for T + */ + @Configured(prefix = Builder.OPENAPI_UI_CONFIG_KEY) + interface Builder, T extends OpenApiUi> extends io.helidon.common.Builder { + + /** + * Config prefix within the {@value OpenApiFeature.Builder#CONFIG_KEY} section containing UI settings. + */ + String OPENAPI_UI_CONFIG_KEY = "ui"; + + /** + * Config key for the {@code enabled} setting. + */ + String ENABLED_CONFIG_KEY = "enabled"; + + /** + * Config key for implementation-dependent {@code options} settings. + */ + String OPTIONS_CONFIG_KEY = "options"; + + /** + * Config key for specifying the entire web context where the UI responds. + */ + String WEB_CONTEXT_CONFIG_KEY = "web-context"; + + /** + * Merges implementation-specific UI options. + * + * @param options the options to for the UI to merge + * @return updated builder + */ + @ConfiguredOption(kind = ConfiguredOption.Kind.MAP) + B options(Map options); + + /** + * Sets whether the UI should be enabled. + * + * @param isEnabled true/false + * @return updated builder + */ + @ConfiguredOption(key = "enabled", value = "true") + B isEnabled(boolean isEnabled); + + /** + * Sets the entire web context (not just the suffix) where the UI response. + * + * @param webContext entire web context (path) where the UI responds + * @return updated builder + */ + @ConfiguredOption(description = "web context (path) where the UI will respond") + B webContext(String webContext); + + /** + * Updates the builder using the specified config node at {@value OPENAPI_UI_CONFIG_KEY} within the + * {@value io.helidon.openapi.OpenApiFeature.Builder#CONFIG_KEY} config section. + * + * @param uiConfig config node containing the UI settings + * @return updated builder + */ + default B config(Config uiConfig) { + uiConfig.get(ENABLED_CONFIG_KEY).asBoolean().ifPresent(this::isEnabled); + uiConfig.get(WEB_CONTEXT_CONFIG_KEY).asString().ifPresent(this::webContext); + uiConfig.get(OPTIONS_CONFIG_KEY).detach().asMap().ifPresent(this::options); + return identity(); + } + + /** + * + * @return correctly-typed self + */ + @SuppressWarnings("unchecked") + default B identity() { + return (B) this; + } + + /** + * Assigns how the OpenAPI UI can obtain a formatted document for a given media type. + *

    + * Developers typically do not invoke this method. Helidon invokes it internally. + *

    + * + * @param documentPreparer the function for obtaining the formatted document + * @return updated builder + */ + B documentPreparer(Function documentPreparer); + + /** + * Assigns the web context the {@code OpenAPISupport} instance uses. + *

    + * Developers typically do not invoke this method. Helidon invokes it internally. + *

    + * @param openApiWebContext the web context used by the {@code OpenAPISupport} service + * @return updated builder + */ + B openApiSupportWebContext(String openApiWebContext); + + /** + * Creates a new {@link OpenApiUi} from the builder. + * + * @param documentPreparer function which converts a {@link MediaType} into the corresponding expression of the OpenAPI + * document + * @param openAPIWebContext web context for the OpenAPI instance + * @return new {@code OpenApiUi} + */ + default OpenApiUi build(Function documentPreparer, String openAPIWebContext) { + documentPreparer(documentPreparer); + openApiSupportWebContext(openAPIWebContext); + return build(); + } + } +} diff --git a/openapi/src/main/java/io/helidon/openapi/OpenApiUiBase.java b/openapi/src/main/java/io/helidon/openapi/OpenApiUiBase.java new file mode 100644 index 00000000000..cf7c8d54d06 --- /dev/null +++ b/openapi/src/main/java/io/helidon/openapi/OpenApiUiBase.java @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.openapi; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.function.Function; + +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.LazyValue; +import io.helidon.common.http.Http; +import io.helidon.common.http.HttpMediaType; +import io.helidon.common.media.type.MediaType; +import io.helidon.common.media.type.MediaTypes; +import io.helidon.common.uri.UriQuery; +import io.helidon.nima.webserver.http.ServerRequest; +import io.helidon.nima.webserver.http.ServerResponse; + +/** + * Common base class for implementations of @link OpenApiUi}. + */ +public abstract class OpenApiUiBase implements OpenApiUi { + + private static final System.Logger LOGGER = System.getLogger(OpenApiUiBase.class.getName()); + + private static final LazyValue> UI_FACTORY = LazyValue.create(OpenApiUiBase::loadUiFactory); + + private static final String HTML_PREFIX = """ + + + + + OpenAPI Document + + +
    +            """;
    +    private static final String HTML_SUFFIX = """
    +                    
    + + + """; + private final Map preparedDocuments = new HashMap<>(); + + /** + * Returns a builder for the UI. + * + * @return a builder for the currently-available implementation of {@link io.helidon.openapi.OpenApiUi}. + */ + static OpenApiUi.Builder builder() { + return UI_FACTORY.get().builder(); + } + + private final boolean isEnabled; + private final Function documentPreparer; + private final String webContext; + private final Map options = new HashMap<>(); + + /** + * Creates a new UI implementation from the specified builder and document preparer. + * + * @param builder the builder containing relevant settings + * @param documentPreparer function returning an OpenAPI document represented as a specified {@link MediaType} + * @param openAPIWebContext final web context for the {@code OpenAPISupport} service + */ + protected OpenApiUiBase(Builder builder, Function documentPreparer, String openAPIWebContext) { + Objects.requireNonNull(builder.documentPreparer, "Builder's documentPreparer must be non-null"); + Objects.requireNonNull(builder.openApiSupportWebContext, + "Builder's OpenAPISupport web context must be non-null"); + this.documentPreparer = documentPreparer; + isEnabled = builder.isEnabled; + webContext = Objects.requireNonNullElse(builder.webContext, + openAPIWebContext + OpenApiUi.UI_WEB_SUBCONTEXT); + options.putAll(builder.options); + } + + /** + * Returns whether the UI is enabled. + * + * @return whether the UI is enabled + */ + protected boolean isEnabled() { + return isEnabled; + } + + /** + * Prepares a representation of the OpenAPI document in the specified media type. + * + * @param mediaType media type in which to express the document + * @return representation of the OpenAPI document + */ + protected String prepareDocument(MediaType mediaType) { + return documentPreparer.apply(mediaType); + } + + /** + * Returns the web context for the UI. + * + * @return web context this UI implementation responds at + */ + protected String webContext() { + return webContext; + } + + /** + * Returns the options set for the UI. + * + * @return options set for this UI implementation (unmodifiable) + */ + protected Map options() { + return Collections.unmodifiableMap(options); + } + + /** + * Sends a static text response of the given media type. + * + * @param request the request to respond to + * @param response the response + * @param mediaType the {@code MediaType} with which to respond, if possible + * @return whether the implementation responded with a static text response + */ + protected boolean sendStaticText(ServerRequest request, ServerResponse response, HttpMediaType mediaType) { + try { + response + .header(Http.Header.CONTENT_TYPE, mediaType.toString()) + .send(prepareDocument(request.query(), mediaType)); + } catch (IOException e) { + LOGGER.log(System.Logger.Level.WARNING, "Error formatting OpenAPI output as " + mediaType, e); + response.status(Http.Status.INTERNAL_SERVER_ERROR_500) + .send("Error formatting OpenAPI output. See server log."); + } + return true; + } + + private static OpenApiUiFactory loadUiFactory() { + return HelidonServiceLoader.builder(ServiceLoader.load(OpenApiUiFactory.class)) + .addService(OpenApiUiNoOpFactory.create(), Integer.MAX_VALUE) + .build() + .iterator() + .next(); + } + + private String prepareDocument(UriQuery queryParameters, HttpMediaType mediaType) throws IOException { + String result = null; + if (preparedDocuments.containsKey(mediaType)) { + return preparedDocuments.get(mediaType); + } + MediaType resultMediaType = queryParameters + .first(OpenApiFeature.OPENAPI_ENDPOINT_FORMAT_QUERY_PARAMETER) + .map(OpenApiFeature.QueryParameterRequestedFormat::chooseFormat) + .map(OpenApiFeature.QueryParameterRequestedFormat::mediaType) + .orElse(mediaType); + + result = prepareDocument(resultMediaType); + if (mediaType.test(MediaTypes.TEXT_HTML)) { + result = embedInHtml(result); + } + preparedDocuments.put(resultMediaType, result); + return result; + } + + private String embedInHtml(String text) { + return HTML_PREFIX + text + HTML_SUFFIX; + } + + /** + * Common base builder implementation for creating a new {@code OpenApiUi}. + * + * @param type of the {@code OpenApiUiBase} to be built + * @param type of the builder for T + */ + public abstract static class Builder, T extends OpenApiUi> implements OpenApiUi.Builder { + + private final Map options = new HashMap<>(); + private boolean isEnabled = true; + private String webContext; + private Function documentPreparer; + private String openApiSupportWebContext; + + /** + * Creates a new instance. + */ + protected Builder() { + } + @Override + public B options(Map options) { + this.options.putAll(options); + return identity(); + } + + @Override + public B isEnabled(boolean isEnabled) { + this.isEnabled = isEnabled; + return identity(); + } + + @Override + public B webContext(String webContext) { + this.webContext = webContext; + return identity(); + } + + @Override + public B documentPreparer(Function documentPreparer) { + this.documentPreparer = documentPreparer; + return identity(); + } + + @Override + public B openApiSupportWebContext(String openApiWebContext) { + this.openApiSupportWebContext = openApiWebContext; + return identity(); + } + + /** + * Returns the web context for OpenAPI support. + * + * @return OpenAPI web context + */ + public String openApiSupportWebContext() { + return openApiSupportWebContext; + } + + /** + * Returns the document preparer for the UI. + * + * @return document preparer + */ + public Function documentPreparer() { + return documentPreparer; + } + + /** + * Returns options settings for the UI. + * + * @return options for the UI + */ + protected Map options() { + return options; + } + } +} diff --git a/examples/webserver/jersey/src/main/java/io/helidon/reactive/webserver/examples/jersey/package-info.java b/openapi/src/main/java/io/helidon/openapi/OpenApiUiFactory.java similarity index 50% rename from examples/webserver/jersey/src/main/java/io/helidon/reactive/webserver/examples/jersey/package-info.java rename to openapi/src/main/java/io/helidon/openapi/OpenApiUiFactory.java index 739e8868e04..94e7d34fd3e 100644 --- a/examples/webserver/jersey/src/main/java/io/helidon/reactive/webserver/examples/jersey/package-info.java +++ b/openapi/src/main/java/io/helidon/openapi/OpenApiUiFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package io.helidon.openapi; /** - * An example of Jersey integration into the Web Server. + * Behavior for factories able to provide new builders of {@link io.helidon.openapi.OpenApiUi} instances. * - * @see io.helidon.reactive.webserver.jersey.JerseySupport - * @see io.helidon.reactive.webserver.WebServer + * @param type of the {@link io.helidon.openapi.OpenApiUi} to be built + * @param type of the builder for T */ -package io.helidon.reactive.webserver.examples.jersey; +public interface OpenApiUiFactory, T extends OpenApiUi> { + + /** + * Returns a builder for the UI. + * + * @return a builder for the selected type of concrete {@link io.helidon.openapi.OpenApiUi}. + */ + B builder(); +} diff --git a/openapi/src/main/java/io/helidon/openapi/OpenApiUiNoOp.java b/openapi/src/main/java/io/helidon/openapi/OpenApiUiNoOp.java new file mode 100644 index 00000000000..a983918807d --- /dev/null +++ b/openapi/src/main/java/io/helidon/openapi/OpenApiUiNoOp.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.openapi; + +import io.helidon.common.http.HttpMediaType; +import io.helidon.nima.webserver.http.HttpRules; +import io.helidon.nima.webserver.http.ServerRequest; +import io.helidon.nima.webserver.http.ServerResponse; + +/** + * Implementation of {@link io.helidon.openapi.OpenApiUi} which provides no UI support but simply honors the interface. + */ +class OpenApiUiNoOp implements OpenApiUi { + + private static final HttpMediaType[] SUPPORTED_TEXT_MEDIA_TYPES_AT_OPENAPI_ENDPOINT = new HttpMediaType[0]; + /** + * + * @return new builder for an {@code OpenApiUiNoOp} service + */ + static Builder builder() { + return new Builder(); + } + + private OpenApiUiNoOp(Builder builder) { + } + + @Override + public void routing(HttpRules rules) { + } + + @Override + public HttpMediaType[] supportedMediaTypes() { + return SUPPORTED_TEXT_MEDIA_TYPES_AT_OPENAPI_ENDPOINT; + } + + @Override + public boolean prepareTextResponseFromMainEndpoint(ServerRequest request, ServerResponse response) { + return false; + } + + static class Builder extends OpenApiUiBase.Builder { + + @Override + public OpenApiUiNoOp build() { + return new OpenApiUiNoOp(this); + } + } +} diff --git a/openapi/src/main/java/io/helidon/openapi/OpenApiUiNoOpFactory.java b/openapi/src/main/java/io/helidon/openapi/OpenApiUiNoOpFactory.java new file mode 100644 index 00000000000..49e8bc9b4ca --- /dev/null +++ b/openapi/src/main/java/io/helidon/openapi/OpenApiUiNoOpFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.openapi; + +/** + * Factory providing builders for {@link io.helidon.openapi.OpenApiUiNoOp} implementations. + */ +public class OpenApiUiNoOpFactory implements OpenApiUiFactory { + + /** + * Returns a new no-op UI factory. + * + * @return new instance of the factory for a minimal implementation of the UI + */ + static OpenApiUiNoOpFactory create() { + return new OpenApiUiNoOpFactory(); + } + + /** + * Creates a new instance of the no-op factory. + */ + public OpenApiUiNoOpFactory() { + } + + @Override + public OpenApiUiNoOp.Builder builder() { + return OpenApiUiNoOp.builder(); + } +} diff --git a/openapi/src/main/java/io/helidon/openapi/internal/OpenAPIConfigImpl.java b/openapi/src/main/java/io/helidon/openapi/internal/OpenAPIConfigImpl.java deleted file mode 100644 index cb3684d4b20..00000000000 --- a/openapi/src/main/java/io/helidon/openapi/internal/OpenAPIConfigImpl.java +++ /dev/null @@ -1,509 +0,0 @@ -/* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.openapi.internal; - -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.regex.Pattern; - -import io.helidon.common.config.Config; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; - -import io.smallrye.openapi.api.OpenApiConfig; - -/** - * Helidon-specific implementation of the smallrye OpenApiConfig interface, - * loadable from a Helidon {@link Config} object as well as individual items - * settable programmatically. - */ -public class OpenAPIConfigImpl implements OpenApiConfig { - - private final String modelReader; - private final String filter; - private final Map> operationServers; - private final Map> pathServers; - private Boolean scanDisable; - private final Pattern scanPackages; - private final Pattern scanClasses; - private final Pattern scanExcludePackages; - private final Pattern scanExcludeClasses; - private final Set servers; - private final Boolean scanDependenciesDisable = Boolean.TRUE; - private final Set scanDependenciesJars = Collections.emptySet(); - private final String customSchemaRegistryClass; - private final Boolean applicationPathDisable; - private final Map schemas; - - private OpenAPIConfigImpl(Builder builder) { - modelReader = builder.modelReader; - filter = builder.filter; - operationServers = builder.operationServers; - pathServers = builder.pathServers; - servers = new HashSet<>(builder.servers); - scanDisable = builder.scanDisable; - scanPackages = builder.scanPackages; - scanClasses = builder.scanClasses; - scanExcludePackages = builder.scanExcludePackages; - scanExcludeClasses = builder.scanExcludeClasses; - customSchemaRegistryClass = builder.customSchemaRegistryClass; - applicationPathDisable = builder.applicationPathDisable; - schemas = Collections.unmodifiableMap(builder.schemas); - } - - /** - * Creates a new builder for composing an OpenAPI config instance. - * - * @return the new {@code Builder} - */ - public static Builder builder() { - return new Builder(); - } - - @Override - public String modelReader() { - return modelReader; - } - - @Override - public String filter() { - return filter; - } - - @Override - public boolean scanDisable() { - return scanDisable; - } - - @Override - public Pattern scanPackages() { - return scanPackages; - } - - @Override - public Pattern scanClasses() { - return scanClasses; - } - - @Override - public Pattern scanExcludePackages() { - return scanExcludePackages; - } - - @Override - public Pattern scanExcludeClasses() { - return scanExcludeClasses; - } - - @Override - public Set servers() { - return servers; - } - - @Override - public Set pathServers(String path) { - return chooseEntry(pathServers, path); - } - - @Override - public Set operationServers(String operationID) { - return chooseEntry(operationServers, operationID); - } - - @Override - public boolean scanDependenciesDisable() { - return scanDependenciesDisable; - } - - @Override - public Set scanDependenciesJars() { - return scanDependenciesJars; - } - - @Override - public String customSchemaRegistryClass() { - return customSchemaRegistryClass; - } - - @Override - public boolean applicationPathDisable() { - return applicationPathDisable; - } - - @Override - public Map getSchemas() { - return schemas; - } - - private static Set chooseEntry(Map> map, T key) { - if (map.containsKey(key)) { - return map.get(key); - } - return Collections.emptySet(); - } - - /** - * Fluent builder for {@link io.helidon.openapi.internal.OpenAPIConfigImpl}. - *

    - * The caller can set values individually by invoking the method - * corresponding to each value, or by passing a {@link Config} object with - * keys as follows: - * - * - * - * - * - * - * - * - * - * - *
    Configuration for Setting OpenAPIConfig
    Key
    {@value MODEL_READER}
    {@value FILTER}
    {@value SERVERS}
    {@value SERVERS_PATH}
    {@value SERVERS_OPERATION}
    - */ - @Configured() - public static final class Builder implements io.helidon.common.Builder { - - /** - * Config key prefix for schema overrides for specified classes. - */ - public static final String SCHEMA = "schema"; - - private static final System.Logger LOGGER = System.getLogger(Builder.class.getName()); - - private static final Pattern MATCH_EVERYTHING = Pattern.compile(".*"); - - // Key names are inspired by the MP OpenAPI config key names - static final String MODEL_READER = "model.reader"; - static final String FILTER = "filter"; - static final String SERVERS = "servers"; - static final String SERVERS_PATH = "servers.path"; - static final String SERVERS_OPERATION = "servers.operation"; - - static final String CUSTOM_SCHEMA_REGISTRY_CLASS = "custom-schema-registry.class"; - static final String APPLICATION_PATH_DISABLE = "application-path.disable"; - - private String modelReader; - private String filter; - private final Map> operationServers = new HashMap<>(); - private final Map> pathServers = new HashMap<>(); - private final Set servers = new HashSet<>(); - private boolean scanDisable = true; - private Pattern scanPackages = MATCH_EVERYTHING; - private Pattern scanClasses = MATCH_EVERYTHING; - private Pattern scanExcludePackages = null; - private Pattern scanExcludeClasses = null; - - private String customSchemaRegistryClass; - private Boolean applicationPathDisable; - private Map schemas = new HashMap<>(); - - private Builder() { - } - - @Override - public OpenApiConfig build() { - return new OpenAPIConfigImpl(this); - } - - /** - * Sets the builder's attributes according to the corresponding entries - * (if present) in the specified openapi {@link Config} object. - * - * @param config {@code} openapi Config object to process - * @return updated builder - */ - public Builder config(Config config) { - stringFromConfig(config, MODEL_READER, this::modelReader); - stringFromConfig(config, FILTER, this::filter); - stringFromConfig(config, SERVERS, this::servers); - listFromConfig(config, SERVERS_PATH, this::pathServers); - listFromConfig(config, SERVERS_OPERATION, this::operationServers); - stringFromConfig(config, CUSTOM_SCHEMA_REGISTRY_CLASS, this::customSchemaRegistryClass); - booleanFromConfig(config, APPLICATION_PATH_DISABLE, this::applicationPathDisable); - mapFromConfig(config, SCHEMA, this::schemas); - return this; - } - - /** - * Sets the developer-provided OpenAPI model reader class name. - * - * @param modelReader model reader class name - * @return updated builder - */ - @ConfiguredOption(key = MODEL_READER) - public Builder modelReader(String modelReader) { - this.modelReader = modelReader; - return this; - } - - /** - * Sets the developer-provided OpenAPI filter class name. - * - * @param filter filter class name - * @return updated builder - */ - @ConfiguredOption - public Builder filter(String filter) { - this.filter = filter; - return this; - } - - /** - * Sets alternative servers to service the specified operation. Repeat for multiple operations. - * - * @param operationID operation ID - * @param operationServers comma-separated list of servers for the given - * operation - * @return updated builder - */ - @ConfiguredOption(key = SERVERS_OPERATION + ".*", - kind = ConfiguredOption.Kind.LIST, - description = """ - Sets alternative servers to service the indicated operation \ - (represented here by '*'). \ - Repeat for multiple operations.""", - type = String.class) - public Builder operationServers(String operationID, String operationServers) { - this.operationServers.clear(); - setEntry(this.operationServers, operationID, operationServers); - return this; - } - - /** - * Adds an alternative server to service the specified operation. - * - * @param operationID operation ID for the server being added - * @param operationServer the server being added - * @return updated builder - */ - public Builder addOperationServer(String operationID, String operationServer) { - addToEntry(operationServers, operationID, operationServer); - return this; - } - - /** - * Sets alternative servers to service all operations in the specified path. Repeat for multiple paths. - * - * @param path path for the servers being set - * @param pathServers comma-list of servers for the given path - * @return updated builder - */ - @ConfiguredOption(key = SERVERS_PATH + ".*", - kind = ConfiguredOption.Kind.LIST, - description = """ - Sets alternative servers to service all operations at the indicated path \ - (represented here by '*'). \ - Repeat for multiple paths.""", - type = String.class) - public Builder pathServers(String path, String pathServers) { - setEntry(this.pathServers, path, pathServers); - return this; - } - - /** - * Adds an alternative server for all operations in the specified path. - * - * @param path path for the server being added - * @param pathServer the server being added - * @return updated builder - */ - public Builder addPathServer(String path, String pathServer) { - addToEntry(pathServers, path, pathServer); - return this; - } - - /** - * Sets servers. - * - * @param servers comma-list of servers - * @return updated builder - */ - @ConfiguredOption(kind = ConfiguredOption.Kind.LIST) - public Builder servers(String servers) { - this.servers.clear(); - this.servers.addAll(commaListToSet(servers)); - return this; - } - - /** - * Adds server. - * - * @param server server to be added - * @return updated builder - */ - public Builder addServer(String server) { - servers.add(server); - return this; - } - - /** - * Sets schemas for one or more classes referenced in the OpenAPI model. - * - * @param schemas map of FQ class name to JSON string depicting the schema - * @return updated builder - */ - @ConfiguredOption(key = SCHEMA + ".*", - description = """ - Sets the schema for the indicated fully-qualified class name (represented here by '*'); \ - value is the schema in JSON format. \ - Repeat for multiple classes. \ - """, - type = String.class) - public Builder schemas(Map schemas) { - this.schemas = new HashMap<>(schemas); - return this; - } - - /** - * Adds a schema for a class. - * - * @param fullyQualifiedClassName name of the class the schema describes - * @param schema JSON text definition of the schema - * @return updated builder - */ - public Builder addSchema(String fullyQualifiedClassName, String schema) { - schemas.put(fullyQualifiedClassName, schema); - return this; - } - - /** - * Sets whether annotation scanning should be disabled. - * - * @param value new setting for annotation scanning disabled flag - * @return updated builder - */ - public Builder scanDisable(boolean value) { - scanDisable = value; - return this; - } - - /** - * Sets the custom schema registry class. - * - * @param className class to be assigned - * @return updated builder - */ - @ConfiguredOption - public Builder customSchemaRegistryClass(String className) { - customSchemaRegistryClass = className; - return this; - } - - /** - * Sets whether the app path search should be disabled. - * - * @param value true/false - * @return updated builder - */ - @ConfiguredOption("false") - public Builder applicationPathDisable(Boolean value) { - applicationPathDisable = value; - return this; - } - - private static void stringFromConfig(Config config, - String key, - Function assignment) { - config.get(key).asString().ifPresent(assignment::apply); - } - - private static void listFromConfig(Config config, - String keyPrefix, - BiFunction assignment) { - config.get(keyPrefix).asNodeList().ifPresent(cf -> cf.forEach(c -> { - String key = c.key().name(); - String value = c.asString().get(); - assignment.apply(key, value); - })); - } - - private static void mapFromConfig(Config config, - String keyPrefix, - Function, Builder> assignment) { - AtomicReference> schemas = new AtomicReference<>(new HashMap<>()); - config.get(keyPrefix) - .detach() - .asMap() - .ifPresent(configNodeMap -> schemas.set(configNodeMap)); - assignment.apply(schemas.get()); - } - - private static void booleanFromConfig(Config config, - String key, - Function assignment) { - config.get(key).asBoolean().ifPresent(assignment::apply); - } - - /** - * Given a Map>, adds an entry to the set for a given key - * value, creating the entry in the map if none already exists. - * - * @param key type of the Map - * @param value type of the Map - * @param map Map - * @param key key for which a value is to be added - * @param value value to add to the Map entry for the given key - */ - private static void addToEntry(Map> map, T key, U value) { - Set set; - if (map.containsKey(key)) { - set = map.get(key); - } else { - set = new HashSet<>(); - map.put(key, set); - } - set.add(value); - } - - /** - * Sets the entry for a key in Map by parsing a - * comma-separated list of values. - * - * @param type of the map's key - * @param map Map - * @param key key value for which to assign its associated values - * @param values comma-separated list of String values to convert to a - * list - */ - private static void setEntry( - Map> map, - T key, - String values) { - Set set = commaListToSet(values); - map.put(key, set); - } - - private static Set commaListToSet(String items) { - /* - * Do not special-case an empty comma-list to an empty set because a - * set created here might be added to later. - */ - final Set result = new HashSet<>(); - if (items != null) { - for (String item : items.split(",")) { - result.add(item.trim()); - } - } - return result; - } - } -} diff --git a/openapi/src/main/java/io/helidon/openapi/internal/package-info.java b/openapi/src/main/java/io/helidon/openapi/internal/package-info.java deleted file mode 100644 index ccf5e7bfcca..00000000000 --- a/openapi/src/main/java/io/helidon/openapi/internal/package-info.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Internal classes for Helidon OpenAPI support. - */ -package io.helidon.openapi.internal; diff --git a/openapi/src/main/java/module-info.java b/openapi/src/main/java/module-info.java index 830b6cd537b..2564aa66bb9 100644 --- a/openapi/src/main/java/module-info.java +++ b/openapi/src/main/java/module-info.java @@ -14,36 +14,27 @@ * limitations under the License. */ -import io.helidon.common.features.api.Feature; -import io.helidon.common.features.api.HelidonFlavor; +import io.helidon.openapi.OpenApiUiNoOpFactory; /** - * Helidon SE OpenAPI Support. + * Helidon common OpenAPI behavior. */ -@Feature(value = "OpenAPI", - description = "Open API support", - in = HelidonFlavor.SE, - path = "OpenAPI" -) module io.helidon.openapi { requires static io.helidon.common.features.api; requires io.helidon.common; requires io.helidon.common.config; requires io.helidon.common.media.type; + requires io.helidon.nima.servicecommon; - requires org.jboss.jandex; - requires smallrye.open.api.core; requires jakarta.json; - requires java.desktop; // for java.beans package - requires org.yaml.snakeyaml; - - requires transitive microprofile.openapi.api; requires static io.helidon.config.metadata; - requires java.logging; // temporary to adjust SnakeYAML logger level exports io.helidon.openapi; - exports io.helidon.openapi.internal to io.helidon.microprofile.openapi, io.helidon.reactive.openapi, io.helidon.nima.openapi; + + uses io.helidon.openapi.OpenApiUiFactory; + + provides io.helidon.openapi.OpenApiUiFactory with OpenApiUiNoOpFactory; } diff --git a/openapi/src/test/java/io/helidon/openapi/OpenAPIConfigTest.java b/openapi/src/test/java/io/helidon/openapi/OpenAPIConfigTest.java deleted file mode 100644 index 6ad9c0e6e7d..00000000000 --- a/openapi/src/test/java/io/helidon/openapi/OpenAPIConfigTest.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.openapi; - -import java.nio.file.Paths; -import java.util.List; -import java.util.Map; -import java.util.StringJoiner; - -import io.helidon.config.Config; -import io.helidon.config.ConfigSources; -import io.helidon.openapi.internal.OpenAPIConfigImpl; - -import io.smallrye.openapi.api.OpenApiConfig; -import org.junit.jupiter.api.Test; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.hasKey; -import static org.hamcrest.Matchers.is; - -class OpenAPIConfigTest { - - private final static String TEST_CONFIG_DIR = "src/test/resources"; - - private static final List SCHEMA_OVERRIDE_CONTENTS = List.of( - "\"name\": \"EpochMillis\"", - "\"type\": \"number\",", - "\"format\": \"int64\",", - "\"description\": \"Milliseconds since January 1, 1970, 00:00:00 GMT\""); - - private static final Map SCHEMA_OVERRIDE_VALUES = Map.of( - "name", "EpochMillis", - "type", "number", - "format", "int64", - "description", "Milliseconds since January 1, 1970, 00:00:00 GMT"); - - private static final String SCHEMA_OVERRIDE_JSON = prepareSchemaOverrideJSON(); - - private static final String SCHEMA_OVERRIDE_CONFIG_FQCN = "java.util.Date"; - - private static final Map SCHEMA_OVERRIDE_CONFIG = Map.of( - "openapi." - + OpenAPIConfigImpl.Builder.SCHEMA - + "." - + SCHEMA_OVERRIDE_CONFIG_FQCN, - SCHEMA_OVERRIDE_JSON); - - private static String prepareSchemaOverrideJSON() { - StringJoiner sj = new StringJoiner(",\n", "{\n", "\n}"); - SCHEMA_OVERRIDE_VALUES.forEach((key, value) -> sj.add("\"" + key + "\": \"" + value + "\"")); - return sj.toString(); - } - - @Test - void simpleConfigTest() { - Config config = Config.builder() - .disableEnvironmentVariablesSource() - .disableSystemPropertiesSource() - .sources(ConfigSources.file(Paths.get(TEST_CONFIG_DIR, "simple.properties").toString())) - .build(); - OpenApiConfig openAPIConfig = OpenAPIConfigImpl.builder() - .config(config.get("openapi")) - .build(); - - assertThat("reader mismatch", openAPIConfig.modelReader(), is("io.helidon.openapi.test.MyModelReader")); - assertThat("filter mismatch", openAPIConfig.filter(), is("io.helidon.openapi.test.MySimpleFilter")); - assertThat("servers mismatch", openAPIConfig.servers(), containsInAnyOrder("s1","s2")); - assertThat("path1 servers mismatch", openAPIConfig.pathServers("path1"), containsInAnyOrder("p1s1","p1s2")); - assertThat("path2 servers mismatch", openAPIConfig.pathServers("path2"), containsInAnyOrder("p2s1","p2s2")); - } - - @Test - void checkUnconfiguredValues() { - Config config = Config.builder() - .disableEnvironmentVariablesSource() - .disableSystemPropertiesSource() - .sources(ConfigSources.file(Paths.get(TEST_CONFIG_DIR, "simple.properties").toString())) - .build(); - OpenApiConfig openAPIConfig = OpenAPIConfigImpl.builder() - .config(config.get("openapi")) - .build(); - - assertThat("scan disable mismatch", openAPIConfig.scanDisable(), is(true)); - } - - @Test - void checkSchemaConfig() { - Config config = Config.just(ConfigSources.file(Paths.get(TEST_CONFIG_DIR, "simple.properties").toString()), - ConfigSources.create(SCHEMA_OVERRIDE_CONFIG)); - OpenApiConfig openAPIConfig = OpenAPIConfigImpl.builder() - .config(config.get("openapi")) - .build(); - - assertThat("Schema override", openAPIConfig.getSchemas(), hasKey(SCHEMA_OVERRIDE_CONFIG_FQCN)); - assertThat("Schema override value for " + SCHEMA_OVERRIDE_CONFIG_FQCN, - openAPIConfig.getSchemas().get(SCHEMA_OVERRIDE_CONFIG_FQCN), - is(SCHEMA_OVERRIDE_JSON)); - } -} diff --git a/openapi/src/test/java/io/helidon/openapi/ParserTest.java b/openapi/src/test/java/io/helidon/openapi/ParserTest.java deleted file mode 100644 index 625cf2b5aa7..00000000000 --- a/openapi/src/test/java/io/helidon/openapi/ParserTest.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.openapi; - -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.Map; - -import org.eclipse.microprofile.openapi.models.OpenAPI; -import org.eclipse.microprofile.openapi.models.Paths; -import org.eclipse.microprofile.openapi.models.parameters.Parameter; -import org.junit.jupiter.api.Test; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -class ParserTest { - - private static ParserHelper helper = ParserHelper.create(); - - @Test - public void testParserUsingYAML() throws IOException { - OpenAPI openAPI = parse(helper, "/petstore.yaml"); - assertThat(openAPI.getOpenapi(), is("3.0.0")); - assertThat(openAPI.getPaths().getPathItem("/pets").getGET().getParameters().get(0).getIn(), - is(Parameter.In.QUERY)); - } - - @Test - public void testExtensions() throws IOException { - OpenAPI openAPI = parse(helper, "/openapi-greeting.yml"); - Object xMyPersonalMap = openAPI.getExtensions().get("x-my-personal-map"); - assertThat(xMyPersonalMap, is(instanceOf(Map.class))); - Map map = (Map) xMyPersonalMap; - Object owner = map.get("owner"); - Object value1 = map.get("value-1"); - assertThat(value1, is(instanceOf(Double.class))); - Double d = (Double) value1; - assertThat(d, equalTo(2.3)); - - assertThat(owner, is(instanceOf(Map.class))); - map = (Map) owner; - assertThat(map.get("first"), equalTo("Me")); - assertThat(map.get("last"), equalTo("Myself")); - - Object xBoolean = openAPI.getExtensions().get("x-boolean"); - assertThat(xBoolean, is(instanceOf(Boolean.class))); - Boolean b = (Boolean) xBoolean; - assertThat(b, is(true)); - - Object xInt = openAPI.getExtensions().get("x-int"); - assertThat(xInt, is(instanceOf(Integer.class))); - Integer i = (Integer) xInt; - assertThat(i, is(117)); - - Object xStrings = openAPI.getExtensions().get("x-string-array"); - assertThat(xStrings, is(instanceOf(List.class))); - List list = (List) xStrings; - Object first = list.get(0); - assertThat(first, is(instanceOf(String.class))); - String f = (String) first; - assertThat(f, is(equalTo("one"))); - } - - - @Test - void testYamlRef() throws IOException { - OpenAPI openAPI = parse(helper, "/petstore.yaml"); - Paths paths = openAPI.getPaths(); - String ref = paths.getPathItem("/pets") - .getGET() - .getResponses() - .getAPIResponse("200") - .getContent() - .getMediaType("application/json") - .getSchema() - .getRef(); - - assertThat("ref value", ref, is(equalTo("#/components/schemas/Pets"))); - } - - @Test - void testJsonRef() throws IOException { - OpenAPI openAPI = parse(helper, "/petstore.json"); - Paths paths = openAPI.getPaths(); - String ref = paths.getPathItem("/user") - .getPOST() - .getRequestBody() - .getContent() - .getMediaType("application/json") - .getSchema() - .getRef(); - - assertThat("ref value", ref, is(equalTo("#/components/schemas/User"))); - } - - @Test - public void testParserUsingJSON() throws IOException { - OpenAPI openAPI = parse(helper, "/petstore.json"); - assertThat(openAPI.getOpenapi(), is("3.0.0")); -// TODO - uncomment the following once full $ref support is in place -// assertThat(openAPI.getPaths().getPathItem("/pet").getPUT().getRequestBody().getDescription(), -// containsString("needs to be added to the store")); - } - - static OpenAPI parse(ParserHelper helper, String path) throws IOException { - try (InputStream is = ParserTest.class.getResourceAsStream(path)) { - return OpenAPIParser.parse(helper.types(), is); - } - } -} \ No newline at end of file diff --git a/openapi/src/test/java/io/helidon/openapi/SerializerTest.java b/openapi/src/test/java/io/helidon/openapi/SerializerTest.java deleted file mode 100644 index 75a9e204cf3..00000000000 --- a/openapi/src/test/java/io/helidon/openapi/SerializerTest.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.openapi; - -import java.io.IOException; -import java.io.LineNumberReader; -import java.io.Reader; -import java.io.StringReader; -import java.io.StringWriter; -import java.io.Writer; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import io.smallrye.openapi.runtime.io.Format; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.json.JsonReader; -import jakarta.json.JsonReaderFactory; -import jakarta.json.JsonStructure; -import jakarta.json.JsonValue; -import org.eclipse.microprofile.openapi.models.OpenAPI; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.hamcrest.MatcherAssert.assertThat; - -class SerializerTest { - private static final JsonReaderFactory JSON_READER_FACTORY - = Json.createReaderFactory(Collections.emptyMap()); - private static ParserHelper helper; - - private static Map, ExpandedTypeDescription> implsToTypes; - - @BeforeAll - static void prepareHelper() { - helper = ParserHelper.create(); - implsToTypes = ExpandedTypeDescription.buildImplsToTypes(helper); - } - - @Test - public void testJSONSerialization() throws IOException { - OpenAPI openAPI = ParserTest.parse(helper, "/openapi-greeting.yml"); - Writer writer = new StringWriter(); - Serializer.serialize(helper.types(), implsToTypes, openAPI, Format.JSON, writer); - JsonStructure json = jsonFromReader(new StringReader(writer.toString())); - - assertThat(json.getValue("/x-my-personal-map/owner/last").toString(), is("\"Myself\"")); - JsonValue otherItem = json.getValue("/x-other-item"); - assertThat(otherItem.getValueType(), is(JsonValue.ValueType.NUMBER)); - assertThat(Double.valueOf(otherItem.toString()), is(10.0)); - - JsonValue seq = json.getValue("/info/x-my-personal-seq"); - assertThat(seq.getValueType(), is(JsonValue.ValueType.ARRAY)); - JsonArray seqArray = seq.asJsonArray(); - JsonValue first = seqArray.get(0); - assertThat(first.getValueType(), is(JsonValue.ValueType.OBJECT)); - JsonObject firstObj = first.asJsonObject(); - checkJsonPathStringValue(firstObj, "/who", "Prof. Plum"); - checkJsonPathStringValue(firstObj, "/why", "felt like it"); - - JsonValue second = seqArray.get(1); - assertThat(second.getValueType(), is(JsonValue.ValueType.OBJECT)); - JsonObject secondObj = second.asJsonObject(); - checkJsonPathStringValue(secondObj, "/when", "yesterday"); - checkJsonPathStringValue(secondObj, "/how", "with the lead pipe"); - - JsonValue xInt = json.getValue("/x-int"); - assertThat(xInt.getValueType(), is(JsonValue.ValueType.NUMBER)); - assertThat(Integer.valueOf(xInt.toString()), is(117)); - - JsonValue xBoolean = json.getValue("/x-boolean"); - assertThat(xBoolean.getValueType(), is(JsonValue.ValueType.TRUE)); - - JsonValue xStrings = json.getValue("/x-string-array"); - assertThat(xStrings.getValueType(), is(JsonValue.ValueType.ARRAY)); - JsonArray xStringArray = xStrings.asJsonArray(); - assertThat(xStringArray.size(), is(2)); - checkJsonStringValue(xStringArray.get(0), "one"); - checkJsonStringValue(xStringArray.get(1), "two"); - - JsonValue xObjects = json.getValue("/x-object-array"); - assertThat(xObjects.getValueType(), is(JsonValue.ValueType.ARRAY)); - JsonArray xObjectArray = xObjects.asJsonArray(); - assertThat(xObjectArray.size(), is(2)); - first = xObjectArray.get(0); - assertThat(first.getValueType(), is(JsonValue.ValueType.OBJECT)); - firstObj = first.asJsonObject(); - checkJsonPathStringValue(firstObj, "/name", "item-1"); - checkJsonPathIntValue(firstObj, "/value", 16); - second = xObjectArray.get(1); - assertThat(second.getValueType(), is(JsonValue.ValueType.OBJECT)); - secondObj = second.asJsonObject(); - checkJsonPathStringValue(secondObj, "/name", "item-2"); - checkJsonPathIntValue(secondObj, "/value", 18); - - } - - private void checkJsonPathStringValue(JsonObject jsonObject, String pointer, String expected) { - checkJsonStringValue(jsonObject.getValue(pointer), expected); - } - - private void checkJsonStringValue(JsonValue jsonValue, String expected) { - assertThat(jsonValue.getValueType(), is(JsonValue.ValueType.STRING)); - assertThat(jsonValue.toString(), is("\"" + expected + "\"")); - } - - private void checkJsonPathIntValue(JsonObject jsonObject, String pointer, int expected) { - checkJsonIntValue(jsonObject.getValue(pointer), expected); - } - - private void checkJsonIntValue(JsonValue val, int expected) { - assertThat(val.getValueType(), is(JsonValue.ValueType.NUMBER)); - assertThat(Integer.valueOf(val.toString()), is(expected)); - } - - @Test - public void testYAMLSerialization() throws IOException { - OpenAPI openAPI = ParserTest.parse(helper, "/openapi-greeting.yml"); - Writer writer = new StringWriter(); - Serializer.serialize(helper.types(), implsToTypes, openAPI, Format.YAML, writer); - try (Reader reader = new StringReader(writer.toString())) { - openAPI = OpenAPIParser.parse(helper.types(), reader); - } - Object candidateMap = openAPI.getExtensions() - .get("x-my-personal-map"); - assertThat(candidateMap, is(instanceOf(Map.class))); - - Map map = (Map) candidateMap; - Object candidateOwnerMap = map.get("owner"); - assertThat(candidateOwnerMap, is(instanceOf(Map.class))); - - Map ownerMap = (Map) candidateOwnerMap; - assertThat(ownerMap.get("last"), is("Myself")); - - List required = openAPI.getPaths().getPathItem("/greet/greeting") - .getPUT() - .getRequestBody() - .getContent() - .getMediaType("application/json") - .getSchema() - .getRequired(); - assertThat(required, hasItem("greeting")); - } - - @Test - void testRefSerializationAsOpenAPI() throws IOException { - OpenAPI openAPI = ParserTest.parse(helper, "/petstore.yaml"); - Writer writer = new StringWriter(); - Serializer.serialize(helper.types(), implsToTypes, openAPI, Format.YAML, writer); - - try (Reader reader = new StringReader(writer.toString())) { - openAPI = OpenAPIParser.parse(helper.types(), reader); - } - - String ref = openAPI.getPaths() - .getPathItem("/pets") - .getGET() - .getResponses() - .getDefaultValue() - .getContent() - .getMediaType("application/json") - .getSchema() - .getRef(); - assertThat("/pets.GET.responses.default.content.application/json.schema.ref", ref, - is(equalTo("#/components/schemas/Error"))); - } - - @Test - void testRefSerializationAsText() throws IOException { - // This test basically replicates the other ref test but without re-parsing, just in case there might be - // compensating bugs in the parsing and the serialization. - Pattern refPattern = Pattern.compile("\\s\\$ref\\: '([^']+)"); - - OpenAPI openAPI = ParserTest.parse(helper, "/petstore.yaml"); - Writer writer = new StringWriter(); - Serializer.serialize(helper.types(), implsToTypes, openAPI, Format.YAML, writer); - - try (LineNumberReader reader = new LineNumberReader(new StringReader(writer.toString()))) { - String line; - while ((line = reader.readLine()) != null) { - Matcher refMatcher = refPattern.matcher(line); - if (refMatcher.matches()) { - assertThat("Apparent reference to component", refMatcher.group(1), startsWith("#/components")); - } - } - } - } - - private static JsonStructure jsonFromReader(Reader reader) { - JsonReader jsonReader = JSON_READER_FACTORY.createReader(reader); - JsonStructure result = jsonReader.read(); - jsonReader.close(); - return result; - } -} \ No newline at end of file diff --git a/reactive/openapi/src/test/java/io/helidon/reactive/openapi/ServerTest.java b/openapi/src/test/java/io/helidon/openapi/ServerTest.java similarity index 87% rename from reactive/openapi/src/test/java/io/helidon/reactive/openapi/ServerTest.java rename to openapi/src/test/java/io/helidon/openapi/ServerTest.java index 20e3f2254eb..b7147060321 100644 --- a/reactive/openapi/src/test/java/io/helidon/reactive/openapi/ServerTest.java +++ b/openapi/src/test/java/io/helidon/openapi/ServerTest.java @@ -13,22 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.reactive.openapi; +package io.helidon.openapi; import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.Map; import java.util.function.Consumer; +import java.util.stream.Stream; import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; import io.helidon.config.Config; import io.helidon.config.ConfigSources; -import io.helidon.reactive.webserver.WebServer; +import io.helidon.nima.webserver.WebServer; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -46,20 +49,20 @@ public class ServerTest { private static final String TIME_PATH = "/openapi-time"; private static final Config OPENAPI_CONFIG_DISABLED_CORS = Config.create( - ConfigSources.classpath("serverNoCORS.properties").build()).get(OpenAPISupport.Builder.CONFIG_KEY); + ConfigSources.classpath("serverNoCORS.properties").build()).get(OpenApiFeature.Builder.CONFIG_KEY); private static final Config OPENAPI_CONFIG_RESTRICTED_CORS = Config.create( - ConfigSources.classpath("serverCORSRestricted.yaml").build()).get(OpenAPISupport.Builder.CONFIG_KEY); + ConfigSources.classpath("serverCORSRestricted.yaml").build()).get(OpenApiFeature.Builder.CONFIG_KEY); - static final OpenAPISupport.Builder GREETING_OPENAPI_SUPPORT_BUILDER - = OpenAPISupport.builder() - .staticFile("src/test/resources/openapi-greeting.yml") + static final OpenApiFeature.Builder GREETING_OPENAPI_SUPPORT_BUILDER + = StaticFileOnlyOpenApiFeatureImpl.builder() + .staticFile("openapi-greeting.yml") .webContext(GREETING_PATH) .config(OPENAPI_CONFIG_DISABLED_CORS); - static final OpenAPISupport.Builder TIME_OPENAPI_SUPPORT_BUILDER - = OpenAPISupport.builder() - .staticFile("src/test/resources/openapi-time-server.yml") + static final OpenApiFeature.Builder TIME_OPENAPI_SUPPORT_BUILDER + = StaticFileOnlyOpenApiFeatureImpl.builder() + .staticFile("openapi-time-server.yml") .webContext(TIME_PATH) .config(OPENAPI_CONFIG_RESTRICTED_CORS); @@ -146,12 +149,17 @@ public void testGreetingAsConfig() throws Exception { * @throws Exception in case of errors sending the request or receiving the * response */ - @Test - public void checkExplicitResponseMediaTypeViaHeaders() throws Exception { - connectAndConsumePayload(MediaTypes.APPLICATION_OPENAPI_YAML); - connectAndConsumePayload(MediaTypes.APPLICATION_YAML); - connectAndConsumePayload(MediaTypes.APPLICATION_OPENAPI_JSON); - connectAndConsumePayload(MediaTypes.APPLICATION_JSON); + @ParameterizedTest + @MethodSource() + public void checkExplicitResponseMediaTypeViaHeaders(MediaType testMediaType) throws Exception { + connectAndConsumePayload(testMediaType); + } + + static Stream checkExplicitResponseMediaTypeViaHeaders() { + return Stream.of(MediaTypes.APPLICATION_OPENAPI_YAML, + MediaTypes.APPLICATION_YAML, + MediaTypes.APPLICATION_OPENAPI_JSON, + MediaTypes.APPLICATION_JSON); } @Test @@ -167,17 +175,6 @@ void checkExplicitResponseMediaTypeViaQueryParameter() throws Exception { MediaTypes.APPLICATION_OPENAPI_YAML); } - /** - * Makes sure that the response is correct if the request specified no - * explicit Accept. - * - * @throws Exception error sending the request or receiving the response - */ - @Test - public void checkDefaultResponseMediaType() throws Exception { - connectAndConsumePayload(null); - } - @Test public void testTimeAsConfig() throws Exception { commonTestTimeAsConfig(null); diff --git a/openapi/src/test/java/io/helidon/openapi/StaticFileOnlyOpenApiFeatureImpl.java b/openapi/src/test/java/io/helidon/openapi/StaticFileOnlyOpenApiFeatureImpl.java new file mode 100644 index 00000000000..3777f67e1ed --- /dev/null +++ b/openapi/src/test/java/io/helidon/openapi/StaticFileOnlyOpenApiFeatureImpl.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.openapi; + +import java.io.StringReader; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.json.stream.JsonParser; +import org.yaml.snakeyaml.Yaml; + +public class StaticFileOnlyOpenApiFeatureImpl extends OpenApiFeature { + + private static final System.Logger LOGGER = System.getLogger(StaticFileOnlyOpenApiFeatureImpl.class.getName()); + + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Map.of()); + + static Builder builder() { + return new Builder(); + } + + private final Map staticContent = new HashMap<>(); + + /** + * Constructor for the feature. + * + * @param builder builder to use for initializing the feature + */ + protected StaticFileOnlyOpenApiFeatureImpl(Builder builder) { + super(LOGGER, builder); + // We should have a static file containing either YAML or JSON. Create a static file of the other type so we have it. + if (staticContent().isEmpty()) { + throw new IllegalArgumentException("Static-only OpenAPI feature does not have static content!"); + } + OpenApiStaticFile staticFile = staticContent().get(); + staticContent.put(staticFile.openApiMediaType(), staticFile.content()); + if (staticFile.openApiMediaType().equals(OpenAPIMediaType.YAML)) { + Yaml yaml = new Yaml(); + Map map = yaml.load(staticFile.content()); + // Simplistic - change Date to String because Json does not know how to handle Date + map = clean(map); + JsonObject json = JSON.createObjectBuilder(map).build(); + staticContent.put(OpenAPIMediaType.JSON, json.toString()); + } else { + Yaml yaml = new Yaml(); + yaml.load(staticFile.content()); + staticContent.put(OpenAPIMediaType.YAML, yaml.toString()); + } + } + + @Override + protected String openApiContent(OpenAPIMediaType openApiMediaType) { + // A real implemention would have only one static content instance. This test implementation + // has two, one each for JSON and YAML. + return staticContent.get(openApiMediaType); + } + + private static Map clean(Map map) { + Map result = new HashMap<>(); + map.forEach((k, v) -> { + if (v instanceof Map vMap) { + result.put(k, clean(vMap)); + } else if (v instanceof Date date) { + result.put(k, date.toString()); + } else { + result.put(k, v); + } + }); + return result; + } + + static class Builder extends OpenApiFeature.Builder { + + + private static final System.Logger LOGGER = System.getLogger(Builder.class.getName()); + + @Override + public StaticFileOnlyOpenApiFeatureImpl build() { + return new StaticFileOnlyOpenApiFeatureImpl(this); + } + + @Override + protected System.Logger logger() { + return LOGGER; + } + } +} diff --git a/openapi/src/test/java/io/helidon/openapi/TestAdditionalProperties.java b/openapi/src/test/java/io/helidon/openapi/TestAdditionalProperties.java deleted file mode 100644 index 1a75be57660..00000000000 --- a/openapi/src/test/java/io/helidon/openapi/TestAdditionalProperties.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.openapi; - - -import java.io.IOException; -import java.io.StringWriter; -import java.util.Map; - -import io.smallrye.openapi.runtime.io.Format; -import org.eclipse.microprofile.openapi.models.OpenAPI; -import org.eclipse.microprofile.openapi.models.media.Schema; -import org.junit.jupiter.api.Test; -import org.yaml.snakeyaml.Yaml; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.hasKey; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; - -class TestAdditionalProperties { - - private static ParserHelper helper = ParserHelper.create(); - - - @Test - void checkParsingBooleanAdditionalProperties() throws IOException { - OpenAPI openAPI = ParserTest.parse(helper, "/withBooleanAddlProps.yml"); - Schema itemSchema = openAPI.getComponents().getSchemas().get("item"); - - Schema additionalPropertiesSchema = itemSchema.getAdditionalPropertiesSchema(); - Boolean additionalPropertiesBoolean = itemSchema.getAdditionalPropertiesBoolean(); - - assertThat("Additional properties as schema", additionalPropertiesSchema, is(nullValue())); - assertThat("Additional properties as boolean", additionalPropertiesBoolean, is(notNullValue())); - assertThat("Additional properties value", additionalPropertiesBoolean.booleanValue(), is(false)); - } - - @Test - void checkParsingSchemaAdditionalProperties() throws IOException { - OpenAPI openAPI = ParserTest.parse(helper, "/withSchemaAddlProps.yml"); - Schema itemSchema = openAPI.getComponents().getSchemas().get("item"); - - Schema additionalPropertiesSchema = itemSchema.getAdditionalPropertiesSchema(); - Boolean additionalPropertiesBoolean = itemSchema.getAdditionalPropertiesBoolean(); - - assertThat("Additional properties as boolean", additionalPropertiesBoolean, is(nullValue())); - assertThat("Additional properties as schema", additionalPropertiesSchema, is(notNullValue())); - - Map additionalProperties = additionalPropertiesSchema.getProperties(); - assertThat("Additional property 'code'", additionalProperties, hasKey("code")); - assertThat("Additional property 'text'", additionalProperties, hasKey("text")); - } - - @Test - void checkWritingSchemaAdditionalProperties() throws IOException { - OpenAPI openAPI = ParserTest.parse(helper, "/withSchemaAddlProps.yml"); - String document = formatModel(openAPI); - - /* - * Expected output (although the - additionalProperties: - type: object - properties: - code: - type: integer - text: - type: string - */ - Yaml yaml = new Yaml(); - Map model = yaml.load(document); - Map item = asMap(model, "components", "schemas", "item"); - - Object additionalProperties = item.get("additionalProperties"); - - assertThat("Additional properties node type", additionalProperties, is(instanceOf(Map.class))); - - } - - private static Map asMap(Map map, String... keys) { - Map m = map; - for (String key : keys) { - m = (Map) m.get(key); - } - return m; - } - - @Test - void checkWritingBooleanAdditionalProperties() throws IOException { - OpenAPI openAPI = ParserTest.parse(helper, "/withBooleanAddlProps.yml"); - String document = formatModel(openAPI); - - /* - * Expected output: additionalProperties: false - */ - - assertThat("Formatted OpenAPI document matches expected pattern", - document, containsString("additionalProperties: false")); - } - - private String formatModel(OpenAPI model) { - StringWriter sw = new StringWriter(); - Map, ExpandedTypeDescription> implsToTypes = ExpandedTypeDescription.buildImplsToTypes(helper); - Serializer.serialize(helper.types(), implsToTypes, model, Format.YAML, sw); - return sw.toString(); - } -} diff --git a/openapi/src/test/java/io/helidon/openapi/TestOpenAPIMediaTypesDescribedCorrectly.java b/openapi/src/test/java/io/helidon/openapi/TestOpenAPIMediaTypesDescribedCorrectly.java index ab223d91c6c..8f28371acdb 100644 --- a/openapi/src/test/java/io/helidon/openapi/TestOpenAPIMediaTypesDescribedCorrectly.java +++ b/openapi/src/test/java/io/helidon/openapi/TestOpenAPIMediaTypesDescribedCorrectly.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import java.util.Set; import java.util.stream.Collectors; +import io.helidon.openapi.OpenApiFeature.OpenAPIMediaType; + import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/openapi/src/test/java/io/helidon/openapi/TestStaticContent.java b/openapi/src/test/java/io/helidon/openapi/TestStaticContent.java new file mode 100644 index 00000000000..b774a1545e8 --- /dev/null +++ b/openapi/src/test/java/io/helidon/openapi/TestStaticContent.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.openapi; + +import java.util.stream.Stream; + +import io.helidon.common.testing.junit5.OptionalMatcher; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.nullValue; + +class TestStaticContent { + + record StaticContentTestValue(String path, + OpenApiFeature.OpenAPIMediaType openAPIMediaType, + String expectedContent) {} + + + @ParameterizedTest + @MethodSource("staticContentTestValues") + void testStaticContent(StaticContentTestValue testValue) { + OpenApiFeature feature = StaticFileOnlyOpenApiFeatureImpl.builder() + .staticFile(testValue.path) + .build(); + + assertThat("YAML static content", + feature.staticContent(), + OptionalMatcher.optionalPresent()); + + assertThat("YAML static content value", + feature.staticContent().get().content(), + containsString(testValue.expectedContent)); + + assertThat("Content", feature.openApiContent(testValue.openAPIMediaType), + containsString(testValue.expectedContent)); + } + + static Stream staticContentTestValues() { + return Stream.of(new StaticContentTestValue("openapi-greeting.yml", + OpenApiFeature.OpenAPIMediaType.YAML, + "Sets the greeting prefix"), + new StaticContentTestValue("petstore.json", + OpenApiFeature.OpenAPIMediaType.JSON, + "This is a sample server Petstore server."), + new StaticContentTestValue("petstore.yaml", + OpenApiFeature.OpenAPIMediaType.YAML, + "A link to the next page of responses")); + } +} diff --git a/reactive/openapi/src/test/java/io/helidon/reactive/openapi/TestUtil.java b/openapi/src/test/java/io/helidon/openapi/TestUtil.java similarity index 86% rename from reactive/openapi/src/test/java/io/helidon/reactive/openapi/TestUtil.java rename to openapi/src/test/java/io/helidon/openapi/TestUtil.java index fdc5537f3af..9b82db94580 100644 --- a/reactive/openapi/src/test/java/io/helidon/reactive/openapi/TestUtil.java +++ b/openapi/src/test/java/io/helidon/openapi/TestUtil.java @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.reactive.openapi; +package io.helidon.openapi; import java.io.IOException; import java.io.InputStreamReader; -import java.lang.System.Logger.Level; +import java.io.Reader; import java.net.HttpURLConnection; import java.net.URL; import java.nio.CharBuffer; @@ -25,8 +25,9 @@ import java.util.Collections; import java.util.Map; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; import io.helidon.common.http.Http; import io.helidon.common.http.HttpMediaType; @@ -34,8 +35,8 @@ import io.helidon.common.media.type.MediaTypes; import io.helidon.config.Config; import io.helidon.config.ConfigSources; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.WebServer; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http.HttpRouting; import jakarta.json.Json; import jakarta.json.JsonReader; @@ -54,7 +55,7 @@ public class TestUtil { private static final JsonReaderFactory JSON_READER_FACTORY = Json.createReaderFactory(Collections.emptyMap()); - private static final System.Logger LOGGER = System.getLogger(TestUtil.class.getName()); + private static final Logger LOGGER = Logger.getLogger(TestUtil.class.getName()); /** * Starts the web server at an available port and sets up OpenAPI using the @@ -64,7 +65,7 @@ public class TestUtil { * server. * @return the {@code WebServer} set up with OpenAPI support */ - public static WebServer startServer(OpenAPISupport.Builder builder) { + public static WebServer startServer(OpenApiFeature.Builder builder) { try { return startServer(0, builder); } catch (InterruptedException | ExecutionException | TimeoutException ex) { @@ -77,12 +78,12 @@ public static WebServer startServer(OpenAPISupport.Builder builder) { * * @param cnx the HttpURLConnection from which to get the response payload * @return String representation of the OpenAPI document as a String - * @throws IOException in case of errors reading the HTTP response payload + * @throws java.io.IOException in case of errors reading the HTTP response payload */ public static String stringYAMLFromResponse(HttpURLConnection cnx) throws IOException { HttpMediaType returnedMediaType = mediaTypeFromResponse(cnx); assertThat("Unexpected returned media type", - HttpMediaType.create(MediaTypes.APPLICATION_OPENAPI_YAML).test(returnedMediaType), is(true)); + returnedMediaType.test(MediaTypes.APPLICATION_OPENAPI_YAML), is(true)); return stringFromResponse(cnx, returnedMediaType); } @@ -140,8 +141,11 @@ static MediaType connectAndConsumePayload( */ public static HttpMediaType mediaTypeFromResponse(HttpURLConnection cnx) { HttpMediaType returnedMediaType = HttpMediaType.create(cnx.getContentType()); - if (returnedMediaType.charset().isEmpty()) { - returnedMediaType = returnedMediaType.withCharset(Charset.defaultCharset().name()); + if (!returnedMediaType.charset().isPresent()) { + returnedMediaType = HttpMediaType.builder() + .mediaType(returnedMediaType) + .charset(Charset.defaultCharset().name()) + .build(); } return returnedMediaType; } @@ -153,13 +157,13 @@ public static HttpMediaType mediaTypeFromResponse(HttpURLConnection cnx) { * @param cnx the HttpURLConnection which already has the response to * process * @return Config representing the OpenAPI document content - * @throws IOException in case of errors reading the returned payload as + * @throws java.io.IOException in case of errors reading the returned payload as * config */ public static Config configFromResponse(HttpURLConnection cnx) throws IOException { HttpMediaType mt = mediaTypeFromResponse(cnx); MediaType configMT = HttpMediaType.create(MediaTypes.APPLICATION_OPENAPI_YAML).test(mt) - ? MediaTypes.APPLICATION_X_YAML + ? MediaTypes.APPLICATION_YAML : MediaTypes.APPLICATION_JSON; String yaml = stringYAMLFromResponse(cnx); return Config.create(ConfigSources.create(yaml, configMT)); @@ -172,7 +176,7 @@ public static Config configFromResponse(HttpURLConnection cnx) throws IOExceptio * @param cnx the {@code HttpURLConnection} containing the response * @return the YAML {@code Map} (created by snakeyaml) from * the HTTP response payload - * @throws IOException in case of errors reading the response + * @throws java.io.IOException in case of errors reading the response */ @SuppressWarnings(value = "unchecked") public static Map yamlFromResponse(HttpURLConnection cnx) throws IOException { @@ -222,7 +226,7 @@ public static String fromConfig(Config c, String key) { * * @param cnx the {@code HttpURLConnection} containing the response * @return {@code JsonStructure} representing the response payload - * @throws IOException in case of errors reading the response + * @throws java.io.IOException in case of errors reading the response */ public static JsonStructure jsonFromResponse(HttpURLConnection cnx) throws IOException { JsonReader reader = JSON_READER_FACTORY.createReader(cnx.getInputStream()); @@ -259,15 +263,15 @@ public static HttpMediaType validateResponseMediaType( MediaType expectedMediaType) throws Exception { assertThat("Unexpected response code", cnx.getResponseCode(), is(Http.Status.OK_200.code())); - MediaType expectedMT = expectedMediaType != null - ? expectedMediaType - : OpenAPISupport.DEFAULT_RESPONSE_MEDIA_TYPE; + HttpMediaType expectedMT = expectedMediaType != null + ? HttpMediaType.create(expectedMediaType) + : HttpMediaType.create(OpenApiFeature.DEFAULT_RESPONSE_MEDIA_TYPE); HttpMediaType actualMT = mediaTypeFromResponse(cnx); assertThat("Expected response media type " + expectedMT.toString() + " but received " + actualMT.toString(), - HttpMediaType.create(expectedMT).test(actualMT), is(true)); + expectedMT.test(actualMT), is(true)); return actualMT; } @@ -313,13 +317,13 @@ static HttpURLConnection getURLConnection( * * @param server the {@code WebServer} to stop * @throws InterruptedException if the stop operation was interrupted - * @throws ExecutionException if the stop operation failed as it ran - * @throws TimeoutException if the stop operation timed out + * @throws java.util.concurrent.ExecutionException if the stop operation failed as it ran + * @throws java.util.concurrent.TimeoutException if the stop operation timed out */ public static void stopServer(WebServer server) throws InterruptedException, ExecutionException, TimeoutException { if (server != null) { - server.shutdown().toCompletableFuture().get(10, TimeUnit.SECONDS); + server.stop(); } } @@ -328,25 +332,26 @@ public static void stopServer(WebServer server) throws * * @param port the port on which to start the server; if less than 1, the * port is dynamically selected - * @param openAPIBuilders OpenAPISupport.Builder instances to use in + * @param openApiBuilders OpenAPISupport.Builder instances to use in * starting the server * @return {@code WebServer} that has been started - * @throws java.lang.InterruptedException if the start was interrupted + * @throws InterruptedException if the start was interrupted * @throws java.util.concurrent.ExecutionException if the start failed * @throws java.util.concurrent.TimeoutException if the start timed out */ public static WebServer startServer( int port, - OpenAPISupport.Builder... openAPIBuilders) throws + OpenApiFeature.Builder... openApiBuilders) throws InterruptedException, ExecutionException, TimeoutException { - WebServer result = WebServer.builder(Routing.builder() - .register(openAPIBuilders) - .build()) + HttpRouting.Builder routingBuilder = HttpRouting.builder(); + for (OpenApiFeature.Builder openApiBuilder : openApiBuilders) { + routingBuilder.addFeature(openApiBuilder); + } + WebServer result = WebServer.builder() + .addRouting(routingBuilder) .port(port) .build() - .start() - .toCompletableFuture() - .get(10, TimeUnit.SECONDS); + .start(); LOGGER.log(Level.INFO, "Started server at: https://localhost:{0}", result.port()); return result; } @@ -360,7 +365,7 @@ public static WebServer startServer( * payload * @return {@code String} of the payload interpreted according to the * specified {@code MediaType} - * @throws IOException in case of errors reading the response payload + * @throws java.io.IOException in case of errors reading the response payload */ public static String stringFromResponse(HttpURLConnection cnx, HttpMediaType mediaType) throws IOException { try (final InputStreamReader isr = new InputStreamReader( diff --git a/openapi/src/test/resources/configWithSchemasWithRef.yaml b/openapi/src/test/resources/configWithSchemasWithRef.yaml new file mode 100644 index 00000000000..f7f515319d0 --- /dev/null +++ b/openapi/src/test/resources/configWithSchemasWithRef.yaml @@ -0,0 +1,56 @@ +# +# Copyright (c) 2023 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +openapi: + schema: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + Order: + title: Pet Order + description: An order for a pets from the pet store + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order \ No newline at end of file diff --git a/openapi/src/test/resources/petstore.yaml b/openapi/src/test/resources/petstore.yaml index 9809b8dd213..7325211ef50 100644 --- a/openapi/src/test/resources/petstore.yaml +++ b/openapi/src/test/resources/petstore.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2020, 2021 Oracle and/or its affiliates. +# Copyright (c) 2020, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ paths: type: integer format: int32 responses: - 200: + '200': description: An paged array of pets headers: x-next: @@ -60,7 +60,7 @@ paths: tags: - pets responses: - 201: + '201': description: Null response default: description: unexpected error @@ -82,7 +82,7 @@ paths: schema: type: string responses: - 200: + '200': description: Expected response to a valid request content: application/json: diff --git a/pico/README.md b/pico/README.md index c5652ef9775..44c04bd536f 100644 --- a/pico/README.md +++ b/pico/README.md @@ -46,6 +46,7 @@ Request and Session scopes are simply not made available in Pico. We believe tha * Provided - jakarta.inject.Provider or javax.inject.Provider - If the scope of a service is not Singleton then it is considered to be a Provided scope - and the cardinality will be ascribed to the implementation of the Provider to determine its cardinality. The provider can optionally use the injection point context to determine the appropriate instance and/or cardinality it provides. * Contract - These are how a service can alias itself for injection. Contracts are typically the interface or abstract base class definitions of a service implementation. Injection points must be based upon either using a contract or service that pico is aware of, usually through annotation processing at compile time. * Qualifier - jakarta.inject.qualifier or javax.inject.qualifier - These are meta annotations that can be ascribed to other annotations. One built-in qualifier type is @Named in the same package. +* RunLevel - A way for you to describe when a service shut start up during process lifecycle. The lower the RunLevel the sooner it should start (usually based at 0). * Dependency - An injection point represents what is considered to be a dependency, perhaps qualified or Optional, on another service or contract. This is just another what to describe an injection point. * Activator (aka ServiceProvider) - This is what is code generated by Pico to lazily activate your service instance(s) in the Pico services registry, and it handles resolving all dependencies it has, along with injecting the fields, methods, etc. that are required to be satisfied as part of that activation process. * Services (aka services registry) - This is the collection of all services that are known to the JVM/runtime in Pico. diff --git a/pico/api/src/main/java/io/helidon/pico/api/CommonQualifiers.java b/pico/api/src/main/java/io/helidon/pico/api/CommonQualifiers.java index 5f22a936730..f61e939ee24 100644 --- a/pico/api/src/main/java/io/helidon/pico/api/CommonQualifiers.java +++ b/pico/api/src/main/java/io/helidon/pico/api/CommonQualifiers.java @@ -31,10 +31,15 @@ public final class CommonQualifiers { */ public static final TypeName NAMED = TypeNameDefault.create(Named.class); + /** + * Represents a wildcard (i.e., matches anything). + */ + public static final String WILDCARD = "*"; + /** * Represents a wildcard {@link #NAMED} qualifier. */ - public static final QualifierAndValue WILDCARD_NAMED = QualifierAndValueDefault.createNamed("*"); + public static final QualifierAndValue WILDCARD_NAMED = QualifierAndValueDefault.createNamed(WILDCARD); private CommonQualifiers() { } diff --git a/pico/api/src/main/java/io/helidon/pico/api/InjectionPointProvider.java b/pico/api/src/main/java/io/helidon/pico/api/InjectionPointProvider.java index 2c5270800ae..2c8d4f609af 100644 --- a/pico/api/src/main/java/io/helidon/pico/api/InjectionPointProvider.java +++ b/pico/api/src/main/java/io/helidon/pico/api/InjectionPointProvider.java @@ -43,7 +43,7 @@ public interface InjectionPointProvider extends Provider { @Override default T get() { return first(PicoServices.SERVICE_QUERY_REQUIRED) - .orElseThrow(() -> couldNotFindMatch()); + .orElseThrow(this::couldNotFindMatch); } /** diff --git a/pico/api/src/main/java/io/helidon/pico/api/InvocationContext.java b/pico/api/src/main/java/io/helidon/pico/api/InvocationContext.java index ac868e43058..5a37a2537a1 100644 --- a/pico/api/src/main/java/io/helidon/pico/api/InvocationContext.java +++ b/pico/api/src/main/java/io/helidon/pico/api/InvocationContext.java @@ -23,7 +23,7 @@ import io.helidon.builder.Builder; import io.helidon.common.types.AnnotationAndValue; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; import jakarta.inject.Provider; @@ -59,14 +59,14 @@ public interface InvocationContext { * * @return the element info represents the method (or the constructor) being invoked */ - TypedElementName elementInfo(); + TypedElementInfo elementInfo(); /** * The method/element argument info. * * @return the method/element argument info */ - Optional elementArgInfo(); + Optional elementArgInfo(); /** * The interceptor chain. diff --git a/pico/api/src/main/java/io/helidon/pico/api/ServiceProviderBindable.java b/pico/api/src/main/java/io/helidon/pico/api/ServiceProviderBindable.java index fcbc8ebc215..fc8c2335b3d 100644 --- a/pico/api/src/main/java/io/helidon/pico/api/ServiceProviderBindable.java +++ b/pico/api/src/main/java/io/helidon/pico/api/ServiceProviderBindable.java @@ -108,6 +108,15 @@ default void rootProvider(ServiceProvider rootProvider) { throw new UnsupportedOperationException(); } + /** + * Returns the previously assigned {@link PicoServices} instance. + * + * @return the previously assigned pico services instance, or empty if never assigned + * + * @see #picoServices(Optional) + */ + Optional picoServices(); + /** * Assigns the services instance this provider is bound to. A service provider can be associated with 0..1 services instance. * If not set, the service provider should use {@link PicoServices#picoServices()} to ascertain the instance. diff --git a/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/ConfiguredByAnnotationProcessor.java b/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/ConfiguredByAnnotationProcessor.java index 9918283c47e..8af3e69bc3b 100644 --- a/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/ConfiguredByAnnotationProcessor.java +++ b/pico/configdriven/processor/src/main/java/io/helidon/pico/configdriven/processor/ConfiguredByAnnotationProcessor.java @@ -26,7 +26,7 @@ import io.helidon.common.types.AnnotationAndValue; import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; import io.helidon.pico.processor.PicoAnnotationProcessor; import io.helidon.pico.tools.ActivatorCreatorProvider; import io.helidon.pico.tools.ServicesToProcess; @@ -65,7 +65,7 @@ protected Set supportedServiceClassTargetAnnotations() { protected void processExtensions(ServicesToProcess services, TypeInfo service, Set serviceTypeNamesToCodeGenerate, - Collection allElementsOfInterest) { + Collection allElementsOfInterest) { Optional configuredByAnno = findFirst(PICO_CONFIGURED_BY, service.annotations()); if (configuredByAnno.isEmpty()) { return; diff --git a/pico/configdriven/runtime/src/main/java/io/helidon/pico/configdriven/runtime/AbstractConfiguredServiceProvider.java b/pico/configdriven/runtime/src/main/java/io/helidon/pico/configdriven/runtime/AbstractConfiguredServiceProvider.java index b244a83d958..0b762eed268 100644 --- a/pico/configdriven/runtime/src/main/java/io/helidon/pico/configdriven/runtime/AbstractConfiguredServiceProvider.java +++ b/pico/configdriven/runtime/src/main/java/io/helidon/pico/configdriven/runtime/AbstractConfiguredServiceProvider.java @@ -234,8 +234,7 @@ public void onPhaseEvent(Event event, Phase phase) { if (phase == Phase.POST_BIND_ALL_MODULES) { assertIsInitializing(); - PicoServices picoServices = picoServices(); - assert (picoServices != null); + PicoServices picoServices = picoServices().orElseThrow(); if (Phase.INIT == currentActivationPhase()) { LogEntryAndResult logEntryAndResult = createLogEntryAndResult(Phase.PENDING); @@ -804,7 +803,7 @@ private Optional> innerPreActivateManag // override our service info instance.serviceInfo(newServiceInfo); - instance.picoServices(Optional.of(picoServices())); + instance.picoServices(picoServices()); instance.rootProvider(this); if (logger().isLoggable(System.Logger.Level.DEBUG)) { diff --git a/pico/configdriven/runtime/src/main/java/io/helidon/pico/configdriven/runtime/UnconfiguredServiceProvider.java b/pico/configdriven/runtime/src/main/java/io/helidon/pico/configdriven/runtime/UnconfiguredServiceProvider.java index 71ebfb8b813..faaf15cc71b 100644 --- a/pico/configdriven/runtime/src/main/java/io/helidon/pico/configdriven/runtime/UnconfiguredServiceProvider.java +++ b/pico/configdriven/runtime/src/main/java/io/helidon/pico/configdriven/runtime/UnconfiguredServiceProvider.java @@ -79,7 +79,7 @@ public DependenciesInfo dependencies() { } @Override - public PicoServices picoServices() { + public Optional picoServices() { return delegate.picoServices(); } diff --git a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractApplicationCreatorMojo.java b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractApplicationCreatorMojo.java index a8066a3248b..6eca1aa51c7 100644 --- a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractApplicationCreatorMojo.java +++ b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractApplicationCreatorMojo.java @@ -230,7 +230,10 @@ Set getServiceTypeNamesForExclusion() { @Override protected void innerExecute() { - this.permittedProviderType = PermittedProviderType.valueOf(permittedProviderTypes.toUpperCase()); + this.permittedProviderType = + (permittedProviderTypes == null || permittedProviderTypes.isBlank()) + ? ApplicationCreatorConfigOptions.DEFAULT_PERMITTED_PROVIDER_TYPE + : PermittedProviderType.valueOf(permittedProviderTypes.toUpperCase()); CallingContext callCtx = null; Optional callingContextBuilder = @@ -297,7 +300,7 @@ protected void innerExecute() { : null; String moduleInfoModuleName = getThisModuleName(); Optional> moduleSp = lookupThisModule(moduleInfoModuleName, services, false); - String packageName = determinePackageName(moduleSp, serviceTypeNames, descriptor, true); + String packageName = determinePackageName(moduleSp, serviceTypeNames, Optional.ofNullable(descriptor), true); CodeGenPaths codeGenPaths = CodeGenPathsDefault.builder() .generatedSourcesPath(getGeneratedSourceDirectory().getPath()) diff --git a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractCreatorMojo.java b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractCreatorMojo.java index eb0269bea96..763e43e94ac 100644 --- a/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractCreatorMojo.java +++ b/pico/maven-plugin/src/main/java/io/helidon/pico/maven/plugin/AbstractCreatorMojo.java @@ -260,16 +260,16 @@ public void execute() throws MojoExecutionException { /** * Determines the primary package name (which also typically doubles as the application name). * - * @param optModuleSp the module service provider - * @param typeNames the type names - * @param descriptor the descriptor - * @param persistIt pass true to write it to scratch, so that we can use it in the future for this module + * @param optModuleSp the module service provider + * @param typeNames the type names + * @param optDescriptor the descriptor + * @param persistIt pass true to write it to scratch, so that we can use it in the future for this module * @return the package name (which also typically doubles as the application name) */ - protected String determinePackageName(Optional> optModuleSp, - Collection typeNames, - ModuleInfoDescriptor descriptor, - boolean persistIt) { + String determinePackageName(Optional> optModuleSp, + Collection typeNames, + Optional optDescriptor, + boolean persistIt) { String packageName = getPackageName(); if (packageName == null) { // check for the existence of the file @@ -281,8 +281,10 @@ protected String determinePackageName(Optional> ServiceProvider moduleSp = optModuleSp.orElse(null); if (moduleSp != null) { packageName = TypeNameDefault.createFromTypeName(moduleSp.serviceInfo().serviceTypeName()).packageName(); + } else if (optDescriptor.isPresent()) { + packageName = toSuggestedGeneratedPackageName(optDescriptor.get(), typeNames, PicoServicesConfig.NAME); } else { - packageName = toSuggestedGeneratedPackageName(descriptor, typeNames, PicoServicesConfig.NAME); + packageName = toSuggestedGeneratedPackageName(typeNames, PicoServicesConfig.NAME); } } diff --git a/pico/processor/pom.xml b/pico/processor/pom.xml index 1316255e2d7..70e806589da 100644 --- a/pico/processor/pom.xml +++ b/pico/processor/pom.xml @@ -57,6 +57,29 @@ hamcrest-all test + + org.mockito + mockito-core + test + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + + + + + diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/ActiveProcessorUtils.java b/pico/processor/src/main/java/io/helidon/pico/processor/ActiveProcessorUtils.java index f6572b8ae23..d82515ca722 100644 --- a/pico/processor/src/main/java/io/helidon/pico/processor/ActiveProcessorUtils.java +++ b/pico/processor/src/main/java/io/helidon/pico/processor/ActiveProcessorUtils.java @@ -38,7 +38,7 @@ import io.helidon.builder.processor.spi.TypeInfoCreatorProvider; import io.helidon.common.HelidonServiceLoader; import io.helidon.common.types.TypeInfo; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; import io.helidon.pico.tools.Messager; import io.helidon.pico.tools.ModuleInfoDescriptor; import io.helidon.pico.tools.Options; @@ -117,7 +117,10 @@ public void error(String message, out(System.Logger.Level.ERROR, Diagnostic.Kind.ERROR, message, null); } - void out(System.Logger.Level level, Diagnostic.Kind kind, String message, Throwable t) { + void out(System.Logger.Level level, + Diagnostic.Kind kind, + String message, + Throwable t) { if (logger.isLoggable(level)) { logger.log(level, getClass().getSimpleName() + ": " + message, t); } @@ -187,14 +190,10 @@ void relayModuleInfoToServicesToProcess(ServicesToProcess servicesToProcess) { */ Optional toTypeInfo(TypeElement element, TypeMirror mirror, - Predicate isOneWeCareAbout) { + Predicate isOneWeCareAbout) { return typeInfoCreatorProvider.createTypeInfo(element, mirror, processingEnv, isOneWeCareAbout); } - System.Logger.Level loggerLevel() { - return (Options.isOptionEnabled(Options.TAG_DEBUG)) ? System.Logger.Level.INFO : System.Logger.Level.DEBUG; - } - RoundEnvironment roundEnv() { return roundEnv; } diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/CustomAnnotationProcessor.java b/pico/processor/src/main/java/io/helidon/pico/processor/CustomAnnotationProcessor.java index 065c353e1e9..7c2f3e9a4d8 100644 --- a/pico/processor/src/main/java/io/helidon/pico/processor/CustomAnnotationProcessor.java +++ b/pico/processor/src/main/java/io/helidon/pico/processor/CustomAnnotationProcessor.java @@ -16,13 +16,13 @@ package io.helidon.pico.processor; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -31,7 +31,6 @@ import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.element.Element; -import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; @@ -39,7 +38,6 @@ import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNameDefault; -import io.helidon.common.types.TypedElementName; import io.helidon.pico.api.ServiceInfoBasics; import io.helidon.pico.tools.AbstractFilerMessager; import io.helidon.pico.tools.CodeGenFiler; @@ -54,9 +52,7 @@ import static io.helidon.pico.processor.GeneralProcessorUtils.hasValue; import static io.helidon.pico.processor.GeneralProcessorUtils.rootStackTraceElementOf; import static io.helidon.pico.tools.TypeTools.createTypeNameFromElement; -import static io.helidon.pico.tools.TypeTools.createTypedElementNameFromElement; -import static io.helidon.pico.tools.TypeTools.isStatic; -import static io.helidon.pico.tools.TypeTools.toAccess; +import static io.helidon.pico.tools.TypeTools.createTypedElementInfoFromElement; import static io.helidon.pico.tools.TypeTools.toFilePath; /** @@ -77,9 +73,7 @@ public CustomAnnotationProcessor() { } static List initialize() { - // note: it is important to use this class' CL since maven will not give us the "right" one. - List creators = HelidonServiceLoader.create(ServiceLoader.load( - CustomAnnotationTemplateCreator.class, CustomAnnotationTemplateCreator.class.getClassLoader())).asList(); + List creators = HelidonServiceLoader.create(loader()).asList(); creators.forEach(creator -> { try { Set annoTypes = creator.annoTypes(); @@ -186,12 +180,12 @@ void doFiler(CustomAnnotationTemplateResponse response) { AbstractFilerMessager filer = AbstractFilerMessager.createAnnotationBasedFiler(processingEnv, utils()); CodeGenFiler codegen = CodeGenFiler.create(filer); response.generatedSourceCode().forEach(codegen::codegenJavaFilerOut); - response.generatedResources().forEach((typedElementName, resourceBody) -> { - String fileType = typedElementName.elementName(); + response.generatedResources().forEach((TypedElementInfo, resourceBody) -> { + String fileType = TypedElementInfo.elementName(); if (!hasValue(fileType)) { fileType = ".generated"; } - codegen.codegenResourceFilerOut(toFilePath(typedElementName.typeName(), fileType), resourceBody); + codegen.codegenResourceFilerOut(toFilePath(TypedElementInfo.typeName(), fileType), resourceBody); }); } @@ -238,26 +232,21 @@ CustomAnnotationTemplateRequestDefault.Builder toRequestBuilder(TypeName annoTyp .filerEnabled(true) .annoTypeName(annoTypeName) .serviceInfo(siInfo) - .targetElement(createTypedElementNameFromElement(typeToProcess, elements).orElseThrow()) - .enclosingTypeInfo(enclosingClassTypeInfo) - // the following are duplicates that should be removed - get them from the enclosingTypeInfo instead - // see https://github.com/helidon-io/helidon/issues/6773 - .targetElementArgs(toArgs(typeToProcess)) - .targetElementAccess(toAccess(typeToProcess)) - .elementStatic(isStatic(typeToProcess)); + .targetElement(createTypedElementInfoFromElement(typeToProcess, elements).orElseThrow()) + .enclosingTypeInfo(enclosingClassTypeInfo); } - List toArgs(Element typeToProcess) { - if (!(typeToProcess instanceof ExecutableElement)) { - return List.of(); + private static ServiceLoader loader() { + try { + // note: it is important to use this class' CL since maven will not give us the "right" one. + return ServiceLoader.load( + CustomAnnotationTemplateCreator.class, CustomAnnotationTemplateCreator.class.getClassLoader()); + } catch (ServiceConfigurationError e) { + // see issue #6261 - running inside the IDE? + // this version will use the thread ctx classloader + System.getLogger(CustomAnnotationProcessor.class.getName()).log(System.Logger.Level.WARNING, e.getMessage(), e); + return ServiceLoader.load(CustomAnnotationTemplateCreator.class); } - - Elements elements = processingEnv.getElementUtils(); - List result = new ArrayList<>(); - ExecutableElement executableElement = (ExecutableElement) typeToProcess; - executableElement.getParameters().forEach(v -> result.add( - createTypedElementNameFromElement(v, elements).orElseThrow())); - return result; } private static TypeElement toEnclosingClassTypeElement(Element typeToProcess) { diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/GeneralProcessorUtils.java b/pico/processor/src/main/java/io/helidon/pico/processor/GeneralProcessorUtils.java index cbcd4b77600..7b34b77f972 100644 --- a/pico/processor/src/main/java/io/helidon/pico/processor/GeneralProcessorUtils.java +++ b/pico/processor/src/main/java/io/helidon/pico/processor/GeneralProcessorUtils.java @@ -35,7 +35,7 @@ import io.helidon.common.types.AnnotationAndValueDefault; import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; import io.helidon.pico.api.QualifierAndValue; import io.helidon.pico.api.QualifierAndValueDefault; import io.helidon.pico.api.RunLevel; @@ -55,7 +55,7 @@ * * @see ActiveProcessorUtils */ -final class GeneralProcessorUtils { +public final class GeneralProcessorUtils { private GeneralProcessorUtils() { } @@ -185,12 +185,12 @@ static Optional toWeight(TypeInfo service) { * @return the post construct method if available */ static Optional toPostConstructMethod(TypeInfo service) { - List postConstructs = service.elementInfo().stream() + List postConstructs = service.interestingElementInfo().stream() .filter(it -> { AnnotationAndValue anno = findFirst(PostConstruct.class, it.annotations()).orElse(null); return (anno != null); }) - .map(TypedElementName::elementName) + .map(TypedElementInfo::elementName) .toList(); if (postConstructs.size() == 1) { return Optional.of(postConstructs.get(0)); @@ -215,12 +215,12 @@ static Optional toPostConstructMethod(TypeInfo service) { * @return the pre destroy method if available */ static Optional toPreDestroyMethod(TypeInfo service) { - List preDestroys = service.elementInfo().stream() + List preDestroys = service.interestingElementInfo().stream() .filter(it -> { AnnotationAndValue anno = findFirst(PreDestroy.class, it.annotations()).orElse(null); return (anno != null); }) - .map(TypedElementName::elementName) + .map(TypedElementInfo::elementName) .toList(); if (preDestroys.size() == 1) { return Optional.of(preDestroys.get(0)); @@ -311,7 +311,7 @@ static Set toQualifiers(TypeInfo service) { * @param service the service for which the typed element belongs * @return the qualifiers associated with the provided element */ - static Set toQualifiers(TypedElementName element, + static Set toQualifiers(TypedElementInfo element, TypeInfo service) { Set result = new LinkedHashSet<>(); @@ -335,7 +335,7 @@ static Set toQualifiers(TypedElementName element, * @param typeName the type name to check * @return true if the provided type is a provider type. */ - static boolean isProviderType(TypeName typeName) { + public static boolean isProviderType(TypeName typeName) { String name = typeName.name(); return (name.equals(TypeNames.JAKARTA_PROVIDER) || name.equals(TypeNames.JAVAX_PROVIDER) @@ -355,27 +355,34 @@ static boolean hasValue(String val) { /** * Looks for either a jakarta or javax annotation. * - * @param jakartaAnno the jakarta annotation class type + * @param annoType the annotation type * @param annotations the set of annotations to look in * @return the optional annotation if there is a match */ - static Optional findFirst(Class jakartaAnno, - Collection annotations) { - return findFirst(jakartaAnno.getName(), annotations); + public static Optional findFirst(Class annoType, + Collection annotations) { + return findFirst(annoType.getName(), annotations); } - static Optional findFirst(String jakartaAnnoName, + static Optional findFirst(String annoTypeName, Collection annotations) { if (annotations == null) { return Optional.empty(); } - Optional anno = AnnotationAndValueDefault.findFirst(jakartaAnnoName, annotations); + Optional anno = AnnotationAndValueDefault.findFirst(annoTypeName, annotations); if (anno.isPresent()) { return anno; } - return AnnotationAndValueDefault.findFirst(TypeTools.oppositeOf(jakartaAnnoName), annotations); + boolean startsWithJakarta = annoTypeName.startsWith(TypeNames.PREFIX_JAKARTA); + boolean startsWithJavax = !startsWithJakarta && annoTypeName.startsWith(TypeNames.PREFIX_JAVAX); + + if (startsWithJakarta || startsWithJavax) { + return AnnotationAndValueDefault.findFirst(TypeTools.oppositeOf(annoTypeName), annotations); + } + + return Optional.empty(); } static ServiceInfoBasics toBasicServiceInfo(TypeInfo service) { diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/GenericTemplateCreatorDefault.java b/pico/processor/src/main/java/io/helidon/pico/processor/GenericTemplateCreatorDefault.java index 56300256648..eca676de395 100644 --- a/pico/processor/src/main/java/io/helidon/pico/processor/GenericTemplateCreatorDefault.java +++ b/pico/processor/src/main/java/io/helidon/pico/processor/GenericTemplateCreatorDefault.java @@ -114,14 +114,10 @@ Map gatherSubstitutions(GenericTemplateCreatorRequest genericReq substitutions.put("basicServiceInfo", req.serviceInfo()); substitutions.put("weight", req.serviceInfo().realizedWeight()); substitutions.put("runLevel", req.serviceInfo().realizedRunLevel()); - substitutions.put("elementAccess", req.targetElementAccess()); - substitutions.put("elementIsStatic", req.isElementStatic()); substitutions.put("elementKind", req.targetElement().elementTypeKind()); substitutions.put("elementName", req.targetElement().elementName()); substitutions.put("elementAnnotations", req.targetElement().annotations()); substitutions.put("elementEnclosingTypeName", req.targetElement().typeName()); - substitutions.put("elementArgs", req.targetElementArgs()); - substitutions.put("elementArgs-declaration", GeneralProcessorUtils.toString(req.targetElementArgs())); substitutions.putAll(genericRequest.overrideProperties()); return substitutions; } diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/PicoAnnotationProcessor.java b/pico/processor/src/main/java/io/helidon/pico/processor/PicoAnnotationProcessor.java index bed22ff111d..9b47cdb437d 100644 --- a/pico/processor/src/main/java/io/helidon/pico/processor/PicoAnnotationProcessor.java +++ b/pico/processor/src/main/java/io/helidon/pico/processor/PicoAnnotationProcessor.java @@ -16,6 +16,8 @@ package io.helidon.pico.processor; +import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; @@ -24,6 +26,8 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; @@ -36,11 +40,13 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; +import io.helidon.common.HelidonServiceLoader; import io.helidon.common.types.AnnotationAndValue; import io.helidon.common.types.AnnotationAndValueDefault; import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypeNameDefault; +import io.helidon.common.types.TypedElementInfo; import io.helidon.pico.api.Activator; import io.helidon.pico.api.Contract; import io.helidon.pico.api.DependenciesInfo; @@ -49,11 +55,16 @@ import io.helidon.pico.api.PicoServicesConfig; import io.helidon.pico.api.QualifierAndValue; import io.helidon.pico.api.ServiceInfoBasics; +import io.helidon.pico.processor.spi.PicoAnnotationProcessorObserver; +import io.helidon.pico.processor.spi.ProcessingEvent; +import io.helidon.pico.processor.spi.ProcessingEventDefault; import io.helidon.pico.runtime.Dependencies; import io.helidon.pico.tools.ActivatorCreatorCodeGen; +import io.helidon.pico.tools.ActivatorCreatorCodeGenDefault; import io.helidon.pico.tools.ActivatorCreatorConfigOptionsDefault; import io.helidon.pico.tools.ActivatorCreatorDefault; import io.helidon.pico.tools.ActivatorCreatorRequest; +import io.helidon.pico.tools.ActivatorCreatorRequestDefault; import io.helidon.pico.tools.ActivatorCreatorResponse; import io.helidon.pico.tools.InterceptionPlan; import io.helidon.pico.tools.InterceptorCreatorProvider; @@ -68,6 +79,7 @@ import jakarta.annotation.PreDestroy; import jakarta.inject.Inject; +import static io.helidon.builder.processor.tools.BeanUtils.isBuiltInJavaType; import static io.helidon.builder.processor.tools.BuilderTypeTools.createTypeNameFromElement; import static io.helidon.common.types.TypeNameDefault.createFromTypeName; import static io.helidon.pico.processor.ActiveProcessorUtils.MAYBE_ANNOTATIONS_CLAIMED_BY_THIS_PROCESSOR; @@ -81,7 +93,11 @@ import static io.helidon.pico.processor.GeneralProcessorUtils.toScopeNames; import static io.helidon.pico.processor.GeneralProcessorUtils.toServiceTypeHierarchy; import static io.helidon.pico.processor.GeneralProcessorUtils.toWeight; -import static io.helidon.pico.tools.TypeTools.createTypedElementNameFromElement; +import static io.helidon.pico.processor.ProcessingTracker.DEFAULT_SCRATCH_FILE_NAME; +import static io.helidon.pico.processor.ProcessingTracker.initializeFrom; +import static io.helidon.pico.tools.CodeGenFiler.scratchClassOutputPath; +import static io.helidon.pico.tools.CodeGenFiler.targetClassOutputPath; +import static io.helidon.pico.tools.TypeTools.createTypedElementInfoFromElement; import static io.helidon.pico.tools.TypeTools.toAccess; import static java.util.Objects.requireNonNull; @@ -110,8 +126,9 @@ public class PicoAnnotationProcessor extends BaseAnnotationProcessor { TypeNames.JAVAX_PRE_DESTROY, TypeNames.JAVAX_POST_CONSTRUCT); - private final Set allElementsOfInterestInThisModule = new LinkedHashSet<>(); + private final Set allElementsOfInterestInThisModule = new LinkedHashSet<>(); private final Map typeInfoToCreateActivatorsForInThisModule = new LinkedHashMap<>(); + private ProcessingTracker tracker; private CreatorHandler creator; private boolean autoAddInterfaces; @@ -149,10 +166,7 @@ public void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); this.autoAddInterfaces = Options.isOptionEnabled(Options.TAG_AUTO_ADD_NON_CONTRACT_INTERFACES); this.creator = new CreatorHandler(getClass().getSimpleName(), processingEnv, utils()); -// if (BaseAnnotationProcessor.ENABLED) { -// // we are is simulation mode when the base one is operating... -// this.creator.activateSimulationMode(); -// } + this.tracker = initializeFrom(trackerStatePath(), processingEnv); } @Override @@ -165,11 +179,10 @@ public boolean process(Set annotations, } ServicesToProcess.onBeginProcessing(utils(), getSupportedAnnotationTypes(), roundEnv); -// ServicesToProcess.addOnDoneRunnable(CreatorHandler.reporting()); try { // build the model - Set elementsOfInterestInThisRound = gatherElementsOfInterestInThisModule(); + Set elementsOfInterestInThisRound = gatherElementsOfInterestInThisModule(); validate(elementsOfInterestInThisRound); allElementsOfInterestInThisModule.addAll(elementsOfInterestInThisRound); @@ -185,6 +198,8 @@ public boolean process(Set annotations, doFiler(services); } + notifyObservers(); + return MAYBE_ANNOTATIONS_CLAIMED_BY_THIS_PROCESSOR; } catch (Throwable t) { ToolsException exc = new ToolsException("Error while processing: " + t @@ -243,6 +258,7 @@ protected Set supportedElementTargetAnnotations() { * Code generate these {@link io.helidon.pico.api.Activator}'s ad {@link io.helidon.pico.api.ModuleComponent}'s. * * @param services the services to code generate + * @throws ToolsException if there is problem code generating sources or resources */ protected void doFiler(ServicesToProcess services) { ActivatorCreatorCodeGen codeGen = ActivatorCreatorDefault.createActivatorCreatorCodeGen(services).orElse(null); @@ -257,8 +273,28 @@ protected void doFiler(ServicesToProcess services) { .build(); ActivatorCreatorRequest req = ActivatorCreatorDefault .createActivatorCreatorRequest(services, codeGen, configOptions, creator.filer(), false); + Set allActivatorTypeNames = tracker.remainingTypeNames().stream() + .map(TypeNameDefault::createFromTypeName) + .collect(Collectors.toSet()); + if (!allActivatorTypeNames.isEmpty()) { + req = ActivatorCreatorRequestDefault.toBuilder(req) + .codeGen(ActivatorCreatorCodeGenDefault.toBuilder(req.codeGen()) + .allModuleActivatorTypeNames(allActivatorTypeNames) + .build()) + .build(); + } ActivatorCreatorResponse res = creator.createModuleActivators(req); - if (!res.success()) { + if (res.success()) { + res.activatorTypeNamesPutInComponentModule() + .forEach(it -> tracker.processing(it.name())); + if (processingOver) { + try { + tracker.close(); + } catch (IOException e) { + throw new ToolsException(e.getMessage(), e); + } + } + } else { ToolsException exc = new ToolsException("Error during codegen", res.error().orElse(null)); utils().error(exc.getMessage(), exc); // should not get here since the error above should halt further processing @@ -271,34 +307,38 @@ protected void doFiler(ServicesToProcess services) { * * @param elementsOfInterest the elements that are eligible for some form of Pico processing */ - protected void validate(Collection elementsOfInterest) { - validatePerClass(elementsOfInterest, - "There can be max of one injectable constructor per class", - 1, - (it) -> it.elementTypeKind().equals(TypeInfo.KIND_CONSTRUCTOR) - && GeneralProcessorUtils.findFirst(Inject.class, it.annotations()).isPresent()); - validatePerClass(elementsOfInterest, - "There can be max of one PostConstruct method per class", - 1, - (it) -> it.elementTypeKind().equals(TypeInfo.KIND_METHOD) - && GeneralProcessorUtils.findFirst(PostConstruct.class, it.annotations()).isPresent()); - validatePerClass(elementsOfInterest, - "There can be max of one PreDestroy method per class", - 1, - (it) -> it.elementTypeKind().equals(TypeInfo.KIND_METHOD) - && GeneralProcessorUtils.findFirst(PreDestroy.class, it.annotations()).isPresent()); - validatePerClass(elementsOfInterest, - PicoServicesConfig.NAME + " does not currently support static or private elements", - 0, - (it) -> toModifierNames(it.modifierNames()).contains(TypeInfo.MODIFIER_PRIVATE) - || toModifierNames(it.modifierNames()).contains(TypeInfo.MODIFIER_STATIC)); + protected void validate(Collection elementsOfInterest) { + validatePerClass( + elementsOfInterest, + "There can be max of one injectable constructor per class", + 1, + (it) -> it.elementTypeKind().equals(TypeInfo.KIND_CONSTRUCTOR) + && GeneralProcessorUtils.findFirst(Inject.class, it.annotations()).isPresent()); + validatePerClass( + elementsOfInterest, + "There can be max of one PostConstruct method per class", + 1, + (it) -> it.elementTypeKind().equals(TypeInfo.KIND_METHOD) + && GeneralProcessorUtils.findFirst(PostConstruct.class, it.annotations()).isPresent()); + validatePerClass( + elementsOfInterest, + "There can be max of one PreDestroy method per class", + 1, + (it) -> it.elementTypeKind().equals(TypeInfo.KIND_METHOD) + && GeneralProcessorUtils.findFirst(PreDestroy.class, it.annotations()).isPresent()); + validatePerClass( + elementsOfInterest, + PicoServicesConfig.NAME + " does not currently support static or private elements", + 0, + (it) -> it.modifierNames().stream().anyMatch(TypeInfo.MODIFIER_PRIVATE::equalsIgnoreCase) + || it.modifierNames().stream().anyMatch(TypeInfo.MODIFIER_STATIC::equalsIgnoreCase)); } - private void validatePerClass(Collection elementsOfInterest, + private void validatePerClass(Collection elementsOfInterest, String msg, int maxAllowed, - Predicate matcher) { - Map> allTypeNamesToMatchingElements = new LinkedHashMap<>(); + Predicate matcher) { + Map> allTypeNamesToMatchingElements = new LinkedHashMap<>(); elementsOfInterest.stream() .filter(matcher) .forEach(it -> allTypeNamesToMatchingElements @@ -332,7 +372,7 @@ protected Set interceptorAndValidate(Collection typesToCreat protected void process(ServicesToProcess services, TypeInfo service, Set serviceTypeNamesToCodeGenerate, - Collection allElementsOfInterest) { + Collection allElementsOfInterest) { utils().debug("Code generating" + Activator.class.getSimpleName() + " for: " + service.typeName()); processBasics(services, service, serviceTypeNamesToCodeGenerate, allElementsOfInterest); processInterceptors(services, service, serviceTypeNamesToCodeGenerate, allElementsOfInterest); @@ -351,14 +391,14 @@ protected void process(ServicesToProcess services, protected void processBasics(ServicesToProcess services, TypeInfo service, Set serviceTypeNamesToCodeGenerate, - Collection allElementsOfInterest) { + Collection allElementsOfInterest) { TypeName serviceTypeName = service.typeName(); TypeInfo superTypeInfo = service.superTypeInfo().orElse(null); if (superTypeInfo != null) { TypeName superTypeName = superTypeInfo.typeName(); services.addParentServiceType(serviceTypeName, superTypeName); } - Set modifierNames = toModifierNames(service.modifierNames()); + Set modifierNames = service.modifierNames(); toRunLevel(service).ifPresent(it -> services.addDeclaredRunLevel(serviceTypeName, it)); toWeight(service).ifPresent(it -> services.addDeclaredWeight(serviceTypeName, it)); @@ -369,7 +409,7 @@ protected void processBasics(ServicesToProcess services, services.addAccessLevel(serviceTypeName, toAccess(modifierNames)); services.addIsAbstract(serviceTypeName, - modifierNames.contains(TypeInfo.MODIFIER_ABSTRACT)); + modifierNames.stream().anyMatch(TypeInfo.MODIFIER_ABSTRACT::equalsIgnoreCase)); services.addServiceTypeHierarchy(serviceTypeName, toServiceTypeHierarchy(service)); services.addQualifiers(serviceTypeName, @@ -389,7 +429,7 @@ protected void processBasics(ServicesToProcess services, private void processInterceptors(ServicesToProcess services, TypeInfo service, Set serviceTypeNamesToCodeGenerate, - Collection allElementsOfInterest) { + Collection allElementsOfInterest) { TypeName serviceTypeName = service.typeName(); InterceptorCreator interceptorCreator = InterceptorCreatorProvider.instance(); ServiceInfoBasics interceptedServiceInfo = toBasicServiceInfo(service); @@ -422,24 +462,24 @@ private void processInterceptors(ServicesToProcess services, protected void processExtensions(ServicesToProcess services, TypeInfo service, Set serviceTypeNamesToCodeGenerate, - Collection allElementsOfInterest) { + Collection allElementsOfInterest) { // NOP; expected that derived classes will implement this } /** * Finds the first jakarta or javax annotation matching the given jakarta annotation class name. * - * @param jakartaAnnoName the jakarta annotation class name - * @param annotations all of the annotations to search through + * @param annoTypeName the annotation class name + * @param annotations all of the annotations to search through * @return the annotation, or empty if not found */ - protected Optional findFirst(String jakartaAnnoName, + protected Optional findFirst(String annoTypeName, Collection annotations) { - return GeneralProcessorUtils.findFirst(jakartaAnnoName, annotations); + return GeneralProcessorUtils.findFirst(annoTypeName, annotations); } private ServicesToProcess toServicesToProcess(Set typesToCodeGenerate, - Collection allElementsOfInterest) { + Collection allElementsOfInterest) { ServicesToProcess services = ServicesToProcess.create(); utils().relayModuleInfoToServicesToProcess(services); @@ -500,14 +540,11 @@ private void gatherContracts(Set contracts, if (fqProviderTypeName != null) { if (!genericTypeName.generic()) { providerForSet.add(genericTypeName); - - Optional moduleName = filterModuleName(typeInfo.moduleNameOf(genericTypeName)); - moduleName.ifPresent(externalModuleNamesRequired::add); - if (moduleName.isPresent()) { - externalContracts.add(genericTypeName); - } else { - contracts.add(genericTypeName); - } + extractModuleAndContract(contracts, + externalContracts, + externalModuleNamesRequired, + typeInfo, + genericTypeName); } // if we are dealing with a Provider<> then we should add those too as module dependencies @@ -525,13 +562,11 @@ private void gatherContracts(Set contracts, || !isTypeAnInterface || AnnotationAndValueDefault.findFirst(Contract.class, typeInfo.annotations()).isPresent(); if (isTypeAContract) { - Optional moduleName = filterModuleName(typeInfo.moduleNameOf(genericTypeName)); - moduleName.ifPresent(externalModuleNamesRequired::add); - if (moduleName.isPresent()) { - externalContracts.add(genericTypeName); - } else { - contracts.add(genericTypeName); - } + extractModuleAndContract(contracts, + externalContracts, + externalModuleNamesRequired, + typeInfo, + genericTypeName); } } } @@ -570,6 +605,20 @@ private void gatherContracts(Set contracts, true)); } + private void extractModuleAndContract(Set contracts, + Set externalContracts, + Set externalModuleNamesRequired, + TypeInfo typeInfo, + TypeName genericTypeName) { + Optional moduleName = filterModuleName(typeInfo.moduleNameOf(genericTypeName)); + moduleName.ifPresent(externalModuleNamesRequired::add); + if (moduleName.isPresent() || isBuiltInJavaType(genericTypeName)) { + externalContracts.add(genericTypeName); + } else { + contracts.add(genericTypeName); + } + } + private Optional filterModuleName(Optional moduleName) { String name = moduleName.orElse(null); if (name != null && (name.startsWith("java.") || name.startsWith("jdk"))) { @@ -579,7 +628,7 @@ private Optional filterModuleName(Optional moduleName) { } private Optional toInjectionDependencies(TypeInfo service, - Collection allElementsOfInterest) { + Collection allElementsOfInterest) { Dependencies.BuilderContinuation builder = Dependencies.builder(service.typeName().name()); gatherInjectionPoints(builder, service, allElementsOfInterest); DependenciesInfo deps = builder.build(); @@ -588,18 +637,42 @@ private Optional toInjectionDependencies(TypeInfo service, private void gatherInjectionPoints(Dependencies.BuilderContinuation builder, TypeInfo service, - Collection allElementsOfInterest) { - List injectableElementsForThisService = allElementsOfInterest.stream() + Collection allElementsOfInterest) { + List injectableElementsForThisService = allElementsOfInterest.stream() .filter(it -> GeneralProcessorUtils.findFirst(Inject.class, it.annotations()).isPresent()) .filter(it -> service.typeName().equals(it.enclosingTypeName().orElseThrow())) .toList(); injectableElementsForThisService - .forEach(elem -> gatherInjectionPoints(builder, elem, service, toModifierNames(elem.modifierNames()))); + .forEach(elem -> gatherInjectionPoints(builder, elem, service, elem.modifierNames())); // // We expect activators at every level for abstract bases - we will therefore NOT recursive up the hierarchy // service.superTypeInfo().ifPresent(it -> gatherInjectionPoints(builder, it, allElementsOfInterest, false)); } + private void notifyObservers() { + List observers = HelidonServiceLoader.create(observerLoader()).asList(); + if (!observers.isEmpty()) { + ProcessingEvent event = ProcessingEventDefault.builder() + .processingEnvironment(processingEnv) + .elementsOfInterest(allElementsOfInterestInThisModule) + .build(); + observers.forEach(it -> it.onProcessingEvent(event)); + } + } + + private static ServiceLoader observerLoader() { + try { + // note: it is important to use this class' CL since maven will not give us the "right" one. + return ServiceLoader.load( + PicoAnnotationProcessorObserver.class, PicoAnnotationProcessorObserver.class.getClassLoader()); + } catch (ServiceConfigurationError e) { + // see issue #6261 - running inside the IDE? + // this version will use the thread ctx classloader + System.getLogger(PicoAnnotationProcessorObserver.class.getName()).log(System.Logger.Level.WARNING, e.getMessage(), e); + return ServiceLoader.load(PicoAnnotationProcessorObserver.class); + } + } + /** * Processes all of the injection points for the provided typed element, accumulating the result in the provided builder * continuation instance. @@ -609,7 +682,7 @@ private void gatherInjectionPoints(Dependencies.BuilderContinuation builder, * @param service the type info of the backing service */ private static void gatherInjectionPoints(Dependencies.BuilderContinuation builder, - TypedElementName typedElement, + TypedElementInfo typedElement, TypeInfo service, Set modifierNames) { String elemName = typedElement.elementName(); @@ -666,8 +739,8 @@ private static void gatherInjectionPoints(Dependencies.BuilderContinuation build } } - private Set gatherElementsOfInterestInThisModule() { - Set result = new LinkedHashSet<>(); + private Set gatherElementsOfInterestInThisModule() { + Set result = new LinkedHashSet<>(); Elements elementUtils = processingEnv.getElementUtils(); for (String annoType : supportedElementTargetAnnotations()) { @@ -675,7 +748,7 @@ private Set gatherElementsOfInterestInThisModule() { TypeElement annoTypeElement = elementUtils.getTypeElement(annoType); if (annoTypeElement != null) { Set typesToProcess = utils().roundEnv().getElementsAnnotatedWith(annoTypeElement); - typesToProcess.forEach(it -> result.add(createTypedElementNameFromElement(it, elementUtils).orElseThrow())); + typesToProcess.forEach(it -> result.add(createTypedElementInfoFromElement(it, elementUtils).orElseThrow())); } } @@ -683,7 +756,7 @@ private Set gatherElementsOfInterestInThisModule() { } private void gatherTypeInfosToProcessInThisModule(Map result, - Collection elementsOfInterest) { + Collection elementsOfInterest) { // this section gathers based upon the class-level annotations in order to discover what to process for (String annoType : supportedServiceClassTargetAnnotations()) { // annotation may not be on the classpath, in such a case just ignore it @@ -706,7 +779,7 @@ private void gatherTypeInfosToProcessInThisModule(Map result // this section gathers based upon the element-level annotations in order to discover what to process Set enclosingElementsOfInterest = elementsOfInterest.stream() - .map(TypedElementName::enclosingTypeName) + .map(TypedElementInfo::enclosingTypeName) .filter(Optional::isPresent) .map(Optional::get) .collect(Collectors.toSet()); @@ -723,9 +796,8 @@ private void gatherTypeInfosToProcessInThisModule(Map result }); } - // will be resolved in https://github.com/helidon-io/helidon/issues/6764 - private static Set toModifierNames(Set names) { - return names.stream().map(String::toUpperCase).collect(Collectors.toSet()); + private Path trackerStatePath() { + return scratchClassOutputPath(targetClassOutputPath(processingEnv.getFiler())).resolve(DEFAULT_SCRATCH_FILE_NAME); } } diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/ProcessingTracker.java b/pico/processor/src/main/java/io/helidon/pico/processor/ProcessingTracker.java new file mode 100644 index 00000000000..3bad1593ac2 --- /dev/null +++ b/pico/processor/src/main/java/io/helidon/pico/processor/ProcessingTracker.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.pico.processor; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.TypeElement; + +import io.helidon.pico.tools.ToolsException; + +/** + * This class adds persistent tracking (typically under ./target/XXX) to allow seamless full and/or incremental processing of + * types to be tracked over repeated compilation cycles over time. It is expected to be integrated into a host annotation + * processor implementation. + *

    + * For example, when incremental processing occurs, the elements passed to process in all rounds will just be a subset of + * all of the annotated services since the compiler (from the IDE) only recompiles the files that have been changed. This is + * typically different from how maven invokes compilation (doing a full compile where all types will be seen in the round). The + * {@link PicoAnnotationProcessor}, for example, would see this reduced subset of types in the round and would otherwise have + * created a {@link io.helidon.pico.api.ModuleComponent} only representative of the reduced subset of classes. This would be + * incorrect and lead to an invalid module component source file to have been generated. + *

    + * We use this tracker to persist the list of generated activators much in the same way that + * {@code META-INF/services} are tracked. A target scratch directory (i.e., target/pico in this case) is used instead - in order + * to keep it out of the build jar. + *

    + * Usage: + *

      + *
    1. {@link #initializeFrom} - during the APT initialization phase
    2. + *
    3. {@link #processing(String)} - during each processed type that the annotation processor visits in the round
    4. + *
    5. {@link #removedTypeNames()} or {@link #remainingTypeNames()} as needed - to see the changes over time
    6. + *
    7. {@link #close()} - during final lifecycle of the APT in order to persist state to be (re)written out to disk
    8. + *
    + * + * @see PicoAnnotationProcessor + */ +class ProcessingTracker implements AutoCloseable { + static final String DEFAULT_SCRATCH_FILE_NAME = "activators.lst"; + + private final Path path; + private final Set allTypeNames; + private final TypeElementFinder typeElementFinder; + private final Set foundOrProcessed = new LinkedHashSet<>(); + + /** + * Creates an instance using the given path to keep persistent state. + * + * @param persistentScratchPath the fully qualified path to carry the state + * @param allLines all lines read at initialization + * @param typeElementFinder the type element finder (e.g., {@link ProcessingEnvironment#getElementUtils}) + */ + ProcessingTracker(Path persistentScratchPath, + List allLines, + TypeElementFinder typeElementFinder) { + this.path = persistentScratchPath; + this.allTypeNames = new LinkedHashSet<>(allLines); + this.typeElementFinder = typeElementFinder; + } + + public static ProcessingTracker initializeFrom(Path persistentScratchPath, + ProcessingEnvironment processingEnv) { + List allLines = List.of(); + File file = persistentScratchPath.toFile(); + if (file.exists() && file.canRead()) { + try { + allLines = Files.readAllLines(persistentScratchPath, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new ToolsException(e.getMessage(), e); + } + } + return new ProcessingTracker(persistentScratchPath, allLines, toTypeElementFinder(processingEnv)); + } + + public ProcessingTracker processing(String typeName) { + foundOrProcessed.add(Objects.requireNonNull(typeName)); + return this; + } + + public Set allTypeNamesFromInitialization() { + return allTypeNames; + } + + public Set removedTypeNames() { + Set typeNames = new LinkedHashSet<>(allTypeNamesFromInitialization()); + typeNames.removeAll(remainingTypeNames()); + return typeNames; + } + + public Set remainingTypeNames() { + Set typeNames = new LinkedHashSet<>(allTypeNamesFromInitialization()); + typeNames.addAll(foundOrProcessed); + typeNames.removeIf(typeName -> !found(typeName)); + return typeNames; + } + + @Override + public void close() throws IOException { + Path parent = path.getParent(); + if (parent == null) { + throw new ToolsException("bad path: " + path); + } + Files.createDirectories(parent); + Files.write(path, remainingTypeNames(), StandardCharsets.UTF_8); + } + + private boolean found(String typeName) { + return (typeElementFinder.apply(typeName) != null); + } + + private static TypeElementFinder toTypeElementFinder(ProcessingEnvironment processingEnv) { + return typeName -> processingEnv.getElementUtils().getTypeElement(typeName); + } + + @FunctionalInterface + interface TypeElementFinder extends Function { + } + +} diff --git a/examples/webserver/jersey/src/main/java/io/helidon/reactive/webserver/examples/jersey/HelloWorld.java b/pico/processor/src/main/java/io/helidon/pico/processor/spi/PicoAnnotationProcessorObserver.java similarity index 53% rename from examples/webserver/jersey/src/main/java/io/helidon/reactive/webserver/examples/jersey/HelloWorld.java rename to pico/processor/src/main/java/io/helidon/pico/processor/spi/PicoAnnotationProcessorObserver.java index 6c445285332..a220ae4eb85 100644 --- a/examples/webserver/jersey/src/main/java/io/helidon/reactive/webserver/examples/jersey/HelloWorld.java +++ b/pico/processor/src/main/java/io/helidon/pico/processor/spi/PicoAnnotationProcessorObserver.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,26 +14,19 @@ * limitations under the License. */ -package io.helidon.reactive.webserver.examples.jersey; - -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.core.Response; +package io.helidon.pico.processor.spi; /** - * The Hello World Example JAX-RS resource. + * Implementations of these are service-loaded by the {@link io.helidon.pico.processor.PicoAnnotationProcessor}, and will be + * called to be able to observe processing events. */ -@Path("/") -public class HelloWorld { +public interface PicoAnnotationProcessorObserver { /** - * A simple resource returning {@code Hello World!} string. + * Called after a processing event that occurred in the {@link io.helidon.pico.processor.PicoAnnotationProcessor}. * - * @return {@code Hello World!} string as a response + * @param event the event */ - @GET - @Path("hello") - public Response hello() { - return Response.ok("Hello World!").build(); - } + void onProcessingEvent(ProcessingEvent event); + } diff --git a/pico/processor/src/main/java/io/helidon/pico/processor/spi/ProcessingEvent.java b/pico/processor/src/main/java/io/helidon/pico/processor/spi/ProcessingEvent.java new file mode 100644 index 00000000000..e8e02fcdab0 --- /dev/null +++ b/pico/processor/src/main/java/io/helidon/pico/processor/spi/ProcessingEvent.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.pico.processor.spi; + +import java.util.Optional; +import java.util.Set; + +import javax.annotation.processing.ProcessingEnvironment; + +import io.helidon.builder.Builder; +import io.helidon.common.types.TypedElementInfo; + +/** + * Attributes that can be observed via {@link PicoAnnotationProcessorObserver}. + */ +@Builder +public interface ProcessingEvent { + + /** + * Optionally, the active {@link ProcessingEnvironment} if it is available. + * + * @return the processing environment if it is available + */ + Optional processingEnvironment(); + + /** + * The {@link jakarta.inject.Inject}'able type elements, and possibly any other elements that are found to be of interest for + * processing. The set of processed elements are subject to change in the future. The implementor is therefore encouraged + * to not make assumptions about the set of elements that are in this set. + * + * @return the set of injectable elements, and any other elements of interest to the pico APT + */ + Set elementsOfInterest(); + +} diff --git a/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/package-info.java b/pico/processor/src/main/java/io/helidon/pico/processor/spi/package-info.java similarity index 83% rename from examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/package-info.java rename to pico/processor/src/main/java/io/helidon/pico/processor/spi/package-info.java index d78d418a922..228ed0c025c 100644 --- a/examples/integrations/neo4j/neo4j-mp/src/main/java/io/helidon/examples/integrations/neo4j/mp/package-info.java +++ b/pico/processor/src/main/java/io/helidon/pico/processor/spi/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,6 @@ */ /** - * Resources. + * Pico APT SPI package. */ -package io.helidon.examples.integrations.neo4j.mp; +package io.helidon.pico.processor.spi; diff --git a/pico/processor/src/main/java/module-info.java b/pico/processor/src/main/java/module-info.java index b9fd9f2393f..87166aaa396 100644 --- a/pico/processor/src/main/java/module-info.java +++ b/pico/processor/src/main/java/module-info.java @@ -30,11 +30,13 @@ requires io.helidon.builder.processor; exports io.helidon.pico.processor; + exports io.helidon.pico.processor.spi; uses io.helidon.builder.processor.BuilderProcessor; + uses io.helidon.builder.processor.spi.TypeInfoCreatorProvider; + uses io.helidon.pico.processor.spi.PicoAnnotationProcessorObserver; uses io.helidon.pico.tools.spi.InterceptorCreator; uses io.helidon.pico.tools.spi.CustomAnnotationTemplateCreator; - uses io.helidon.builder.processor.spi.TypeInfoCreatorProvider; provides javax.annotation.processing.Processor with io.helidon.pico.processor.CustomAnnotationProcessor, diff --git a/pico/processor/src/test/java/io/helidon/pico/processor/CustomAnnotationProcessorTest.java b/pico/processor/src/test/java/io/helidon/pico/processor/CustomAnnotationProcessorTest.java index e8e61db4413..2e10f697b41 100644 --- a/pico/processor/src/test/java/io/helidon/pico/processor/CustomAnnotationProcessorTest.java +++ b/pico/processor/src/test/java/io/helidon/pico/processor/CustomAnnotationProcessorTest.java @@ -25,9 +25,8 @@ import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeInfoDefault; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementName; -import io.helidon.common.types.TypedElementNameDefault; -import io.helidon.pico.api.ElementInfo; +import io.helidon.common.types.TypedElementInfo; +import io.helidon.common.types.TypedElementInfoDefault; import io.helidon.pico.api.ServiceInfoBasics; import io.helidon.pico.api.ServiceInfoDefault; import io.helidon.pico.processor.testsubjects.BasicEndpoint; @@ -68,12 +67,12 @@ void extensibleGET() { .typeName(create(BasicEndpoint.class)) .annotations(annotations) .build(); - TypedElementName target = TypedElementNameDefault.builder() + TypedElementInfo target = TypedElementInfoDefault.builder() .typeName(create(String.class)) .elementKind(ElementKind.METHOD.name()) .elementName("itWorks") .build(); - TypedElementName arg1 = TypedElementNameDefault.builder() + TypedElementInfo arg1 = TypedElementInfoDefault.builder() .typeName(create(String.class)) .elementName("header") .build(); @@ -84,8 +83,6 @@ void extensibleGET() { .annoTypeName(create(ExtensibleGET.class)) .serviceInfo(serviceInfo) .targetElement(target) - .targetElementArgs(List.of(arg1)) - .targetElementAccess(ElementInfo.Access.PUBLIC) .enclosingTypeInfo(enclosingTypeInfo) .genericTemplateCreator(genericTemplateCreator) .build(); diff --git a/pico/processor/src/test/java/io/helidon/pico/processor/ProcessingTrackerTest.java b/pico/processor/src/test/java/io/helidon/pico/processor/ProcessingTrackerTest.java new file mode 100644 index 00000000000..c06928094eb --- /dev/null +++ b/pico/processor/src/test/java/io/helidon/pico/processor/ProcessingTrackerTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.pico.processor; + +import java.util.List; + +import javax.lang.model.element.TypeElement; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.mockito.Mockito.mock; + +class ProcessingTrackerTest { + + @Test + void noDelta() { + List typeNames = List.of("a", "b", "c"); + ProcessingTracker tracker = new ProcessingTracker(null, typeNames, + typeName -> mock(TypeElement.class)); + assertThat(tracker.removedTypeNames().size(), + is(0)); + assertThat(tracker.remainingTypeNames(), + containsInAnyOrder("a", "b", "c")); + } + + @Test + void incrementalCompilation() { + List typeNames = List.of("a", "b", "c"); + ProcessingTracker tracker = new ProcessingTracker(null, typeNames, + typeName -> mock(TypeElement.class)); + tracker.processing("b"); + + assertThat(tracker.removedTypeNames().size(), + is(0)); + assertThat(tracker.remainingTypeNames(), + containsInAnyOrder("a", "b", "c")); + } + + @Test + void incrementalCompilationWithFilesRemoved() { + List typeNames = List.of("a", "b", "c"); + ProcessingTracker tracker = new ProcessingTracker(null, typeNames, + typeName -> (typeName.equals("b") ? null : mock(TypeElement.class))); + + assertThat(tracker.removedTypeNames().size(), + is(1)); + assertThat(tracker.remainingTypeNames(), + containsInAnyOrder("a", "c")); + } + + @Test + void incrementalCompilationWithFilesAddedAndRemoved() { + List typeNames = List.of("a"); + ProcessingTracker tracker = new ProcessingTracker(null, typeNames, + typeName -> mock(TypeElement.class)); + tracker.processing("b"); + tracker.processing("a"); + + assertThat(tracker.removedTypeNames().size(), + is(0)); + assertThat(tracker.remainingTypeNames(), + containsInAnyOrder("a", "b")); + } + + @Test + void cleanCompilation() { + List typeNames = List.of(); + ProcessingTracker tracker = new ProcessingTracker(null, typeNames, + typeName -> mock(TypeElement.class)); + tracker.processing("a"); + tracker.processing("b"); + tracker.processing("c"); + + assertThat(tracker.removedTypeNames().size(), + is(0)); + assertThat(tracker.remainingTypeNames(), + containsInAnyOrder("a", "b", "c")); + } + + @Test + void fullCompilationWithFilesAdded() { + List typeNames = List.of("a"); + ProcessingTracker tracker = new ProcessingTracker(null, typeNames, + typeName -> mock(TypeElement.class)); + tracker.processing("a"); + tracker.processing("b"); + tracker.processing("c"); + + assertThat(tracker.removedTypeNames().size(), + is(0)); + assertThat(tracker.remainingTypeNames(), + containsInAnyOrder("a", "b", "c")); + } + +} diff --git a/pico/runtime/src/main/java/io/helidon/pico/runtime/AbstractServiceProvider.java b/pico/runtime/src/main/java/io/helidon/pico/runtime/AbstractServiceProvider.java index e8ace90d5cc..12c3cd748a1 100644 --- a/pico/runtime/src/main/java/io/helidon/pico/runtime/AbstractServiceProvider.java +++ b/pico/runtime/src/main/java/io/helidon/pico/runtime/AbstractServiceProvider.java @@ -68,6 +68,8 @@ import jakarta.inject.Provider; +import static io.helidon.pico.api.PicoServices.createActivationRequestDefault; +import static io.helidon.pico.api.PicoServices.isDebugEnabled; import static io.helidon.pico.runtime.ServiceUtils.isQualifiedInjectionTarget; /** @@ -199,13 +201,17 @@ public Phase currentActivationPhase() { return phase; } - /** - * Used to access the current pico services instance assigned to this service provider. - * - * @return the pico services assigned to this service provider - */ - public PicoServices picoServices() { - return Objects.requireNonNull(picoServices, description() + ": picoServices should have been previously set"); + @Override + public Optional picoServices() { + return Optional.ofNullable(picoServices); + } + + PicoServices requiredPicoServices() { + PicoServices picoServices = picoServices().orElse(null); + if (picoServices == null) { + throw new PicoServiceProviderException(description() + ": picoServices should have been previously set", this); + } + return picoServices; } @Override @@ -321,6 +327,12 @@ public String name(boolean simple) { return (simple) ? TypeNameDefault.createFromTypeName(name).className() : name; } + @Override + public T get() { + return first(PicoServices.SERVICE_QUERY_REQUIRED) + .orElseThrow(() -> new PicoServiceProviderException("Expected to find a match", this)); + } + @SuppressWarnings("unchecked") @Override public Optional first(ContextualServiceQuery ctx) { @@ -341,6 +353,10 @@ public Optional first(ContextualServiceQuery ctx) { instance = NonSingletonServiceProvider.createAndActivate(this); } + if (ctx.expected() && instance == null) { + throw new PicoServiceProviderException("Expected to find a match: " + ctx, this); + } + return Optional.ofNullable(instance); } } catch (InjectionException ie) { @@ -527,9 +543,8 @@ public ServiceInjectionPlanBinder.Binder resolvedBind(String id, InjectionPointInfo ipInfo = InjectionPointInfoDefault.builder() .id(id) .dependencyToServiceInfo(serviceInfo); - // .build(); Object resolved = Objects.requireNonNull( - resolver.resolve(ipInfo, picoServices(), AbstractServiceProvider.this, false)); + resolver.resolve(ipInfo, requiredPicoServices(), AbstractServiceProvider.this, false)); PicoInjectionPlan plan = createBuilder(id) .unqualifiedProviders(List.of(resolved)) .resolved(false) @@ -600,8 +615,8 @@ public Map getOrCreateInjectionPlan(boolean resolveIp dependencies(dependencies()); } - Map plan = - DefaultInjectionPlans.createInjectionPlans(picoServices(), this, dependencies, resolveIps, logger()); + Map plan = DefaultInjectionPlans + .createInjectionPlans(requiredPicoServices(), this, dependencies, resolveIps, logger()); assert (this.injectionPlan == null); this.injectionPlan = Objects.requireNonNull(plan); @@ -617,7 +632,7 @@ public boolean reset(boolean deep) { didAcquire = activationSemaphore.tryAcquire(1, TimeUnit.MILLISECONDS); if (service != null) { - System.Logger.Level level = (PicoServices.isDebugEnabled()) + System.Logger.Level level = (isDebugEnabled()) ? System.Logger.Level.INFO : System.Logger.Level.DEBUG; logger().log(level, "Resetting " + this); if (deep && service instanceof Resettable) { @@ -671,7 +686,7 @@ public ActivationResult deactivate(DeActivationRequest req) { return ActivationResult.createSuccess(this); } - PicoServices picoServices = picoServices(); + PicoServices picoServices = requiredPicoServices(); PicoServicesConfig cfg = picoServices.config(); // if we are here then we are not yet at the ultimate target phase, and we either have to activate or deactivate @@ -849,7 +864,7 @@ protected Optional maybeActivate(ContextualServiceQuery ctx) { if (serviceOrProvider == null || Phase.ACTIVE != currentActivationPhase()) { - ActivationRequest req = PicoServices.createActivationRequestDefault(); + ActivationRequest req = createActivationRequestDefault(); ActivationResult res = activate(req); if (res.failure()) { if (ctx.expected()) { @@ -1100,7 +1115,7 @@ void onFailedFinish(LogEntryAndResult logEntryAndResult, ActivationLogEntryDefault.Builder res = logEntryAndResult.logEntry; Throwable prev = res.error().orElse(null); if (prev == null || !(t instanceof InjectionException)) { - String msg = (t != null && t.getMessage() != null) ? t.getMessage() : "failed to complete operation"; + String msg = (t != null && t.getMessage() != null) ? t.getMessage() : "Failed to complete operation"; e = new InjectionException(msg, t, this); log.ifPresent(e::activationLog); } else { diff --git a/pico/runtime/src/main/java/io/helidon/pico/runtime/BoundedServiceProvider.java b/pico/runtime/src/main/java/io/helidon/pico/runtime/BoundedServiceProvider.java index f65ea31e201..7854afc8c00 100644 --- a/pico/runtime/src/main/java/io/helidon/pico/runtime/BoundedServiceProvider.java +++ b/pico/runtime/src/main/java/io/helidon/pico/runtime/BoundedServiceProvider.java @@ -53,7 +53,7 @@ private BoundedServiceProvider(ServiceProvider binding, ContextualServiceQuery query = ContextualServiceQueryDefault.builder() .injectionPointInfo(ipInfoCtx) .serviceInfoCriteria(ipInfoCtx.dependencyToServiceInfo()) - .expected(true).build(); + .expected(false).build(); this.instance = LazyValue.create(() -> binding.first(query).orElse(null)); this.instances = LazyValue.create(() -> binding.list(query)); } diff --git a/pico/runtime/src/main/java/io/helidon/pico/runtime/DefaultInjectionPlans.java b/pico/runtime/src/main/java/io/helidon/pico/runtime/DefaultInjectionPlans.java index 32b05cd30b0..bd72d738b6f 100644 --- a/pico/runtime/src/main/java/io/helidon/pico/runtime/DefaultInjectionPlans.java +++ b/pico/runtime/src/main/java/io/helidon/pico/runtime/DefaultInjectionPlans.java @@ -428,7 +428,7 @@ private static List toEligibleInjectionRefs(InjectionPointInfo ipInfo, private static InjectionException expectedToResolveCriteria(InjectionPointInfo ipInfo, Throwable cause, ServiceProvider self) { - String msg = (cause == null) ? "expected" : "failed"; + String msg = (cause == null) ? "Expected" : "Failed"; return new InjectionException(msg + " to resolve a service instance appropriate for '" + ipInfo.serviceTypeName() + "." + ipInfo.elementName() + "' with criteria = '" + ipInfo.dependencyToServiceInfo(), diff --git a/pico/runtime/src/main/java/io/helidon/pico/runtime/DefaultPicoServices.java b/pico/runtime/src/main/java/io/helidon/pico/runtime/DefaultPicoServices.java index 299d632e1fa..d856d8f1af8 100644 --- a/pico/runtime/src/main/java/io/helidon/pico/runtime/DefaultPicoServices.java +++ b/pico/runtime/src/main/java/io/helidon/pico/runtime/DefaultPicoServices.java @@ -396,13 +396,13 @@ private List findModules(boolean load) { private void bindApplications(DefaultServices services, Collection apps) { if (!cfg.usesCompileTimeApplications()) { - LOGGER.log(System.Logger.Level.DEBUG, "application binding is disabled"); + LOGGER.log(System.Logger.Level.DEBUG, "Application binding is disabled"); return; } if (apps.size() > 1) { - LOGGER.log(System.Logger.Level.WARNING, - "there is typically only 1 application instance; app instances = " + apps); + LOGGER.log(System.Logger.Level.INFO, + "There is typically only 1 application instance; app instances = " + apps); } else if (apps.isEmpty()) { LOGGER.log(System.Logger.Level.TRACE, "no " + Application.class.getName() + " was found."); return; @@ -415,12 +415,12 @@ private void bindApplications(DefaultServices services, private void bindModules(DefaultServices services, Collection modules) { if (!cfg.usesCompileTimeModules()) { - LOGGER.log(System.Logger.Level.DEBUG, "module binding is disabled"); + LOGGER.log(System.Logger.Level.DEBUG, "Module binding is disabled"); return; } if (modules.isEmpty()) { - LOGGER.log(System.Logger.Level.WARNING, "no " + ModuleComponent.class.getName() + " was found."); + LOGGER.log(System.Logger.Level.WARNING, "No " + ModuleComponent.class.getName() + " was found."); } else { modules.forEach(module -> services.bind(this, module, isBinding.get())); } diff --git a/pico/runtime/src/main/java/io/helidon/pico/runtime/InterceptedMethod.java b/pico/runtime/src/main/java/io/helidon/pico/runtime/InterceptedMethod.java index adbdcc17ce1..f26a23ef102 100644 --- a/pico/runtime/src/main/java/io/helidon/pico/runtime/InterceptedMethod.java +++ b/pico/runtime/src/main/java/io/helidon/pico/runtime/InterceptedMethod.java @@ -22,7 +22,7 @@ import io.helidon.common.types.AnnotationAndValue; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; import io.helidon.pico.api.Interceptor; import io.helidon.pico.api.InvocationContext; import io.helidon.pico.api.InvocationContextDefault; @@ -58,8 +58,8 @@ protected InterceptedMethod(I interceptedImpl, TypeName serviceTypeName, Collection serviceLevelAnnotations, Collection> interceptors, - TypedElementName methodInfo, - TypedElementName[] methodArgInfo) { + TypedElementInfo methodInfo, + TypedElementInfo[] methodArgInfo) { this.impl = Objects.requireNonNull(interceptedImpl); this.ctx = InvocationContextDefault.builder() .serviceProvider(serviceProvider) @@ -86,7 +86,7 @@ protected InterceptedMethod(I interceptedImpl, TypeName serviceTypeName, Collection serviceLevelAnnotations, Collection> interceptors, - TypedElementName methodInfo) { + TypedElementInfo methodInfo) { this.impl = Objects.requireNonNull(interceptedImpl); this.ctx = InvocationContextDefault.builder() .serviceProvider(serviceProvider) diff --git a/pico/runtime/src/main/java/io/helidon/pico/runtime/NonSingletonServiceProvider.java b/pico/runtime/src/main/java/io/helidon/pico/runtime/NonSingletonServiceProvider.java index dfc047b568e..39b3002ffe1 100644 --- a/pico/runtime/src/main/java/io/helidon/pico/runtime/NonSingletonServiceProvider.java +++ b/pico/runtime/src/main/java/io/helidon/pico/runtime/NonSingletonServiceProvider.java @@ -20,7 +20,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import io.helidon.pico.api.Phase; @@ -35,7 +34,7 @@ class NonSingletonServiceProvider extends AbstractServiceProvider { private NonSingletonServiceProvider(AbstractServiceProvider delegate) { this.delegate = delegate; - picoServices(Optional.of(delegate.picoServices())); + picoServices(delegate.picoServices()); serviceInfo(delegate.serviceInfo()); dependencies(delegate.dependencies()); } diff --git a/pico/runtime/src/main/java/io/helidon/pico/runtime/ServiceBinderDefault.java b/pico/runtime/src/main/java/io/helidon/pico/runtime/ServiceBinderDefault.java index 4c35ff411af..d83ca26acb7 100644 --- a/pico/runtime/src/main/java/io/helidon/pico/runtime/ServiceBinderDefault.java +++ b/pico/runtime/src/main/java/io/helidon/pico/runtime/ServiceBinderDefault.java @@ -67,6 +67,9 @@ public void bind(ServiceProvider sp) { } Optional> bindableSp = toBindableProvider(sp); + if (bindableSp.isPresent() && alreadyBoundToThisPicoServices(bindableSp.get(), picoServices)) { + return; + } if (moduleName != null) { bindableSp.ifPresent(it -> it.moduleName(moduleName)); @@ -85,6 +88,12 @@ public void bind(ServiceProvider sp) { bindableSp.ifPresent(it -> it.picoServices(Optional.of(picoServices))); } + private boolean alreadyBoundToThisPicoServices(ServiceProviderBindable serviceProvider, + PicoServices picoServices) { + PicoServices assigned = serviceProvider.picoServices().orElse(null); + return (assigned == picoServices); + } + /** * Returns the bindable service provider for what is passed if available. * diff --git a/pico/runtime/src/test/java/io/helidon/pico/runtime/InvocationTest.java b/pico/runtime/src/test/java/io/helidon/pico/runtime/InvocationTest.java index 15d82ac3642..4fb047ebb79 100644 --- a/pico/runtime/src/test/java/io/helidon/pico/runtime/InvocationTest.java +++ b/pico/runtime/src/test/java/io/helidon/pico/runtime/InvocationTest.java @@ -22,7 +22,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; -import io.helidon.common.types.TypedElementNameDefault; +import io.helidon.common.types.TypedElementInfoDefault; import io.helidon.pico.api.Interceptor; import io.helidon.pico.api.InvocationContext; import io.helidon.pico.api.InvocationContextDefault; @@ -42,7 +42,7 @@ class InvocationTest { TestInterceptor first = new TestInterceptor("first"); TestInterceptor second = new TestInterceptor("second"); InvocationContext dummyCtx = InvocationContextDefault.builder() - .elementInfo(TypedElementNameDefault.builder().elementName("test").typeName(InvocationTest.class).build()) + .elementInfo(TypedElementInfoDefault.builder().elementName("test").typeName(InvocationTest.class).build()) .interceptors(List.of(first.provider, second.provider)); ArrayList calls = new ArrayList<>(); @@ -63,7 +63,7 @@ void normalCaseWithInterceptors() { @Test void normalCaseWithNoInterceptors() { InvocationContext dummyCtx = InvocationContextDefault.builder() - .elementInfo(TypedElementNameDefault.builder().elementName("test").typeName(InvocationTest.class).build()) + .elementInfo(TypedElementInfoDefault.builder().elementName("test").typeName(InvocationTest.class).build()) .interceptors(List.of()); Object[] args = new Object[] {}; diff --git a/pico/tests/interception/src/main/java/io/helidon/pico/tests/interception/TheOtherService.java b/pico/tests/interception/src/main/java/io/helidon/pico/tests/interception/TheOtherService.java index 3690106f14c..f9863ab8f33 100644 --- a/pico/tests/interception/src/main/java/io/helidon/pico/tests/interception/TheOtherService.java +++ b/pico/tests/interception/src/main/java/io/helidon/pico/tests/interception/TheOtherService.java @@ -19,7 +19,7 @@ import jakarta.inject.Singleton; @Singleton -class TheOtherService implements OtherContract{ +class TheOtherService implements OtherContract { private boolean throwException; @Modify diff --git a/pico/tests/resources-pico/pom.xml b/pico/tests/resources-pico/pom.xml index f2234d1b4a6..7f7d185cf5a 100644 --- a/pico/tests/resources-pico/pom.xml +++ b/pico/tests/resources-pico/pom.xml @@ -94,7 +94,7 @@ -Apico.autoAddNonContractInterfaces=true -Apico.allowListedInterceptorAnnotations=io.helidon.pico.tests.pico.interceptor.TestNamed - -Apico.application.pre.create=true + -Apico.application.pre.create=false -Apico.mapApplicationToSingletonScope=true -Apico.debug=${pico.debug} @@ -145,10 +145,7 @@ -Apico.debug=${pico.debug} -Apico.autoAddNonContractInterfaces=true - -Apico.application.pre.create=true - - - + -Apico.application.pre.create=false NAMED diff --git a/pico/tests/resources-pico/src/main/java/module-info.java b/pico/tests/resources-pico/src/main/java/module-info.java index 96a9a0ac8e8..714aeae2c47 100644 --- a/pico/tests/resources-pico/src/main/java/module-info.java +++ b/pico/tests/resources-pico/src/main/java/module-info.java @@ -33,5 +33,4 @@ exports io.helidon.pico.tests.pico.tbox; provides io.helidon.pico.api.ModuleComponent with io.helidon.pico.tests.pico.Pico$$Module; - provides io.helidon.pico.api.Application with io.helidon.pico.tests.pico.Pico$$Application; } diff --git a/pico/tests/resources-pico/src/test/resources/expected/module-info.java._pico_ b/pico/tests/resources-pico/src/test/resources/expected/module-info.java._pico_ index 32608ecea9b..b156cd28f92 100644 --- a/pico/tests/resources-pico/src/test/resources/expected/module-info.java._pico_ +++ b/pico/tests/resources-pico/src/test/resources/expected/module-info.java._pico_ @@ -33,7 +33,6 @@ module io.helidon.pico.tests.pico { exports io.helidon.pico.tests.pico.tbox; provides io.helidon.pico.api.ModuleComponent with io.helidon.pico.tests.pico.Pico$$Module; - provides io.helidon.pico.api.Application with io.helidon.pico.tests.pico.Pico$$Application; // pico external contract usage - Generated(value = "io.helidon.pico.tools.ActivatorCreatorDefault", comments = "version=1") requires test1; requires test2; @@ -44,4 +43,6 @@ module io.helidon.pico.tests.pico { uses io.helidon.pico.api.OptionallyNamed; // pico contract usage - Generated(value = "io.helidon.pico.tools.ActivatorCreatorDefault", comments = "version=1") exports io.helidon.pico.tests.pico.provider; + // pico application - Generated(value = "io.helidon.pico.tools.ApplicationCreatorDefault", comments = "version=1") + provides io.helidon.pico.api.Application with io.helidon.pico.tests.pico.Pico$$Application; } diff --git a/pico/tests/resources-pico/src/test/resources/expected/tests-module-info.java._pico_ b/pico/tests/resources-pico/src/test/resources/expected/tests-module-info.java._pico_ index 6aef3a52f72..52b16aea6cc 100644 --- a/pico/tests/resources-pico/src/test/resources/expected/tests-module-info.java._pico_ +++ b/pico/tests/resources-pico/src/test/resources/expected/tests-module-info.java._pico_ @@ -3,12 +3,12 @@ module io.helidon.pico.tests.pico/test { exports io.helidon.pico.tests.pico; // pico module - Generated(value = "io.helidon.pico.tools.ActivatorCreatorDefault", comments = "version=1") provides io.helidon.pico.api.ModuleComponent with io.helidon.pico.tests.pico.Pico$$TestModule; - // pico application - Generated(value = "io.helidon.pico.tools.ActivatorCreatorDefault", comments = "version=1") - provides io.helidon.pico.api.Application with io.helidon.pico.tests.pico.Pico$$TestApplication; // pico external contract usage - Generated(value = "io.helidon.pico.tools.ActivatorCreatorDefault", comments = "version=1") uses io.helidon.pico.api.Resettable; uses io.helidon.pico.tests.pico.stacking.Intercepted; uses io.helidon.pico.tests.pico.stacking.InterceptedImpl; // pico services - Generated(value = "io.helidon.pico.tools.ActivatorCreatorDefault", comments = "version=1") requires transitive io.helidon.pico.runtime; + // pico application - Generated(value = "io.helidon.pico.tools.ApplicationCreatorDefault", comments = "version=1") + provides io.helidon.pico.api.Application with io.helidon.pico.tests.pico.Pico$$TestApplication; } diff --git a/pico/tests/resources-pico/src/test/resources/expected/ximpl-interceptor._java_ b/pico/tests/resources-pico/src/test/resources/expected/ximpl-interceptor._java_ index 6cf4ce06cda..cb04c127469 100644 --- a/pico/tests/resources-pico/src/test/resources/expected/ximpl-interceptor._java_ +++ b/pico/tests/resources-pico/src/test/resources/expected/ximpl-interceptor._java_ @@ -10,9 +10,9 @@ import java.util.function.Function; import io.helidon.common.types.AnnotationAndValue; import io.helidon.common.types.AnnotationAndValueDefault; import io.helidon.common.types.TypeNameDefault; -import io.helidon.common.types.TypedElementNameDefault; +import io.helidon.common.types.TypedElementInfoDefault; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; import io.helidon.pico.api.ClassNamed; import io.helidon.pico.api.InvocationContextDefault; import io.helidon.pico.api.Interceptor; @@ -46,7 +46,7 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce AnnotationAndValueDefault.create(io.helidon.pico.api.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable")), AnnotationAndValueDefault.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))); - private static final TypedElementName __ctor = TypedElementNameDefault.builder() + private static final TypedElementInfo __ctor = TypedElementInfoDefault.builder() .typeName(create(void.class)) .elementName(io.helidon.pico.api.ElementInfo.CONSTRUCTOR) .addAnnotation(AnnotationAndValueDefault.create(io.helidon.pico.api.ClassNamed.class, Map.of("value", "io.helidon.pico.tests.pico.ClassNamedX"))) @@ -56,7 +56,7 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce .addAnnotation(AnnotationAndValueDefault.create(jakarta.inject.Singleton.class)) .addAnnotation(AnnotationAndValueDefault.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); - private static final TypedElementName __methodIA1 = TypedElementNameDefault.builder() + private static final TypedElementInfo __methodIA1 = TypedElementInfoDefault.builder() .typeName(create(void.class)) .elementName("methodIA1") .addAnnotation(AnnotationAndValueDefault.create(io.helidon.pico.api.ClassNamed.class, Map.of("value", "io.helidon.pico.tests.pico.ClassNamedX"))) @@ -66,7 +66,7 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce .addAnnotation(AnnotationAndValueDefault.create(java.lang.Override.class)) .addAnnotation(AnnotationAndValueDefault.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); - private static final TypedElementName __methodIA2 = TypedElementNameDefault.builder() + private static final TypedElementInfo __methodIA2 = TypedElementInfoDefault.builder() .typeName(create(void.class)) .elementName("methodIA2") .addAnnotation(AnnotationAndValueDefault.create(io.helidon.pico.api.ClassNamed.class, Map.of("value", "io.helidon.pico.tests.pico.ClassNamedX"))) @@ -77,7 +77,7 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce .addAnnotation(AnnotationAndValueDefault.create(java.lang.Override.class)) .addAnnotation(AnnotationAndValueDefault.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); - private static final TypedElementName __methodIB = TypedElementNameDefault.builder() + private static final TypedElementInfo __methodIB = TypedElementInfoDefault.builder() .typeName(create(void.class)) .elementName("methodIB") .addAnnotation(AnnotationAndValueDefault.create(io.helidon.pico.api.ClassNamed.class, Map.of("value", "io.helidon.pico.tests.pico.ClassNamedX"))) @@ -89,12 +89,12 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce .addAnnotation(AnnotationAndValueDefault.create(java.lang.Override.class)) .addAnnotation(AnnotationAndValueDefault.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); - private static final TypedElementName __methodIB__p1 = TypedElementNameDefault.builder() + private static final TypedElementInfo __methodIB__p1 = TypedElementInfoDefault.builder() .typeName(create(java.lang.String.class)) .elementName("p1") .addAnnotation(AnnotationAndValueDefault.create(jakarta.inject.Named.class, Map.of("value", "arg1"))) .build(); - private static final TypedElementName __methodIB2 = TypedElementNameDefault.builder() + private static final TypedElementInfo __methodIB2 = TypedElementInfoDefault.builder() .typeName(create(java.lang.String.class)) .elementName("methodIB2") .addAnnotation(AnnotationAndValueDefault.create(io.helidon.pico.api.ClassNamed.class, Map.of("value", "io.helidon.pico.tests.pico.ClassNamedX"))) @@ -106,12 +106,12 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce .addAnnotation(AnnotationAndValueDefault.create(java.lang.Override.class)) .addAnnotation(AnnotationAndValueDefault.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); - private static final TypedElementName __methodIB2__p1 = TypedElementNameDefault.builder() + private static final TypedElementInfo __methodIB2__p1 = TypedElementInfoDefault.builder() .typeName(create(java.lang.String.class)) .elementName("p1") .addAnnotation(AnnotationAndValueDefault.create(jakarta.inject.Named.class, Map.of("value", "arg1"))) .build(); - private static final TypedElementName __close = TypedElementNameDefault.builder() + private static final TypedElementInfo __close = TypedElementInfoDefault.builder() .typeName(create(void.class)) .elementName("close") .addAnnotation(AnnotationAndValueDefault.create(io.helidon.pico.api.ClassNamed.class, Map.of("value", "io.helidon.pico.tests.pico.ClassNamedX"))) @@ -122,7 +122,7 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce .addAnnotation(AnnotationAndValueDefault.create(java.lang.Override.class)) .addAnnotation(AnnotationAndValueDefault.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); - private static final TypedElementName __methodX = TypedElementNameDefault.builder() + private static final TypedElementInfo __methodX = TypedElementInfoDefault.builder() .typeName(create(long.class)) .elementName("methodX") .addAnnotation(AnnotationAndValueDefault.create(io.helidon.pico.api.ClassNamed.class, Map.of("value", "io.helidon.pico.tests.pico.ClassNamedX"))) @@ -131,19 +131,19 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce .addAnnotation(AnnotationAndValueDefault.create(jakarta.inject.Singleton.class)) .addAnnotation(AnnotationAndValueDefault.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); - private static final TypedElementName __methodX__p1 = TypedElementNameDefault.builder() + private static final TypedElementInfo __methodX__p1 = TypedElementInfoDefault.builder() .typeName(create(java.lang.String.class)) .elementName("p1") .build(); - private static final TypedElementName __methodX__p2 = TypedElementNameDefault.builder() + private static final TypedElementInfo __methodX__p2 = TypedElementInfoDefault.builder() .typeName(create(int.class)) .elementName("p2") .build(); - private static final TypedElementName __methodX__p3 = TypedElementNameDefault.builder() + private static final TypedElementInfo __methodX__p3 = TypedElementInfoDefault.builder() .typeName(create(boolean.class)) .elementName("p3") .build(); - private static final TypedElementName __methodY = TypedElementNameDefault.builder() + private static final TypedElementInfo __methodY = TypedElementInfoDefault.builder() .typeName(create(java.lang.String.class)) .elementName("methodY") .addAnnotation(AnnotationAndValueDefault.create(io.helidon.pico.api.ClassNamed.class, Map.of("value", "io.helidon.pico.tests.pico.ClassNamedX"))) @@ -152,7 +152,7 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce .addAnnotation(AnnotationAndValueDefault.create(jakarta.inject.Singleton.class)) .addAnnotation(AnnotationAndValueDefault.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); - private static final TypedElementName __methodZ = TypedElementNameDefault.builder() + private static final TypedElementInfo __methodZ = TypedElementInfoDefault.builder() .typeName(create(java.lang.String.class)) .elementName("methodZ") .addAnnotation(AnnotationAndValueDefault.create(io.helidon.pico.api.ClassNamed.class, Map.of("value", "io.helidon.pico.tests.pico.ClassNamedX"))) @@ -161,7 +161,7 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce .addAnnotation(AnnotationAndValueDefault.create(jakarta.inject.Singleton.class)) .addAnnotation(AnnotationAndValueDefault.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); - private static final TypedElementName __throwRuntimeException = TypedElementNameDefault.builder() + private static final TypedElementInfo __throwRuntimeException = TypedElementInfoDefault.builder() .typeName(create(void.class)) .elementName("throwRuntimeException") .addAnnotation(AnnotationAndValueDefault.create(io.helidon.pico.api.ClassNamed.class, Map.of("value", "io.helidon.pico.tests.pico.ClassNamedX"))) @@ -247,7 +247,7 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce this.__methodIB__call = new InterceptedMethod( __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __methodIB__interceptors, __methodIB, - new TypedElementName[] {__methodIB__p1}) { + new TypedElementInfo[] {__methodIB__p1}) { @Override public java.lang.Void invoke(Object... args) throws Throwable { impl().methodIB((java.lang.String) args[0]); @@ -257,7 +257,7 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce this.__methodIB2__call = new InterceptedMethod( __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __methodIB2__interceptors, __methodIB2, - new TypedElementName[] {__methodIB2__p1}) { + new TypedElementInfo[] {__methodIB2__p1}) { @Override public java.lang.String invoke(Object... args) throws Throwable { return impl().methodIB2((java.lang.String) args[0]); @@ -275,7 +275,7 @@ public class XImpl$$Pico$$Interceptor extends io.helidon.pico.tests.pico.interce this.__methodX__call = new InterceptedMethod( __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __methodX__interceptors, __methodX, - new TypedElementName[] {__methodX__p1, __methodX__p2, __methodX__p3}) { + new TypedElementInfo[] {__methodX__p1, __methodX__p2, __methodX__p3}) { @Override public java.lang.Long invoke(Object... args) throws Throwable { return impl().methodX((java.lang.String) args[0], (java.lang.Integer) args[1], (java.lang.Boolean) args[2]); diff --git a/pico/tests/resources-pico/src/test/resources/expected/yimpl-interceptor._java_ b/pico/tests/resources-pico/src/test/resources/expected/yimpl-interceptor._java_ index 3dfcb9a065e..679ecab1eda 100644 --- a/pico/tests/resources-pico/src/test/resources/expected/yimpl-interceptor._java_ +++ b/pico/tests/resources-pico/src/test/resources/expected/yimpl-interceptor._java_ @@ -10,9 +10,9 @@ import java.util.function.Function; import io.helidon.common.types.AnnotationAndValue; import io.helidon.common.types.AnnotationAndValueDefault; import io.helidon.common.types.TypeNameDefault; -import io.helidon.common.types.TypedElementNameDefault; +import io.helidon.common.types.TypedElementInfoDefault; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; import io.helidon.pico.api.InvocationContextDefault; import io.helidon.pico.api.Interceptor; import io.helidon.pico.api.InvocationException; @@ -44,7 +44,7 @@ public class YImpl$$Pico$$Interceptor /* extends io.helidon.pico.tests.pico.inte AnnotationAndValueDefault.create(io.helidon.pico.api.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable")), AnnotationAndValueDefault.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))); - private static final TypedElementName __ctor = TypedElementNameDefault.builder() + private static final TypedElementInfo __ctor = TypedElementInfoDefault.builder() .typeName(create(void.class)) .elementName(io.helidon.pico.api.ElementInfo.CONSTRUCTOR) .addAnnotation(AnnotationAndValueDefault.create(io.helidon.pico.api.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) @@ -53,7 +53,7 @@ public class YImpl$$Pico$$Interceptor /* extends io.helidon.pico.tests.pico.inte .addAnnotation(AnnotationAndValueDefault.create(jakarta.inject.Singleton.class)) .addAnnotation(AnnotationAndValueDefault.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); - private static final TypedElementName __methodIB = TypedElementNameDefault.builder() + private static final TypedElementInfo __methodIB = TypedElementInfoDefault.builder() .typeName(create(void.class)) .elementName("methodIB") .addAnnotation(AnnotationAndValueDefault.create(io.helidon.pico.api.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) @@ -63,12 +63,12 @@ public class YImpl$$Pico$$Interceptor /* extends io.helidon.pico.tests.pico.inte .addAnnotation(AnnotationAndValueDefault.create(java.lang.Override.class)) .addAnnotation(AnnotationAndValueDefault.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); - private static final TypedElementName __methodIB__p1 = TypedElementNameDefault.builder() + private static final TypedElementInfo __methodIB__p1 = TypedElementInfoDefault.builder() .typeName(create(java.lang.String.class)) .elementName("p1") .addAnnotation(AnnotationAndValueDefault.create(jakarta.inject.Named.class, Map.of("value", "arg1"))) .build(); - private static final TypedElementName __methodIB2 = TypedElementNameDefault.builder() + private static final TypedElementInfo __methodIB2 = TypedElementInfoDefault.builder() .typeName(create(java.lang.String.class)) .elementName("methodIB2") .addAnnotation(AnnotationAndValueDefault.create(io.helidon.pico.api.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) @@ -78,12 +78,12 @@ public class YImpl$$Pico$$Interceptor /* extends io.helidon.pico.tests.pico.inte .addAnnotation(AnnotationAndValueDefault.create(java.lang.Override.class)) .addAnnotation(AnnotationAndValueDefault.create(java.lang.SuppressWarnings.class, Map.of("value", "unused"))) .build(); - private static final TypedElementName __methodIB2__p1 = TypedElementNameDefault.builder() + private static final TypedElementInfo __methodIB2__p1 = TypedElementInfoDefault.builder() .typeName(create(java.lang.String.class) ) .elementName("p1") .addAnnotation(AnnotationAndValueDefault.create(jakarta.inject.Named.class, Map.of("value", "arg1"))) .build(); - private static final TypedElementName __close = TypedElementNameDefault.builder() + private static final TypedElementInfo __close = TypedElementInfoDefault.builder() .typeName(create(void.class)) .elementName("close") .addAnnotation(AnnotationAndValueDefault.create(io.helidon.pico.api.ExternalContracts.class, Map.of("moduleNames", "test1, test2", "value", "java.io.Closeable"))) @@ -133,7 +133,7 @@ public class YImpl$$Pico$$Interceptor /* extends io.helidon.pico.tests.pico.inte this.__methodIB__call = new InterceptedMethod( __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __methodIB__interceptors, __methodIB, - new TypedElementName[] {__methodIB__p1}) { + new TypedElementInfo[] {__methodIB__p1}) { @Override public java.lang.Void invoke(Object... args) throws Throwable { impl().methodIB((java.lang.String) args[0]); @@ -143,7 +143,7 @@ public class YImpl$$Pico$$Interceptor /* extends io.helidon.pico.tests.pico.inte this.__methodIB2__call = new InterceptedMethod( __impl, __sp, __serviceTypeName, __serviceLevelAnnotations, __methodIB2__interceptors, __methodIB2, - new TypedElementName[] {__methodIB2__p1}) { + new TypedElementInfo[] {__methodIB2__p1}) { @Override public java.lang.String invoke(Object... args) throws Throwable { return impl().methodIB2((java.lang.String) args[0]); diff --git a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/TestNamedInterceptor.java b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/TestNamedInterceptor.java index bc38aaf5179..303c9cfd181 100644 --- a/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/TestNamedInterceptor.java +++ b/pico/tests/resources-plain/src/main/java/io/helidon/pico/tests/plain/interceptor/TestNamedInterceptor.java @@ -19,7 +19,7 @@ import java.util.concurrent.atomic.AtomicInteger; import io.helidon.common.types.TypeNameDefault; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; import io.helidon.pico.api.Interceptor; import io.helidon.pico.api.InvocationContext; @@ -37,7 +37,7 @@ public V proceed(InvocationContext ctx, Object... args) { assert (ctx != null); - TypedElementName methodInfo = ctx.elementInfo(); + TypedElementInfo methodInfo = ctx.elementInfo(); if (methodInfo != null && methodInfo.typeName().equals(TypeNameDefault.create(long.class))) { V result = chain.proceed(args); long longResult = (Long) result; diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorCodeGen.java b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorCodeGen.java index 01c2d200d62..580dc95c986 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorCodeGen.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorCodeGen.java @@ -188,4 +188,13 @@ public interface ActivatorCreatorCodeGen { @ConfiguredOption(DEFAULT_CLASS_PREFIX_NAME) String classPrefixName(); + /** + * Used in conjunction with {@link ActivatorCreatorConfigOptions#isModuleCreated()}. If a module is created and this set is + * populated then this set will be used to represent all {@link io.helidon.pico.api.Activator} type names that should be code + * generated for this {@link io.helidon.pico.api.ModuleComponent}. + * + * @return all module activator type names known for this given module being processed + */ + Set allModuleActivatorTypeNames(); + } diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorDefault.java b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorDefault.java index f2fc6069626..7bf8bbff38e 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorDefault.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorDefault.java @@ -27,6 +27,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; @@ -139,7 +140,7 @@ ActivatorCreatorResponse codegen(ActivatorCreatorRequest req, CodeGenPaths codeGenPaths = req.codeGenPaths().orElse(null); Map serviceTypeToIsAbstractType = req.codeGen().serviceTypeIsAbstractTypes(); List activatorTypeNames = new ArrayList<>(); - List activatorTypeNamesPutInModule = new ArrayList<>(); + Set activatorTypeNamesPutInModule = new TreeSet<>(req.codeGen().allModuleActivatorTypeNames()); Map activatorDetails = new LinkedHashMap<>(); for (TypeName serviceTypeName : req.serviceTypeNames()) { try { @@ -164,6 +165,7 @@ ActivatorCreatorResponse codegen(ActivatorCreatorRequest req, } } builder.serviceTypeNames(activatorTypeNames) + .activatorTypeNamesPutInComponentModule(activatorTypeNamesPutInModule) .serviceTypeDetails(activatorDetails); ModuleDetail moduleDetail; @@ -207,7 +209,7 @@ ActivatorCreatorResponse codegen(ActivatorCreatorRequest req, } private ModuleDetail toModuleDetail(ActivatorCreatorRequest req, - List activatorTypeNamesPutInModule, + Set activatorTypeNamesPutInModule, TypeName moduleTypeName, TypeName applicationTypeName, boolean isApplicationCreated, @@ -628,7 +630,7 @@ String toModuleBody(ActivatorCreatorRequest req, String packageName, String className, String moduleName, - List activatorTypeNames) { + Set activatorTypeNames) { String template = templateHelper().safeLoadTemplate(req.templateName(), SERVICE_PROVIDER_MODULE_HBS); Map subst = new HashMap<>(); diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorResponse.java b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorResponse.java index 5bc1c06b027..319ec8a87b9 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorResponse.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ActivatorCreatorResponse.java @@ -18,6 +18,7 @@ import java.util.Map; import java.util.Optional; +import java.util.Set; import io.helidon.builder.Builder; import io.helidon.builder.Singular; @@ -37,13 +38,20 @@ public interface ActivatorCreatorResponse extends GeneralCreatorResponse { ActivatorCreatorConfigOptions getConfigOptions(); /** - * return The interceptors that were generated. + * Return the interceptors that were generated. * * @return interceptors generated */ @Singular Map serviceTypeInterceptorPlans(); + /** + * The activator types placed in the generated {@link io.helidon.pico.api.ModuleComponent}. + * + * @return the activator type names placed in the module component + */ + Set activatorTypeNamesPutInComponentModule(); + /** * The module-info detail, if a module was created. * diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorConfigOptions.java b/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorConfigOptions.java index 860ce45317a..23f0e5d9985 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorConfigOptions.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorConfigOptions.java @@ -21,6 +21,7 @@ import io.helidon.builder.Builder; import io.helidon.builder.Singular; import io.helidon.common.types.TypeName; +import io.helidon.config.metadata.ConfiguredOption; /** * Configuration directives and options optionally provided to the {@link io.helidon.pico.tools.spi.ApplicationCreator}. @@ -52,11 +53,17 @@ enum PermittedProviderType { } + /** + * The default permitted provider type. + */ + PermittedProviderType DEFAULT_PERMITTED_PROVIDER_TYPE = PermittedProviderType.ALL; + /** * Determines the application generator's tolerance around the usage of providers. * * @return provider generation permission type */ + @ConfiguredOption("ALL") PermittedProviderType permittedProviderTypes(); /** diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorDefault.java b/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorDefault.java index 06369d46f98..e31b507785d 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorDefault.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ApplicationCreatorDefault.java @@ -32,6 +32,7 @@ import io.helidon.common.Weight; import io.helidon.common.types.AnnotationAndValue; import io.helidon.common.types.TypeName; +import io.helidon.pico.api.CommonQualifiers; import io.helidon.pico.api.DependenciesInfo; import io.helidon.pico.api.InjectionPointInfo; import io.helidon.pico.api.ModuleComponent; @@ -160,10 +161,22 @@ static boolean isAllowListedProviderName(ApplicationCreatorConfigOptions configO } else if (ApplicationCreatorConfigOptions.PermittedProviderType.NONE == opt) { return false; } else { - return configOptions.permittedProviderNames().contains(typeName.name()); + if (configOptions.permittedProviderNames().contains(typeName.name())) { + return true; + } + + return anyWildcardMatches(typeName, configOptions.permittedProviderNames()); } } + static boolean anyWildcardMatches(TypeName typeName, + Set permittedProviderTypeNames) { + return permittedProviderTypeNames.stream() + .filter(it -> it.endsWith(CommonQualifiers.WILDCARD)) + .map(it -> it.substring(0, it.length() - 1)) + .anyMatch(it -> typeName.name().startsWith(it)); + } + static ServiceInfoCriteria toServiceInfoCriteria(TypeName typeName) { return ServiceInfoCriteriaDefault.builder() .serviceTypeName(typeName.name()) diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenFiler.java b/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenFiler.java index 950e1fdbad8..76000e05c78 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenFiler.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/CodeGenFiler.java @@ -191,7 +191,13 @@ public void codegenMetaInfServices(CodeGenPaths paths, } } - private static Path targetClassOutputPath(Filer filer) { + /** + * Returns the target class output directory. + * + * @param filer the filer + * @return the path to the target class output directory + */ + public static Path targetClassOutputPath(Filer filer) { if (filer instanceof AbstractFilerMessager.DirectFilerMessager) { CodeGenPaths paths = ((AbstractFilerMessager.DirectFilerMessager) filer).codeGenPaths(); return Path.of(paths.outputPath().orElseThrow()); @@ -209,7 +215,13 @@ private static Path targetClassOutputPath(Filer filer) { } } - private static Path scratchClassOutputPath(Path targetOutputPath) { + /** + * Returns the path to the target scratch directory for Pico. + * + * @param targetOutputPath the target class output path + * @return the pico target scratch path + */ + public static Path scratchClassOutputPath(Path targetOutputPath) { Path fileName = targetOutputPath.getFileName(); Path parent = targetOutputPath.getParent(); if (fileName == null || parent == null) { diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/CustomAnnotationTemplateRequest.java b/pico/tools/src/main/java/io/helidon/pico/tools/CustomAnnotationTemplateRequest.java index 493691c544c..d8161dbcf7a 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/CustomAnnotationTemplateRequest.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/CustomAnnotationTemplateRequest.java @@ -16,14 +16,11 @@ package io.helidon.pico.tools; -import java.util.List; - import io.helidon.builder.Builder; import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.pico.api.InjectionPointInfo; import io.helidon.pico.api.ServiceInfoBasics; /** @@ -45,29 +42,7 @@ public interface CustomAnnotationTemplateRequest { * * @return the target element being processed */ - TypedElementName targetElement(); - - /** - * The access modifier of the element. - * - * @return the access modifier of the element - */ - InjectionPointInfo.Access targetElementAccess(); - - /** - * Only applicable for {@link javax.lang.model.element.ElementKind#METHOD} or - * {@link javax.lang.model.element.ElementKind#CONSTRUCTOR}. - * - * @return the list of typed arguments for this method or constructor - */ - List targetElementArgs(); - - /** - * Returns true if the element is declared to be static. - * - * @return returns true if the element is declared to be private - */ - boolean isElementStatic(); + TypedElementInfo targetElement(); /** * Projects the {@link #enclosingTypeInfo()} as a {@link ServiceInfoBasics} type. diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/CustomAnnotationTemplateResponse.java b/pico/tools/src/main/java/io/helidon/pico/tools/CustomAnnotationTemplateResponse.java index 41d2eea5e76..8e94cd5e697 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/CustomAnnotationTemplateResponse.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/CustomAnnotationTemplateResponse.java @@ -21,7 +21,7 @@ import io.helidon.builder.Builder; import io.helidon.builder.Singular; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; /** * The response from {@link io.helidon.pico.tools.spi.CustomAnnotationTemplateCreator#create(CustomAnnotationTemplateRequest)}. @@ -50,7 +50,7 @@ public interface CustomAnnotationTemplateResponse { * @return map of generated type name (which is really just a directory path under resources) to the resource to be generated */ @Singular - Map generatedResources(); + Map generatedResources(); /** * Aggregates the responses given to one response. diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/InterceptorCreatorDefault.java b/pico/tools/src/main/java/io/helidon/pico/tools/InterceptorCreatorDefault.java index c1d40e5f496..421c1003304 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/InterceptorCreatorDefault.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/InterceptorCreatorDefault.java @@ -1057,7 +1057,7 @@ private static InterceptedMethodCodeGen toBody(InterceptedElement method) { String elementArgInfo = ""; if (hasArgs) { - elementArgInfo = ",\n\t\t\t\tnew TypedElementName[] {" + typedElementArgs + "}"; + elementArgInfo = ",\n\t\t\t\tnew TypedElementInfo[] {" + typedElementArgs + "}"; } return new InterceptedMethodCodeGen(name, methodDecl, true, hasReturn, supplierType, elementArgInfo, args, objArrayArgs, untypedElementArgs, diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ModuleUtils.java b/pico/tools/src/main/java/io/helidon/pico/tools/ModuleUtils.java index 20dff362081..ef1fc0d30f9 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/ModuleUtils.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ModuleUtils.java @@ -26,19 +26,24 @@ import java.nio.file.Paths; import java.util.Collection; import java.util.LinkedHashSet; +import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; import java.util.Set; import java.util.Stack; import java.util.concurrent.atomic.AtomicReference; import javax.lang.model.element.TypeElement; +import io.helidon.common.HelidonServiceLoader; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNameDefault; import io.helidon.pico.api.Application; import io.helidon.pico.api.ModuleComponent; import io.helidon.pico.api.PicoServicesConfig; +import io.helidon.pico.tools.spi.ModuleComponentNamer; import static io.helidon.pico.tools.CommonUtils.first; import static io.helidon.pico.tools.CommonUtils.hasValue; @@ -76,16 +81,34 @@ private ModuleUtils() { /** * Returns the suggested package name to use. * - * @param descriptor optionally, the module-info descriptor - * @param typeNames optionally, the set of types that are being codegen'ed + * @param descriptor the module-info descriptor + * @param typeNames the set of types that are being codegen'ed * @param defaultPackageName the default package name to use if all options are exhausted * @return the suggested package name */ public static String toSuggestedGeneratedPackageName(ModuleInfoDescriptor descriptor, Collection typeNames, String defaultPackageName) { - String export = null; + Objects.requireNonNull(descriptor); + return innerToSuggestedGeneratedPackageName(descriptor, typeNames, defaultPackageName); + } + + /** + * Returns the suggested package name to use. + * + * @param typeNames the set of types that are being codegen'ed + * @param defaultPackageName the default package name to use if all options are exhausted + * @return the suggested package name + */ + public static String toSuggestedGeneratedPackageName(Collection typeNames, + String defaultPackageName) { + return innerToSuggestedGeneratedPackageName(null, typeNames, defaultPackageName); + } + static String innerToSuggestedGeneratedPackageName(ModuleInfoDescriptor descriptor, + Collection typeNames, + String defaultPackageName) { + String export = null; if (descriptor != null) { Optional provides = descriptor.first(Application.class.getName()); if (provides.isEmpty() || provides.get().withOrTo().isEmpty()) { @@ -100,6 +123,13 @@ public static String toSuggestedGeneratedPackageName(ModuleInfoDescriptor descri } if (export == null && typeNames != null) { + // check for any providers who want to give us a name to use + Optional suggested = toSuggestedPackageNameFromProviders(typeNames); + if (suggested.isPresent()) { + return suggested.get(); + } + + // default to the first one export = typeNames.stream() .sorted() .map(TypeName::packageName) @@ -109,6 +139,28 @@ public static String toSuggestedGeneratedPackageName(ModuleInfoDescriptor descri return (export != null) ? export : defaultPackageName; } + private static Optional toSuggestedPackageNameFromProviders(Collection typeNames) { + List namers = HelidonServiceLoader.create(namerLoader()).asList(); + return namers.stream() + .map(it -> it.suggestedPackageName(typeNames)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + } + + private static ServiceLoader namerLoader() { + try { + // note: it is important to use this class' CL since maven will not give us the "right" one. + return ServiceLoader.load( + ModuleComponentNamer.class, ModuleComponentNamer.class.getClassLoader()); + } catch (ServiceConfigurationError e) { + // see issue #6261 - running inside the IDE? + // this version will use the thread ctx classloader + System.getLogger(ModuleComponentNamer.class.getName()).log(System.Logger.Level.WARNING, e.getMessage(), e); + return ServiceLoader.load(ModuleComponentNamer.class); + } + } + /** * Common way for naming a module (generally for use by {@link io.helidon.pico.api.Application} and * {@link io.helidon.pico.api.ModuleComponent}). diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/ServicesToProcess.java b/pico/tools/src/main/java/io/helidon/pico/tools/ServicesToProcess.java index bc0f4bb56f9..489ad4d2c95 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/ServicesToProcess.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/ServicesToProcess.java @@ -45,6 +45,8 @@ import io.helidon.pico.api.Resettable; import io.helidon.pico.runtime.Dependencies; +import static io.helidon.pico.tools.ModuleUtils.innerToSuggestedGeneratedPackageName; + /** * Tracks the services to process, and ingests them to build the codegen model. *

    @@ -823,7 +825,8 @@ String determineGeneratedPackageName() { } ModuleInfoDescriptor descriptor = lastKnownModuleInfoDescriptor(); - String packageName = ModuleUtils.toSuggestedGeneratedPackageName(descriptor, serviceTypeNames(), PicoServicesConfig.NAME); + String packageName = innerToSuggestedGeneratedPackageName(descriptor, serviceTypeNames(), PicoServicesConfig.NAME); + return Objects.requireNonNull(packageName); } diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/TypeTools.java b/pico/tools/src/main/java/io/helidon/pico/tools/TypeTools.java index e1869374543..a9d9fc8468c 100644 --- a/pico/tools/src/main/java/io/helidon/pico/tools/TypeTools.java +++ b/pico/tools/src/main/java/io/helidon/pico/tools/TypeTools.java @@ -1362,7 +1362,7 @@ public static String oppositeOf(String typeName) { boolean startsWithJakarta = typeName.startsWith(TypeNames.PREFIX_JAKARTA); boolean startsWithJavax = !startsWithJakarta && typeName.startsWith(TypeNames.PREFIX_JAVAX); - assert (startsWithJakarta || startsWithJavax); + assert (startsWithJakarta || startsWithJavax) : typeName; if (startsWithJakarta) { return typeName.replace(TypeNames.PREFIX_JAKARTA, TypeNames.PREFIX_JAVAX); @@ -1472,11 +1472,11 @@ static InjectionPointInfo.Access toAccess(int modifiers) { * @return the access */ public static ElementInfo.Access toAccess(Set modifiers) { - if (modifiers.contains(TypeInfo.MODIFIER_PROTECTED)) { + if (modifiers.stream().anyMatch(TypeInfo.MODIFIER_PROTECTED::equalsIgnoreCase)) { return ElementInfo.Access.PROTECTED; - } else if (modifiers.contains(TypeInfo.MODIFIER_PRIVATE)) { + } else if (modifiers.stream().anyMatch(TypeInfo.MODIFIER_PRIVATE::equalsIgnoreCase)) { return ElementInfo.Access.PRIVATE; - } else if (modifiers.contains(TypeInfo.MODIFIER_PUBLIC)) { + } else if (modifiers.stream().anyMatch(TypeInfo.MODIFIER_PUBLIC::equalsIgnoreCase)) { return ElementInfo.Access.PUBLIC; } return ElementInfo.Access.PACKAGE_PRIVATE; diff --git a/pico/tools/src/main/java/io/helidon/pico/tools/spi/ModuleComponentNamer.java b/pico/tools/src/main/java/io/helidon/pico/tools/spi/ModuleComponentNamer.java new file mode 100644 index 00000000000..8b1462e54a2 --- /dev/null +++ b/pico/tools/src/main/java/io/helidon/pico/tools/spi/ModuleComponentNamer.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.pico.tools.spi; + +import java.util.Collection; +import java.util.Optional; + +import io.helidon.common.types.TypeName; + +/** + * Implementors of these are responsible for choosing the best {@link io.helidon.common.types.TypeName} for any + * {@link io.helidon.pico.api.ModuleComponent} being generated. Note that this provider will only be called if there is some + * ambiguity in choosing a name (e.g., there are no exports or there is no {@code module-info} for the module being processed, + * etc.) + */ +public interface ModuleComponentNamer { + + /** + * Implementors should return the suggested {@link io.helidon.pico.api.ModuleComponent} package name, or {@code empty} + * to abstain from naming. + * + * @param serviceActivatorTypeNames the set of activator type names to be generated + * @return the suggested package name for the component module, or empty to abstain from naming + */ + Optional suggestedPackageName(Collection serviceActivatorTypeNames); + +} diff --git a/pico/tools/src/main/java/module-info.java b/pico/tools/src/main/java/module-info.java index ee54890cb90..373c9b46d0f 100644 --- a/pico/tools/src/main/java/module-info.java +++ b/pico/tools/src/main/java/module-info.java @@ -14,15 +14,6 @@ * limitations under the License. */ -import io.helidon.pico.tools.ActivatorCreatorDefault; -import io.helidon.pico.tools.ApplicationCreatorDefault; -import io.helidon.pico.tools.ExternalModuleCreatorDefault; -import io.helidon.pico.tools.InterceptorCreatorDefault; -import io.helidon.pico.tools.spi.ActivatorCreator; -import io.helidon.pico.tools.spi.ApplicationCreator; -import io.helidon.pico.tools.spi.ExternalModuleCreator; -import io.helidon.pico.tools.spi.InterceptorCreator; - /** * The Pico Tools module. */ @@ -50,13 +41,14 @@ uses io.helidon.pico.tools.spi.CustomAnnotationTemplateCreator; uses io.helidon.pico.tools.spi.ExternalModuleCreator; uses io.helidon.pico.tools.spi.InterceptorCreator; + uses io.helidon.pico.tools.spi.ModuleComponentNamer; - provides ActivatorCreator - with ActivatorCreatorDefault; - provides ApplicationCreator - with ApplicationCreatorDefault; - provides ExternalModuleCreator - with ExternalModuleCreatorDefault; - provides InterceptorCreator - with InterceptorCreatorDefault; + provides io.helidon.pico.tools.spi.ActivatorCreator + with io.helidon.pico.tools.ActivatorCreatorDefault; + provides io.helidon.pico.tools.spi.ApplicationCreator + with io.helidon.pico.tools.ApplicationCreatorDefault; + provides io.helidon.pico.tools.spi.ExternalModuleCreator + with io.helidon.pico.tools.ExternalModuleCreatorDefault; + provides io.helidon.pico.tools.spi.InterceptorCreator + with io.helidon.pico.tools.InterceptorCreatorDefault; } diff --git a/pico/tools/src/main/resources/templates/pico/default/interface-based-interceptor.hbs b/pico/tools/src/main/resources/templates/pico/default/interface-based-interceptor.hbs index 0640aeadc35..15ee366b4b7 100644 --- a/pico/tools/src/main/resources/templates/pico/default/interface-based-interceptor.hbs +++ b/pico/tools/src/main/resources/templates/pico/default/interface-based-interceptor.hbs @@ -24,9 +24,9 @@ import java.util.function.Function; import io.helidon.common.types.AnnotationAndValue; import io.helidon.common.types.AnnotationAndValueDefault; import io.helidon.common.types.TypeNameDefault; -import io.helidon.common.types.TypedElementNameDefault; +import io.helidon.common.types.TypedElementInfoDefault; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; import io.helidon.pico.api.ClassNamed; import io.helidon.pico.api.InvocationContextDefault; import io.helidon.pico.api.Interceptor; @@ -56,7 +56,7 @@ public class {{className}} /* extends {{parent}} */ implements {{interfaces}} { private static final List __serviceLevelAnnotations = List.of({{#servicelevelannotations}} {{{.}}}{{#unless @last}},{{/unless}}{{/servicelevelannotations}}); {{#interceptedmethoddecls}} - private static final TypedElementName __{{id}} = TypedElementNameDefault.builder() + private static final TypedElementInfo __{{id}} = TypedElementInfoDefault.builder() {{{.}}} .build();{{/interceptedmethoddecls}} diff --git a/pico/tools/src/main/resources/templates/pico/default/no-arg-based-interceptor.hbs b/pico/tools/src/main/resources/templates/pico/default/no-arg-based-interceptor.hbs index 074c2c51f44..b118502f96c 100644 --- a/pico/tools/src/main/resources/templates/pico/default/no-arg-based-interceptor.hbs +++ b/pico/tools/src/main/resources/templates/pico/default/no-arg-based-interceptor.hbs @@ -24,9 +24,9 @@ import java.util.function.Function; import io.helidon.common.types.AnnotationAndValue; import io.helidon.common.types.AnnotationAndValueDefault; import io.helidon.common.types.TypeNameDefault; -import io.helidon.common.types.TypedElementNameDefault; +import io.helidon.common.types.TypedElementInfoDefault; import io.helidon.common.types.TypeName; -import io.helidon.common.types.TypedElementName; +import io.helidon.common.types.TypedElementInfo; import io.helidon.pico.api.ClassNamed; import io.helidon.pico.api.InvocationContextDefault; import io.helidon.pico.api.Interceptor; @@ -56,7 +56,7 @@ public class {{className}} extends {{parent}} { private static final List __serviceLevelAnnotations = List.of({{#servicelevelannotations}} {{{.}}}{{#unless @last}},{{/unless}}{{/servicelevelannotations}}); {{#interceptedmethoddecls}} - private static final TypedElementName __{{id}} = TypedElementNameDefault.builder() + private static final TypedElementInfo __{{id}} = TypedElementInfoDefault.builder() {{{.}}} .build();{{/interceptedmethoddecls}} diff --git a/pom.xml b/pom.xml index ca2867a2f61..e36d4f21666 100644 --- a/pom.xml +++ b/pom.xml @@ -96,7 +96,7 @@ 1.12 3.1.2 3.10.1 - 3.1.2 + 3.6.0 1.0 2.7.5.1 3.0.0-M1 @@ -106,7 +106,7 @@ 3.0.3 ${version.lib.hibernate} 0.8.5 - 1.1.0 + 3.1.2 3.0.2 3.3.1 2.5.0 @@ -474,7 +474,7 @@ ${version.plugin.exec} - org.jboss.jandex + io.smallrye jandex-maven-plugin ${version.plugin.jandex} diff --git a/reactive/openapi/src/main/java/io/helidon/reactive/openapi/OpenAPISupport.java b/reactive/openapi/src/main/java/io/helidon/reactive/openapi/OpenAPISupport.java deleted file mode 100644 index 06c22701ce0..00000000000 --- a/reactive/openapi/src/main/java/io/helidon/reactive/openapi/OpenAPISupport.java +++ /dev/null @@ -1,864 +0,0 @@ -/* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.reactive.openapi; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; -import java.io.StringWriter; -import java.lang.System.Logger.Level; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import io.helidon.common.LazyValue; -import io.helidon.common.http.Http; -import io.helidon.common.media.type.MediaType; -import io.helidon.common.media.type.MediaTypes; -import io.helidon.config.Config; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.cors.CrossOriginConfig; -import io.helidon.openapi.ExpandedTypeDescription; -import io.helidon.openapi.OpenAPIMediaType; -import io.helidon.openapi.OpenAPIParser; -import io.helidon.openapi.ParserHelper; -import io.helidon.openapi.Serializer; -import io.helidon.openapi.internal.OpenAPIConfigImpl; -import io.helidon.reactive.media.common.MessageBodyReaderContext; -import io.helidon.reactive.media.common.MessageBodyWriterContext; -import io.helidon.reactive.media.jsonp.JsonpSupport; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.ServerRequest; -import io.helidon.reactive.webserver.ServerResponse; -import io.helidon.reactive.webserver.Service; -import io.helidon.reactive.webserver.cors.CorsEnabledServiceHelper; - -import io.smallrye.openapi.api.OpenApiConfig; -import io.smallrye.openapi.api.OpenApiDocument; -import io.smallrye.openapi.api.models.OpenAPIImpl; -import io.smallrye.openapi.api.util.MergeUtil; -import io.smallrye.openapi.runtime.OpenApiProcessor; -import io.smallrye.openapi.runtime.OpenApiStaticFile; -import io.smallrye.openapi.runtime.io.Format; -import io.smallrye.openapi.runtime.scanner.AnnotationScannerExtension; -import io.smallrye.openapi.runtime.scanner.OpenApiAnnotationScanner; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonNumber; -import jakarta.json.JsonObject; -import jakarta.json.JsonReader; -import jakarta.json.JsonReaderFactory; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; -import org.eclipse.microprofile.openapi.models.Extensible; -import org.eclipse.microprofile.openapi.models.OpenAPI; -import org.eclipse.microprofile.openapi.models.Operation; -import org.eclipse.microprofile.openapi.models.PathItem; -import org.eclipse.microprofile.openapi.models.Reference; -import org.eclipse.microprofile.openapi.models.media.Schema; -import org.eclipse.microprofile.openapi.models.servers.ServerVariable; -import org.jboss.jandex.IndexView; -import org.yaml.snakeyaml.TypeDescription; - -import static io.helidon.reactive.webserver.cors.CorsEnabledServiceHelper.CORS_CONFIG_KEY; - -/** - * Provides an endpoint and supporting logic for returning an OpenAPI document - * that describes the endpoints handled by the server. - *

    - * The server can use the {@link io.helidon.reactive.openapi.OpenAPISupport.Builder} to set OpenAPI-related attributes. If - * the server uses none of these builder methods and does not provide a static - * {@code openapi} file, then the {@code /openapi} endpoint responds with a - * nearly-empty OpenAPI document. - */ -public class OpenAPISupport implements Service { - - /** - * Default path for serving the OpenAPI document. - */ - public static final String DEFAULT_WEB_CONTEXT = "/openapi"; - - /** - * Default media type used in responses in absence of incoming Accept - * header. - */ - public static final MediaType DEFAULT_RESPONSE_MEDIA_TYPE = MediaTypes.APPLICATION_OPENAPI_YAML; - private static final String OPENAPI_ENDPOINT_FORMAT_QUERY_PARAMETER = "format"; - private static final System.Logger LOGGER = System.getLogger(OpenAPISupport.class.getName()); - private static final String DEFAULT_STATIC_FILE_PATH_PREFIX = "META-INF/openapi."; - private static final String OPENAPI_EXPLICIT_STATIC_FILE_LOG_MESSAGE_FORMAT = "Using specified OpenAPI static file %s"; - private static final String OPENAPI_DEFAULTED_STATIC_FILE_LOG_MESSAGE_FORMAT = "Using default OpenAPI static file %s"; - private static final String FEATURE_NAME = "OpenAPI"; - private static final JsonReaderFactory JSON_READER_FACTORY = Json.createReaderFactory(Collections.emptyMap()); - private static final LazyValue HELPER = LazyValue.create(ParserHelper::create); - - private final String webContext; - private final ConcurrentMap cachedDocuments = new ConcurrentHashMap<>(); - private final Map, ExpandedTypeDescription> implsToTypes; - private final CorsEnabledServiceHelper corsEnabledServiceHelper; - /* - * To handle the MP case, we must defer constructing the OpenAPI in-memory model until after the server has instantiated - * the Application instances. By then the builder has already been used to build the OpenAPISupport object. So save the - * following raw materials so we can construct the model at that later time. - */ - private final OpenApiConfig openApiConfig; - private final OpenApiStaticFile openApiStaticFile; - private final Supplier> indexViewsSupplier; - private final Lock modelAccess = new ReentrantLock(true); - private OpenAPI model = null; - - /** - * Creates a new instance of {@code OpenAPISupport}. - * - * @param builder the builder to use in constructing the instance - */ - protected OpenAPISupport(Builder builder) { - implsToTypes = ExpandedTypeDescription.buildImplsToTypes(HELPER.get()); - webContext = builder.webContext(); - corsEnabledServiceHelper = CorsEnabledServiceHelper.create(FEATURE_NAME, builder.crossOriginConfig); - openApiConfig = builder.openAPIConfig(); - openApiStaticFile = builder.staticFile(); - indexViewsSupplier = builder.indexViewsSupplier(); - } - - /** - * Creates a new {@link io.helidon.reactive.openapi.OpenAPISupport.Builder} for {@code OpenAPISupport} using defaults. - * - * @return new Builder - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Creates a new {@link io.helidon.reactive.openapi.OpenAPISupport} instance using defaults. - * - * @return new OpenAPISUpport - */ - public static OpenAPISupport create() { - return builder().build(); - } - - /** - * Creates a new {@link io.helidon.reactive.openapi.OpenAPISupport} instance using the - * 'openapi' portion of the provided - * {@link io.helidon.config.Config} object. - * - * @param config {@code Config} object containing OpenAPI-related settings - * @return new {@code OpenAPISupport} instance created using the - * helidonConfig settings - */ - public static OpenAPISupport create(Config config) { - return builder().config(config).build(); - } - - @Override - public void update(Routing.Rules rules) { - configureEndpoint(rules); - } - - /** - * Sets up the OpenAPI endpoint by adding routing to the specified rules - * set. - * - * @param rules routing rules to be augmented with OpenAPI endpoint - */ - public void configureEndpoint(Routing.Rules rules) { - - rules.get(this::registerJsonpSupport) - .any(webContext, corsEnabledServiceHelper.processor()) - .get(webContext, this::prepareResponse); - } - - /** - * Triggers preparation of the model from external code. - */ - protected void prepareModel() { - model(); - } - - /** - * Returns the OpenAPI document in the requested format. - * - * @param resultMediaType requested media type - * @return String containing the formatted OpenAPI document - * @throws java.io.IOException in case of errors serializing the OpenAPI document - * from its underlying data - */ - String prepareDocument(MediaType resultMediaType) { - OpenAPIMediaType matchingOpenAPIMediaType - = OpenAPIMediaType.byMediaType(resultMediaType) - .orElseGet(() -> { - LOGGER.log(Level.TRACE, - () -> String.format( - "Requested media type %s not supported; using default", - resultMediaType.text())); - return OpenAPIMediaType.DEFAULT_TYPE; - }); - - Format resultFormat = matchingOpenAPIMediaType.format(); - - String result = cachedDocuments.computeIfAbsent(resultFormat, - fmt -> { - String r = formatDocument(fmt); - LOGGER.log(Level.TRACE, - "Created and cached OpenAPI document in {0} format", - fmt.toString()); - return r; - }); - return result; - } - - private static void adjustTypeDescriptions(Map, ExpandedTypeDescription> types) { - /* - * We need to adjust the {@code TypeDescription} objects set up by the generated {@code SnakeYAMLParserHelper} class - * because there are some OpenAPI-specific issues that the general-purpose helper generator cannot know about. - */ - - /* - * In the OpenAPI document, HTTP methods are expressed in lower-case. But the associated Java methods on the PathItem - * class use the HTTP method names in upper-case. So for each HTTP method, "add" a property to PathItem's type - * description using the lower-case name but upper-case Java methods and exclude the upper-case property that - * SnakeYAML's automatic analysis of the class already created. - */ - ExpandedTypeDescription pathItemTD = types.get(PathItem.class); - for (PathItem.HttpMethod m : PathItem.HttpMethod.values()) { - pathItemTD.substituteProperty(m.name().toLowerCase(), Operation.class, getter(m), setter(m)); - pathItemTD.addExcludes(m.name()); - } - - /* - * An OpenAPI document can contain a property named "enum" for Schema and ServerVariable, but the related Java methods - * use "enumeration". - */ - Set.>of(Schema.class, ServerVariable.class).forEach(c -> { - ExpandedTypeDescription tdWithEnumeration = types.get(c); - tdWithEnumeration.substituteProperty("enum", List.class, "getEnumeration", "setEnumeration"); - tdWithEnumeration.addPropertyParameters("enum", String.class); - tdWithEnumeration.addExcludes("enumeration"); - }); - - /* - * SnakeYAML derives properties only from methods declared directly by each OpenAPI interface, not from methods defined - * on other interfaces which the original one extends. Those we have to handle explicitly. - */ - for (ExpandedTypeDescription td : types.values()) { - if (Extensible.class.isAssignableFrom(td.getType())) { - td.addExtensions(); - } - if (td.hasDefaultProperty()) { - td.substituteProperty("default", Object.class, "getDefaultValue", "setDefaultValue"); - td.addExcludes("defaultValue"); - } - if (isRef(td)) { - td.addRef(); - } - } - } - - private static boolean isRef(TypeDescription td) { - for (Class c : td.getType().getInterfaces()) { - if (c.equals(Reference.class)) { - return true; - } - } - return false; - } - - private static String getter(PathItem.HttpMethod method) { - return methodName("get", method); - } - - private static String setter(PathItem.HttpMethod method) { - return methodName("set", method); - } - - private static String methodName(String operation, PathItem.HttpMethod method) { - return operation + method.name(); - } - - private static ClassLoader getContextClassLoader() { - return Thread.currentThread().getContextClassLoader(); - } - - private static String typeFromPath(Path path) { - Path staticFileNamePath = path.getFileName(); - if (staticFileNamePath == null) { - throw new IllegalArgumentException("File path " - + path.toAbsolutePath() - + " does not seem to have a file name value but one is expected"); - } - String pathText = staticFileNamePath.toString(); - String specifiedFileType = pathText.substring(pathText.lastIndexOf(".") + 1); - return specifiedFileType; - } - - private static T access(Lock guard, Supplier operation) { - guard.lock(); - try { - return operation.get(); - } finally { - guard.unlock(); - } - } - - private OpenAPI model() { - return access(modelAccess, () -> { - if (model == null) { - model = prepareModel(openApiConfig, openApiStaticFile, indexViewsSupplier.get()); - } - return model; - }); - } - - private void registerJsonpSupport(ServerRequest req, ServerResponse res) { - MessageBodyReaderContext readerContext = req.content().readerContext(); - MessageBodyWriterContext writerContext = res.writerContext(); - JsonpSupport.create().register(readerContext, writerContext); - req.next(); - } - - /** - * Prepares the OpenAPI model that later will be used to create the OpenAPI - * document for endpoints in this application. - * - * @param config {@code OpenApiConfig} object describing paths, servers, etc. - * @param staticFile the static file, if any, to be included in the resulting model - * @param filteredIndexViews possibly empty list of FilteredIndexViews to use in harvesting definitions from the code - * @return the OpenAPI model - * @throws RuntimeException in case of errors reading any existing static - * OpenAPI document - */ - private OpenAPI prepareModel(OpenApiConfig config, OpenApiStaticFile staticFile, - List filteredIndexViews) { - try { - // The write lock guarding the model has already been acquired. - OpenApiDocument.INSTANCE.reset(); - OpenApiDocument.INSTANCE.config(config); - OpenApiDocument.INSTANCE.modelFromReader(OpenApiProcessor.modelFromReader(config, getContextClassLoader())); - if (staticFile != null) { - OpenApiDocument.INSTANCE.modelFromStaticFile(OpenAPIParser.parse(HELPER.get().types(), staticFile.getContent())); - } - if (isAnnotationProcessingEnabled(config)) { - expandModelUsingAnnotations(config, filteredIndexViews); - } else { - LOGGER.log(Level.DEBUG, "OpenAPI Annotation processing is disabled"); - } - OpenApiDocument.INSTANCE.filter(OpenApiProcessor.getFilter(config, getContextClassLoader())); - OpenApiDocument.INSTANCE.initialize(); - OpenAPIImpl instance = OpenAPIImpl.class.cast(OpenApiDocument.INSTANCE.get()); - - // Create a copy, primarily to avoid problems during unit testing. - // The SmallRye MergeUtil omits the openapi value, so we need to set it explicitly. - return MergeUtil.merge(new OpenAPIImpl(), instance) - .openapi(instance.getOpenapi()); - } catch (IOException ex) { - throw new RuntimeException("Error initializing OpenAPI information", ex); - } - } - - private boolean isAnnotationProcessingEnabled(OpenApiConfig config) { - return !config.scanDisable(); - } - - private void expandModelUsingAnnotations(OpenApiConfig config, List filteredIndexViews) { - if (filteredIndexViews.isEmpty() || config.scanDisable()) { - return; - } - - /* - * Conduct a SmallRye OpenAPI annotation scan for each filtered index view, merging the resulting OpenAPI models into one. - * The AtomicReference is effectively final so we can update the actual reference from inside the lambda. - */ - AtomicReference aggregateModelRef = new AtomicReference<>(new OpenAPIImpl()); // Start with skeletal model - filteredIndexViews.forEach(filteredIndexView -> { - OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(config, filteredIndexView, - List.of(new HelidonAnnotationScannerExtension())); - OpenAPI modelForApp = scanner.scan(); - if (LOGGER.isLoggable(Level.TRACE)) { - - LOGGER.log(Level.TRACE, String.format("Intermediate model from filtered index view %s:%n%s", - filteredIndexView.getKnownClasses(), - formatDocument(Format.YAML, modelForApp))); - } - aggregateModelRef.set( - MergeUtil.merge(aggregateModelRef.get(), modelForApp) - .openapi(modelForApp.getOpenapi())); // SmallRye's merge skips openapi value. - - }); - OpenApiDocument.INSTANCE.modelFromAnnotations(aggregateModelRef.get()); - } - - private void prepareResponse(ServerRequest req, ServerResponse resp) { - - try { - MediaType resultMediaType = chooseResponseMediaType(req); - String openAPIDocument = prepareDocument(resultMediaType); - resp.status(Http.Status.OK_200); - resp.headers().add(Http.Header.CONTENT_TYPE, resultMediaType.text()); - resp.send(openAPIDocument); - } catch (Exception ex) { - resp.status(Http.Status.INTERNAL_SERVER_ERROR_500); - resp.send("Error serializing OpenAPI document; " + ex.getMessage()); - LOGGER.log(Level.ERROR, "Error serializing OpenAPI document", ex); - } - } - - private String formatDocument(Format fmt) { - return formatDocument(fmt, model()); - } - - private String formatDocument(Format fmt, OpenAPI model) { - StringWriter sw = new StringWriter(); - Serializer.serialize(HELPER.get().types(), implsToTypes, model, fmt, sw); - return sw.toString(); - - } - - private MediaType chooseResponseMediaType(ServerRequest req) { - /* - * Response media type default is application/vnd.oai.openapi (YAML) - * unless otherwise specified. - */ - Optional queryParameterFormat = req.queryParams() - .first(OPENAPI_ENDPOINT_FORMAT_QUERY_PARAMETER); - if (queryParameterFormat.isPresent()) { - String queryParameterFormatValue = queryParameterFormat.get(); - try { - return QueryParameterRequestedFormat.chooseFormat(queryParameterFormatValue).mediaType(); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException( - "Query parameter 'format' had value '" - + queryParameterFormatValue - + "' but expected " + Arrays.toString(QueryParameterRequestedFormat.values())); - } - } - - Optional requestedMediaType = req.headers() - .bestAccepted(OpenAPIMediaType.preferredOrdering()); - - MediaType resultMediaType = requestedMediaType - .orElseGet(() -> { - LOGGER.log(Level.TRACE, - () -> String.format("Did not recognize requested media type %s; responding with default %s", - req.headers().acceptedTypes(), - DEFAULT_RESPONSE_MEDIA_TYPE.text())); - return DEFAULT_RESPONSE_MEDIA_TYPE; - }); - return resultMediaType; - } - - private enum QueryParameterRequestedFormat { - JSON(MediaTypes.APPLICATION_JSON), YAML(MediaTypes.APPLICATION_OPENAPI_YAML); - - private final MediaType mt; - - QueryParameterRequestedFormat(MediaType mt) { - this.mt = mt; - } - - static QueryParameterRequestedFormat chooseFormat(String format) { - return QueryParameterRequestedFormat.valueOf(format); - } - - MediaType mediaType() { - return mt; - } - } - - /** - * Extension we want SmallRye's OpenAPI implementation to use for parsing the JSON content in Extension annotations. - */ - private static class HelidonAnnotationScannerExtension implements AnnotationScannerExtension { - - @Override - public Object parseExtension(String key, String value) { - - // Inspired by SmallRye's JsonUtil#parseValue method. - if (value == null) { - return null; - } - - value = value.trim(); - - if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) { - return Boolean.valueOf(value); - } - - // See if we should parse the value fully. - switch (value.charAt(0)) { - case '{': - case '[': - case '-': - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - try { - JsonReader reader = JSON_READER_FACTORY.createReader(new StringReader(value)); - JsonValue jsonValue = reader.readValue(); - return convertJsonValue(jsonValue); - } catch (Exception ex) { - LOGGER.log(Level.ERROR, String.format("Error parsing extension key: %s, value: %s", key, value), ex); - } - break; - - default: - break; - } - - // Treat as JSON string. - return value; - } - - private static Object convertJsonValue(JsonValue jsonValue) { - switch (jsonValue.getValueType()) { - case ARRAY: - JsonArray jsonArray = jsonValue.asJsonArray(); - return jsonArray.stream() - .map(HelidonAnnotationScannerExtension::convertJsonValue) - .collect(Collectors.toList()); - - case FALSE: - return Boolean.FALSE; - - case TRUE: - return Boolean.TRUE; - - case NULL: - return null; - - case STRING: - return JsonString.class.cast(jsonValue).getString(); - - case NUMBER: - JsonNumber jsonNumber = JsonNumber.class.cast(jsonValue); - return jsonNumber.numberValue(); - - case OBJECT: - JsonObject jsonObject = jsonValue.asJsonObject(); - return jsonObject.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, entry -> convertJsonValue(entry.getValue()))); - - default: - return jsonValue.toString(); - } - } - } - - /** - * Fluent API builder for {@link io.helidon.reactive.openapi.OpenAPISupport}. - */ - @Configured(description = "OpenAPI support configuration") - public static class Builder implements io.helidon.common.Builder { - - /** - * Config key to select the openapi node from Helidon config. - */ - public static final String CONFIG_KEY = "openapi"; - - private final OpenAPIConfigImpl.Builder apiConfigBuilder = OpenAPIConfigImpl.builder(); - private String webContext; - private String staticFilePath; - private CrossOriginConfig crossOriginConfig = null; - - private Builder() { - } - - @Override - public OpenAPISupport build() { - OpenAPISupport openAPISupport = new OpenAPISupport(this); - openAPISupport.prepareModel(); - return openAPISupport; - } - - /** - * Set various builder attributes from the specified {@code Config} object. - *

    - * The {@code Config} object can specify web-context and static-file in addition to settings - * supported by {@link io.helidon.openapi.internal.OpenAPIConfigImpl.Builder}. - * - * @param config the openapi {@code Config} object possibly containing settings - * @return updated builder instance - * @throws NullPointerException if the provided {@code Config} is null - */ - @ConfiguredOption(type = OpenApiConfig.class) - public Builder config(Config config) { - config.get("web-context") - .asString() - .ifPresent(this::webContext); - config.get("static-file") - .asString() - .ifPresent(this::staticFile); - config.get(CORS_CONFIG_KEY) - .as(CrossOriginConfig::create) - .ifPresent(this::crossOriginConfig); - return this; - } - - /** - * Makes sure the set-up for OpenAPI is consistent, internally and with - * the current Helidon runtime environment (SE or MP). - * - * @throws IllegalStateException if validation fails - */ - public void validate() throws IllegalStateException { - } - - /** - * Sets the web context path for the OpenAPI endpoint. - * - * @param path webContext to use, defaults to - * {@value DEFAULT_WEB_CONTEXT} - * @return updated builder instance - */ - @ConfiguredOption(DEFAULT_WEB_CONTEXT) - public Builder webContext(String path) { - if (!path.startsWith("/")) { - path = "/" + path; - } - this.webContext = path; - return this; - } - - /** - * Sets the file system path of the static OpenAPI document file. Default types are `json`, `yaml`, and `yml`. - * - * @param path non-null location of the static OpenAPI document file - * @return updated builder instance - */ - @ConfiguredOption(value = DEFAULT_STATIC_FILE_PATH_PREFIX + "*") - public Builder staticFile(String path) { - Objects.requireNonNull(path, "path to static file must be non-null"); - staticFilePath = path; - return this; - } - - /** - * Assigns the CORS settings for the OpenAPI endpoint. - * - * @param crossOriginConfig {@code CrossOriginConfig} containing CORS set-up - * @return updated builder instance - */ - @ConfiguredOption(key = CORS_CONFIG_KEY) - public Builder crossOriginConfig(CrossOriginConfig crossOriginConfig) { - Objects.requireNonNull(crossOriginConfig, "CrossOriginConfig must be non-null"); - this.crossOriginConfig = crossOriginConfig; - return this; - } - - /** - * Sets the app-provided model reader class. - * - * @param className name of the model reader class - * @return updated builder instance - */ - public Builder modelReader(String className) { - Objects.requireNonNull(className, "modelReader class name must be non-null"); - apiConfigBuilder.modelReader(className); - return this; - } - - /** - * Set the app-provided OpenAPI model filter class. - * - * @param className name of the filter class - * @return updated builder instance - */ - public Builder filter(String className) { - Objects.requireNonNull(className, "filter class name must be non-null"); - apiConfigBuilder.filter(className); - return this; - } - - /** - * Sets the servers which offer the endpoints in the OpenAPI document. - * - * @param serverList comma-separated list of servers - * @return updated builder instance - */ - public Builder servers(String serverList) { - Objects.requireNonNull(serverList, "serverList must be non-null"); - apiConfigBuilder.servers(serverList); - return this; - } - - /** - * Adds an operation server for a given operation ID. - * - * @param operationID operation ID to which the server corresponds - * @param operationServer name of the server to add for this operation - * @return updated builder instance - */ - public Builder addOperationServer(String operationID, String operationServer) { - Objects.requireNonNull(operationID, "operationID must be non-null"); - Objects.requireNonNull(operationServer, "operationServer must be non-null"); - apiConfigBuilder.addOperationServer(operationID, operationServer); - return this; - } - - /** - * Adds a path server for a given path. - * - * @param path path to which the server corresponds - * @param pathServer name of the server to add for this path - * @return updated builder instance - */ - public Builder addPathServer(String path, String pathServer) { - Objects.requireNonNull(path, "path must be non-null"); - Objects.requireNonNull(pathServer, "pathServer must be non-null"); - apiConfigBuilder.addPathServer(path, pathServer); - return this; - } - - /** - * Returns the supplier of index views. - * - * @return index views supplier - */ - protected Supplier> indexViewsSupplier() { - // Only in MP can we have possibly multiple index views, one per app, from scanning classes (or the Jandex index). - return List::of; - } - - /** - * Returns the smallrye OpenApiConfig instance describing the set-up - * that will govern the smallrye OpenAPI behavior. - * - * @return {@code OpenApiConfig} conveying how OpenAPI should behave - */ - OpenApiConfig openAPIConfig() { - return apiConfigBuilder.build(); - } - - /** - * Returns the web context (path) at which the OpenAPI endpoint should - * be exposed, either the most recent explicitly-set value via - * {@link #webContext(String)} or the default - * {@value #DEFAULT_WEB_CONTEXT}. - * - * @return path the web context path for the OpenAPI endpoint - */ - String webContext() { - String webContextPath = webContext == null ? DEFAULT_WEB_CONTEXT : webContext; - if (webContext == null) { - LOGGER.log(Level.DEBUG, "OpenAPI path defaulting to {0}", webContextPath); - } else { - LOGGER.log(Level.DEBUG, "OpenAPI path set to {0}", webContextPath); - } - return webContextPath; - } - - /** - * Returns the path to a static OpenAPI document file (if any exists), - * either as explicitly set using {@link #staticFile(String) } - * or one of the default files. - * - * @return the OpenAPI static file instance for the static file if such - * a file exists, null otherwise - */ - OpenApiStaticFile staticFile() { - return staticFilePath == null ? getDefaultStaticFile() : getExplicitStaticFile(); - } - - private OpenApiStaticFile getExplicitStaticFile() { - Path path = Paths.get(staticFilePath); - String specifiedFileType = typeFromPath(path); - OpenAPIMediaType specifiedMediaType = OpenAPIMediaType.byFileType(specifiedFileType) - .orElseThrow(() -> new IllegalArgumentException("OpenAPI file path " - + path.toAbsolutePath() - + " is not one of recognized types: " - + OpenAPIMediaType.recognizedFileTypes())); - - try { - InputStream is = new BufferedInputStream(Files.newInputStream(path)); - LOGGER.log(Level.DEBUG, - () -> String.format( - OPENAPI_EXPLICIT_STATIC_FILE_LOG_MESSAGE_FORMAT, - path.toAbsolutePath())); - return new OpenApiStaticFile(is, specifiedMediaType.format()); - } catch (IOException ex) { - throw new IllegalArgumentException("OpenAPI file " - + path.toAbsolutePath() - + " was specified but was not found", ex); - } - } - - private OpenApiStaticFile getDefaultStaticFile() { - List candidatePaths = LOGGER.isLoggable(Level.TRACE) ? new ArrayList<>() : null; - for (OpenAPIMediaType candidate : OpenAPIMediaType.values()) { - for (String type : candidate.matchingTypes()) { - String candidatePath = DEFAULT_STATIC_FILE_PATH_PREFIX + type; - InputStream is = null; - try { - is = getContextClassLoader().getResourceAsStream(candidatePath); - if (is != null) { - Path path = Paths.get(candidatePath); - LOGGER.log(Level.DEBUG, () -> String.format( - OPENAPI_DEFAULTED_STATIC_FILE_LOG_MESSAGE_FORMAT, - path.toAbsolutePath())); - return new OpenApiStaticFile(is, candidate.format()); - } - if (candidatePaths != null) { - candidatePaths.add(candidatePath); - } - } catch (Exception ex) { - if (is != null) { - try { - is.close(); - } catch (IOException ioex) { - ex.addSuppressed(ioex); - } - } - throw ex; - } - } - } - if (candidatePaths != null) { - LOGGER.log(Level.TRACE, - candidatePaths.stream() - .collect(Collectors.joining( - ",", - "No default static OpenAPI description file found; checked [", - "]"))); - } - return null; - } - } -} diff --git a/reactive/openapi/src/main/java/io/helidon/reactive/openapi/package-info.java b/reactive/openapi/src/main/java/io/helidon/reactive/openapi/package-info.java deleted file mode 100644 index c76c556e4b5..00000000000 --- a/reactive/openapi/src/main/java/io/helidon/reactive/openapi/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * OpenAPI integration with Helidon Reactive WebServer. - */ -package io.helidon.reactive.openapi; diff --git a/reactive/openapi/src/main/java/module-info.java b/reactive/openapi/src/main/java/module-info.java deleted file mode 100644 index ccb232a4b53..00000000000 --- a/reactive/openapi/src/main/java/module-info.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * OpenAPI integration with Helidon Reactive WebServer. - */ -module io.helidon.reactive.openapi { - requires io.helidon.common; - requires io.helidon.common.http; - requires io.helidon.config; - requires io.helidon.cors; - requires io.helidon.openapi; - requires io.helidon.reactive.media.common; - requires io.helidon.reactive.webserver; - requires io.helidon.reactive.media.jsonp; - requires io.helidon.reactive.webserver.cors; - requires smallrye.open.api.core; - requires org.jboss.jandex; - requires org.yaml.snakeyaml; - - requires static io.helidon.config.metadata; - - exports io.helidon.reactive.openapi; -} \ No newline at end of file diff --git a/reactive/openapi/src/test/java/io/helidon/reactive/openapi/ServerModelReaderTest.java b/reactive/openapi/src/test/java/io/helidon/reactive/openapi/ServerModelReaderTest.java deleted file mode 100644 index 1126bdf099b..00000000000 --- a/reactive/openapi/src/test/java/io/helidon/reactive/openapi/ServerModelReaderTest.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.reactive.openapi; - -import java.net.HttpURLConnection; - -import io.helidon.common.media.type.MediaTypes; -import io.helidon.config.Config; -import io.helidon.config.ConfigSources; -import io.helidon.reactive.openapi.test.MyModelReader; -import io.helidon.reactive.webserver.WebServer; - -import jakarta.json.JsonException; -import jakarta.json.JsonString; -import jakarta.json.JsonStructure; -import jakarta.json.JsonValue; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -/** - * Makes sure that the app-supplied model reader participates in constructing - * the OpenAPI model. - */ -public class ServerModelReaderTest { - - private static final String SIMPLE_PROPS_PATH = "/openapi"; - - private static final OpenAPISupport.Builder OPENAPI_SUPPORT_BUILDER = - OpenAPISupport.builder() - .config(Config.create(ConfigSources.classpath("simple.properties")).get(OpenAPISupport.Builder.CONFIG_KEY)); - - private static WebServer webServer; - - @BeforeAll - public static void startup() { - webServer = TestUtil.startServer(OPENAPI_SUPPORT_BUILDER); - } - - @AfterAll - public static void shutdown() { - TestUtil.shutdownServer(webServer); - } - - @Test - @Disabled - public void checkCustomModelReader() throws Exception { - HttpURLConnection cnx = TestUtil.getURLConnection( - webServer.port(), - "GET", - SIMPLE_PROPS_PATH, - MediaTypes.APPLICATION_OPENAPI_JSON); - TestUtil.validateResponseMediaType(cnx, MediaTypes.APPLICATION_OPENAPI_JSON); - JsonStructure json = TestUtil.jsonFromResponse(cnx); - // The model reader adds the following key/value (among others) to the model. - JsonValue v = json.getValue(String.format("/paths/%s/get/summary", - TestUtil.escapeForJsonPointer(MyModelReader.MODEL_READER_PATH))); - if (v.getValueType().equals(JsonValue.ValueType.STRING)) { - JsonString s = (JsonString) v; - assertThat("Unexpected summary value as added by model reader", - s.getString(), is(MyModelReader.SUMMARY)); - } - } - - @Test - public void makeSureFilteredPathIsMissing() throws Exception { - HttpURLConnection cnx = TestUtil.getURLConnection( - webServer.port(), - "GET", - SIMPLE_PROPS_PATH, - MediaTypes.APPLICATION_OPENAPI_JSON); - TestUtil.validateResponseMediaType(cnx, MediaTypes.APPLICATION_OPENAPI_JSON); - JsonStructure json = TestUtil.jsonFromResponse(cnx); - /* - * Although the model reader adds this path, the filter should have - * removed it. - */ - final JsonException ex = assertThrows( - JsonException.class, - () -> { - JsonValue v = json.getValue(String.format("/paths/%s/get/summary", - TestUtil.escapeForJsonPointer(MyModelReader.DOOMED_PATH))); - }); - assertThat(ex.getMessage(), containsString( - String.format("contains no mapping for the name '%s'", MyModelReader.DOOMED_PATH))); - } -} diff --git a/reactive/openapi/src/test/java/io/helidon/reactive/openapi/TestCors.java b/reactive/openapi/src/test/java/io/helidon/reactive/openapi/TestCors.java deleted file mode 100644 index 2d9b6d6747e..00000000000 --- a/reactive/openapi/src/test/java/io/helidon/reactive/openapi/TestCors.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.reactive.openapi; - -import java.net.HttpURLConnection; - -import io.helidon.common.http.Http; -import io.helidon.common.media.type.MediaTypes; -import io.helidon.config.Config; -import io.helidon.reactive.webserver.WebServer; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import static io.helidon.reactive.openapi.ServerTest.GREETING_OPENAPI_SUPPORT_BUILDER; -import static io.helidon.reactive.openapi.ServerTest.TIME_OPENAPI_SUPPORT_BUILDER; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -@Disabled -public class TestCors { - - private static WebServer greetingWebServer; - private static WebServer timeWebServer; - - private static final String GREETING_PATH = "/openapi-greeting"; - private static final String TIME_PATH = "/openapi-time"; - - @BeforeAll - public static void startup() { - greetingWebServer = TestUtil.startServer(GREETING_OPENAPI_SUPPORT_BUILDER); - timeWebServer = TestUtil.startServer(TIME_OPENAPI_SUPPORT_BUILDER); - } - @Test - public void testCrossOriginGreetingWithoutCors() throws Exception { - HttpURLConnection cnx = TestUtil.getURLConnection( - greetingWebServer.port(), - "GET", - GREETING_PATH, - MediaTypes.APPLICATION_OPENAPI_YAML); - cnx.setRequestProperty("Origin", "http://foo.bar"); - cnx.setRequestProperty("Host", "localhost"); - - Config c = TestUtil.configFromResponse(cnx); - - assertThat(cnx.getResponseCode(), is(Http.Status.OK_200.code())); - } - - @Test - public void testTimeRestrictedCorsValidOrigin() throws Exception { - HttpURLConnection cnx = TestUtil.getURLConnection( - timeWebServer.port(), - "GET", - TIME_PATH, - MediaTypes.APPLICATION_OPENAPI_YAML); - cnx.setRequestProperty("Origin", "http://foo.bar"); - cnx.setRequestProperty("Host", "localhost"); - - assertThat(cnx.getResponseCode(), is(Http.Status.OK_200.code())); - } - - @Test - public void testTimeRestrictedCorsInvalidOrigin() throws Exception { - HttpURLConnection cnx = TestUtil.getURLConnection( - timeWebServer.port(), - "GET", - TIME_PATH, - MediaTypes.APPLICATION_OPENAPI_YAML); - cnx.setRequestProperty("Origin", "http://other.com"); - cnx.setRequestProperty("Host", "localhost"); - - assertThat(cnx.getResponseCode(), is(Http.Status.FORBIDDEN_403.code())); - } -} diff --git a/reactive/pom.xml b/reactive/pom.xml index 511a8acda43..0a9fc488561 100644 --- a/reactive/pom.xml +++ b/reactive/pom.xml @@ -44,7 +44,6 @@ webclient webserver service-common - openapi graphql diff --git a/reactive/webclient/jaxrs/pom.xml b/reactive/webclient/jaxrs/pom.xml deleted file mode 100644 index 60a0f9d0f7e..00000000000 --- a/reactive/webclient/jaxrs/pom.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - helidon-reactive-webclient-project - io.helidon.reactive.webclient - 4.0.0-SNAPSHOT - - 4.0.0 - - helidon-reactive-webclient-jaxrs - Helidon WebClient JAX-RS - - - - io.helidon.jersey - helidon-jersey-client - - - io.helidon.jersey - helidon-jersey-common - - - io.helidon.common - helidon-common-context - - - io.helidon.config - helidon-config - - - io.helidon.common - helidon-common-configurable - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - --enable-preview - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - --enable-preview - - - - - - diff --git a/reactive/webclient/jaxrs/src/main/java/io/helidon/reactive/webclient/jaxrs/JaxRsClient.java b/reactive/webclient/jaxrs/src/main/java/io/helidon/reactive/webclient/jaxrs/JaxRsClient.java deleted file mode 100644 index 31d02e15102..00000000000 --- a/reactive/webclient/jaxrs/src/main/java/io/helidon/reactive/webclient/jaxrs/JaxRsClient.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.reactive.webclient.jaxrs; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; - -import io.helidon.common.config.Config; -import io.helidon.common.configurable.ThreadPoolSupplier; -import io.helidon.common.context.Contexts; - -/** - * Point of access to {@link jakarta.ws.rs.client.ClientBuilder} to support Helidon features, - * such as propagation of tracing, correct handling of {@link io.helidon.common.context.Context}. - */ -public final class JaxRsClient { - private static final AtomicReference> EXECUTOR_SUPPLIER = - new AtomicReference<>(ThreadPoolSupplier.builder() - .threadNamePrefix("helidon-jaxrs-client-") - .build()); - - private JaxRsClient() { - } - - /** - * Configure defaults for all clients created. - * Configuration options: - * - * - * - * - * - * - * - * - * - * - * - * - * - *
    Configuration parameters
    keydefault valuedescription
    executor{@link io.helidon.common.configurable.ThreadPoolSupplier#create(io.helidon.common.config.Config, String)}Default executor service to use for asynchronous operations. For configuration options - * of {@code executor}, please refer to - * {@link io.helidon.common.configurable.ThreadPoolSupplier.Builder#config(io.helidon.common.config.Config)}
    - * - * @param config configuration to use to configure JAX-RS clients defaults - */ - public static void configureDefaults(Config config) { - EXECUTOR_SUPPLIER.set(ThreadPoolSupplier.create(config, "jaxrs-client-thread-pool")); - } - - /** - * Configure the default executor supplier to be used for asynchronous requests when explicit supplier is not - * provided. - * - * @param executorServiceSupplier supplier that provides the executor service - */ - public static void defaultExecutor(Supplier executorServiceSupplier) { - Supplier wrapped = () -> Contexts.wrap(executorServiceSupplier.get()); - - EXECUTOR_SUPPLIER.set(wrapped); - } - - /** - * The executor supplier configured as default. - * - * @return supplier of {@link java.util.concurrent.ExecutorService} to use for client - * asynchronous operations - */ - static Supplier executor() { - return EXECUTOR_SUPPLIER.get(); - } -} diff --git a/reactive/webclient/jaxrs/src/main/java/io/helidon/reactive/webclient/jaxrs/JerseyClientAutoDiscoverable.java b/reactive/webclient/jaxrs/src/main/java/io/helidon/reactive/webclient/jaxrs/JerseyClientAutoDiscoverable.java deleted file mode 100644 index 64bbaf9f59b..00000000000 --- a/reactive/webclient/jaxrs/src/main/java/io/helidon/reactive/webclient/jaxrs/JerseyClientAutoDiscoverable.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.reactive.webclient.jaxrs; - -import java.lang.System.Logger.Level; -import java.util.concurrent.ExecutorService; - -import jakarta.annotation.Priority; -import jakarta.ws.rs.ConstrainedTo; -import jakarta.ws.rs.RuntimeType; -import jakarta.ws.rs.core.Configuration; -import jakarta.ws.rs.core.FeatureContext; -import org.glassfish.jersey.client.ClientAsyncExecutor; -import org.glassfish.jersey.internal.spi.AutoDiscoverable; -import org.glassfish.jersey.model.internal.CommonConfig; -import org.glassfish.jersey.spi.ExecutorServiceProvider; - -import static org.glassfish.jersey.CommonProperties.PROVIDER_DEFAULT_DISABLE; - -/** - * Auto discoverable feature to use a custom executor service - * for all client asynchronous operations. - * This is needed to support {@link io.helidon.common.context.Context} for - * outbound calls. - * Also disables default providers, unless configured by user. - */ -@ConstrainedTo(RuntimeType.CLIENT) -@Priority(121) -public class JerseyClientAutoDiscoverable implements AutoDiscoverable { - private static final System.Logger LOGGER = System.getLogger(JerseyClientAutoDiscoverable.class.getName()); - - @Override - public void configure(FeatureContext context) { - context.register(new EsProvider()); - Configuration jaxRsConfiguration = context.getConfiguration(); - if (jaxRsConfiguration instanceof CommonConfig) { - // this should be always true, as we are in Jersey - CommonConfig configuration = (CommonConfig) jaxRsConfiguration; - - Object property = configuration.getProperty(PROVIDER_DEFAULT_DISABLE); - if (null == property) { - LOGGER.log(Level.DEBUG, "Disabling all Jersey default providers (DOM, SAX, Rendered Image, XML Source, and " - + "XML Stream Source). You can enabled them by setting system property " - + PROVIDER_DEFAULT_DISABLE + " to NONE"); - configuration.property(PROVIDER_DEFAULT_DISABLE, "ALL"); - } else if ("NONE".equals(property)) { - configuration.property(PROVIDER_DEFAULT_DISABLE, null); - } - } - } - - @ClientAsyncExecutor - private static final class EsProvider implements ExecutorServiceProvider { - @Override - public ExecutorService getExecutorService() { - return JaxRsClient.executor().get(); - } - - @Override - public void dispose(ExecutorService executorService) { - // no-op, as we use a shared executor instance - } - } -} diff --git a/reactive/webclient/jaxrs/src/main/java/io/helidon/reactive/webclient/jaxrs/package-info.java b/reactive/webclient/jaxrs/src/main/java/io/helidon/reactive/webclient/jaxrs/package-info.java deleted file mode 100644 index 42346d992a3..00000000000 --- a/reactive/webclient/jaxrs/src/main/java/io/helidon/reactive/webclient/jaxrs/package-info.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Helidon support for JAX-RS (Jersey) client. - * You can create the JAX-RS client as usual using {@link jakarta.ws.rs.client.ClientBuilder#newBuilder()} - * and {@link jakarta.ws.rs.client.ClientBuilder#newClient()}. - *

    - * If you want to configure defaults for asynchronous executor service, - * you can use {@link io.helidon.reactive.webclient.jaxrs.JaxRsClient#configureDefaults(io.helidon.config.Config)} - * or {@link io.helidon.reactive.webclient.jaxrs.JaxRsClient#defaultExecutor(java.util.function.Supplier)}. - * - * @see io.helidon.reactive.webclient.jaxrs.JaxRsClient - */ -package io.helidon.reactive.webclient.jaxrs; diff --git a/reactive/webclient/jaxrs/src/main/java/module-info.java b/reactive/webclient/jaxrs/src/main/java/module-info.java deleted file mode 100644 index 3ee24259e1e..00000000000 --- a/reactive/webclient/jaxrs/src/main/java/module-info.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import io.helidon.reactive.webclient.jaxrs.JerseyClientAutoDiscoverable; - -import org.glassfish.jersey.internal.spi.AutoDiscoverable; - -/** - * Basic integration with JAX-RS client. - */ -module io.helidon.reactive.webclient.jaxrs { - requires jakarta.annotation; - - requires jakarta.ws.rs; - requires io.helidon.jersey.client; - requires io.helidon.jersey.common; - - requires io.helidon.common; - requires io.helidon.common.configurable; - requires io.helidon.common.context; - - exports io.helidon.reactive.webclient.jaxrs; - - provides AutoDiscoverable with JerseyClientAutoDiscoverable; -} diff --git a/reactive/webclient/pom.xml b/reactive/webclient/pom.xml index 89c296fe20e..c6669cb8431 100644 --- a/reactive/webclient/pom.xml +++ b/reactive/webclient/pom.xml @@ -34,7 +34,6 @@ pom - jaxrs webclient metrics security diff --git a/reactive/webclient/security/src/main/java/io/helidon/reactive/webclient/security/WebClientSecurity.java b/reactive/webclient/security/src/main/java/io/helidon/reactive/webclient/security/WebClientSecurity.java index b35f1fb4d3c..11cfe8fa815 100644 --- a/reactive/webclient/security/src/main/java/io/helidon/reactive/webclient/security/WebClientSecurity.java +++ b/reactive/webclient/security/src/main/java/io/helidon/reactive/webclient/security/WebClientSecurity.java @@ -145,8 +145,10 @@ public Single request(WebClientServiceRequest request) throw e; } - return Single.create(clientBuilder.submit() - .thenApply(providerResponse -> processResponse(request, span, providerResponse))); + return Single.create(() -> { + OutboundSecurityResponse providerResponse = clientBuilder.submit(); + return processResponse(request, span, providerResponse); + }); } private WebClientServiceRequest processResponse(WebClientServiceRequest request, diff --git a/security/integration/grpc/src/main/java/io/helidon/security/integration/grpc/GrpcSecurityHandler.java b/security/integration/grpc/src/main/java/io/helidon/security/integration/grpc/GrpcSecurityHandler.java index 3f4ed451ffc..ef5e54b742b 100644 --- a/security/integration/grpc/src/main/java/io/helidon/security/integration/grpc/GrpcSecurityHandler.java +++ b/security/integration/grpc/src/main/java/io/helidon/security/integration/grpc/GrpcSecurityHandler.java @@ -25,8 +25,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; @@ -367,34 +365,31 @@ private ServerCall.Listener processSecurity(SecurityContext .customObjects(customObjects.orElse(new ClassToInstanceStore<>())) .build()); - CompletionStage stage = processAuthentication(call, headers, securityContext, tracing.atnTracing()) - .thenCompose(atnResult -> { - if (atnResult.proceed) { - // authentication was OK or disabled, we should continue - return processAuthorization(securityContext, tracing.atzTracing()); - } else { - // authentication told us to stop processing - return CompletableFuture.completedFuture(AtxResult.STOP); - } - }) - .thenApply(atzResult -> { - if (atzResult.proceed) { - // authorization was OK, we can continue processing - tracing.logProceed(); - tracing.finish(); - return true; - } else { - tracing.logDeny(); - tracing.finish(); - return false; - } - }); - ServerCall.Listener listener; CallWrapper callWrapper = new CallWrapper<>(call); try { - boolean proceed = stage.toCompletableFuture().get(); + AtxResult atnResult = processAuthentication(call, headers, securityContext, tracing.atnTracing()); + AtxResult atzResult; + if (atnResult.proceed) { + // authentication was OK or disabled, we should continue + atzResult = processAuthorization(securityContext, tracing.atzTracing()); + } else { + // authentication told us to stop processing + atzResult = AtxResult.STOP; + } + boolean proceed; + if (atzResult.proceed) { + // authorization was OK, we can continue processing + tracing.logProceed(); + tracing.finish(); + proceed = true; + } else { + tracing.logDeny(); + tracing.finish(); + proceed = false; + } + if (proceed) { listener = next.startCall(callWrapper, headers); @@ -441,59 +436,51 @@ private void processAudit(ServerCall call, securityContext.audit(auditEvent); } - private CompletionStage processAuthentication(ServerCall call, - Metadata headers, - SecurityContext securityContext, - AtnTracing atnTracing) { + private AtxResult processAuthentication(ServerCall call, + Metadata headers, + SecurityContext securityContext, + AtnTracing atnTracing) { if (!authenticate.orElse(false)) { - return CompletableFuture.completedFuture(AtxResult.PROCEED); + return AtxResult.PROCEED; } - CompletableFuture future = new CompletableFuture<>(); - SecurityClientBuilder clientBuilder = securityContext.atnClientBuilder(); configureSecurityRequest(clientBuilder, atnTracing.findParent().orElse(null)); - clientBuilder.explicitProvider(explicitAuthenticator.orElse(null)).submit().thenAccept(response -> { + try { + AuthenticationResponse response = clientBuilder.explicitProvider(explicitAuthenticator.orElse(null)).submit(); switch (response.status()) { case SUCCESS: //everything is fine, we can continue with processing break; case FAILURE_FINISH: - if (atnFinishFailure(future)) { + if (atnFinishFailure()) { atnSpanFinish(atnTracing, response); - return; + return AtxResult.STOP; } break; case SUCCESS_FINISH: - atnFinish(future); atnSpanFinish(atnTracing, response); - return; + return AtxResult.STOP; case ABSTAIN: case FAILURE: - if (atnAbstainFailure(future)) { + if (atnAbstainFailure()) { atnSpanFinish(atnTracing, response); - return; + return AtxResult.STOP; } break; default: - Exception e = new SecurityException("Invalid SecurityStatus returned: " + response.status()); - future.completeExceptionally(e); - atnTracing.error(e); - return; + throw new SecurityException("Invalid SecurityStatus returned: " + response.status()); } atnSpanFinish(atnTracing, response); - future.complete(new AtxResult(clientBuilder.buildRequest())); - }).exceptionally(throwable -> { - atnTracing.error(throwable); - future.completeExceptionally(throwable); - return null; - }); - - return future; + return new AtxResult(clientBuilder.buildRequest()); + } catch (Exception e) { + atnTracing.error(e); + throw e; + } } private void atnSpanFinish(AtnTracing atnTracing, AuthenticationResponse response) { @@ -504,31 +491,24 @@ private void atnSpanFinish(AtnTracing atnTracing, AuthenticationResponse respons atnTracing.finish(); } - private boolean atnAbstainFailure(CompletableFuture future) { + private boolean atnAbstainFailure() { if (authenticationOptional.orElse(false)) { LOGGER.log(Level.TRACE, "Authentication failed, but was optional, so assuming anonymous"); return false; } - - future.complete(AtxResult.STOP); return true; } - private boolean atnFinishFailure(CompletableFuture future) { + private boolean atnFinishFailure() { if (authenticationOptional.orElse(false)) { LOGGER.log(Level.TRACE, "Authentication failed, but was optional, so assuming anonymous"); return false; } else { - future.complete(AtxResult.STOP); return true; } } - private void atnFinish(CompletableFuture future) { - future.complete(AtxResult.STOP); - } - private void configureSecurityRequest(SecurityRequestBuilder> request, SpanContext parentSpanContext) { @@ -536,16 +516,11 @@ private void configureSecurityRequest(SecurityRequestBuilder processAuthorization( - SecurityContext context, - AtzTracing atzTracing) { - CompletableFuture future = new CompletableFuture<>(); - + private AtxResult processAuthorization(SecurityContext context, AtzTracing atzTracing) { if (!authorize.orElse(false)) { - future.complete(AtxResult.PROCEED); atzTracing.logStatus(SecurityResponse.SecurityStatus.ABSTAIN); atzTracing.finish(); - return future; + return AtxResult.PROCEED; } Set rolesSet = rolesAllowed.orElse(Set.of()); @@ -554,15 +529,13 @@ private CompletionStage processAuthorization( // first validate roles - RBAC is supported out of the box by security, no need to invoke provider if (explicitAuthorizer.isPresent()) { if (rolesSet.stream().noneMatch(role -> context.isUserInRole(role, explicitAuthorizer.get()))) { - future.complete(AtxResult.STOP); atzTracing.finish(); - return future; + return AtxResult.STOP; } } else { if (rolesSet.stream().noneMatch(context::isUserInRole)) { - future.complete(AtxResult.STOP); atzTracing.finish(); - return future; + return AtxResult.STOP; } } } @@ -573,7 +546,8 @@ private CompletionStage processAuthorization( configureSecurityRequest(client, atzTracing.findParent().orElse(null)); - client.explicitProvider(explicitAuthorizer.orElse(null)).submit().thenAccept(response -> { + try { + AuthorizationResponse response = client.explicitProvider(explicitAuthorizer.orElse(null)).submit(); atzTracing.logStatus(response.status()); switch (response.status()) { case SUCCESS: @@ -581,31 +555,21 @@ private CompletionStage processAuthorization( break; case FAILURE_FINISH: case SUCCESS_FINISH: - atzTracing.finish(); - future.complete(AtxResult.STOP); - return; case ABSTAIN: case FAILURE: atzTracing.finish(); - future.complete(AtxResult.STOP); - return; + return AtxResult.STOP; default: - SecurityException e = new SecurityException("Invalid SecurityStatus returned: " + response.status()); - atzTracing.error(e); - future.completeExceptionally(e); - return; + throw new SecurityException("Invalid SecurityStatus returned: " + response.status()); } atzTracing.finish(); // everything was OK - future.complete(AtxResult.PROCEED); - }).exceptionally(throwable -> { - atzTracing.error(throwable); - future.completeExceptionally(throwable); - return null; - }); - - return future; + return AtxResult.PROCEED; + } catch (Exception e) { + atzTracing.error(e); + throw e; + } } /** diff --git a/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/OutboundSecurityIT.java b/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/OutboundSecurityIT.java index e86f854d3df..86bc78b42f9 100644 --- a/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/OutboundSecurityIT.java +++ b/security/integration/grpc/src/test/java/io/helidon/security/integration/grpc/OutboundSecurityIT.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/security/integration/jersey-client/pom.xml b/security/integration/jersey-client/pom.xml index e65cacac90e..79aaf1987e7 100644 --- a/security/integration/jersey-client/pom.xml +++ b/security/integration/jersey-client/pom.xml @@ -58,10 +58,6 @@ helidon-jersey-client provided - - io.helidon.reactive.webclient - helidon-reactive-webclient-jaxrs - org.junit.jupiter junit-jupiter-api diff --git a/security/integration/jersey-client/src/main/java/module-info.java b/security/integration/jersey-client/src/main/java/module-info.java index 7db0ee8df06..394d74bf1ae 100644 --- a/security/integration/jersey-client/src/main/java/module-info.java +++ b/security/integration/jersey-client/src/main/java/module-info.java @@ -30,7 +30,6 @@ requires io.helidon.common.uri; requires io.helidon.jersey.common; requires io.helidon.security.integration.common; - requires io.helidon.reactive.webclient.jaxrs; requires jersey.common; requires jersey.client; diff --git a/security/integration/jersey/pom.xml b/security/integration/jersey/pom.xml index a851b7c5a51..8397b6190e8 100644 --- a/security/integration/jersey/pom.xml +++ b/security/integration/jersey/pom.xml @@ -73,10 +73,6 @@ helidon-jersey-client provided - - io.helidon.reactive.webclient - helidon-reactive-webclient-jaxrs - io.helidon.jersey helidon-jersey-server diff --git a/security/integration/jersey/src/main/java/module-info.java b/security/integration/jersey/src/main/java/module-info.java index e1581fbfdc9..551f1281746 100644 --- a/security/integration/jersey/src/main/java/module-info.java +++ b/security/integration/jersey/src/main/java/module-info.java @@ -42,7 +42,6 @@ requires io.helidon.jersey.server; requires io.helidon.jersey.client; requires io.helidon.security.integration.common; - requires io.helidon.reactive.webclient.jaxrs; requires jakarta.inject; diff --git a/security/integration/jersey/src/test/java/io/helidon/security/integration/jersey/BindingTestProvider.java b/security/integration/jersey/src/test/java/io/helidon/security/integration/jersey/BindingTestProvider.java index 5ec32f5627a..f3096179035 100644 --- a/security/integration/jersey/src/test/java/io/helidon/security/integration/jersey/BindingTestProvider.java +++ b/security/integration/jersey/src/test/java/io/helidon/security/integration/jersey/BindingTestProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,15 +29,14 @@ import io.helidon.security.spi.AuthenticationProvider; import io.helidon.security.spi.AuthorizationProvider; import io.helidon.security.spi.OutboundSecurityProvider; -import io.helidon.security.spi.SynchronousProvider; /** * Simple authorization provider, denying access to "deny" path. */ -public class BindingTestProvider extends SynchronousProvider +public class BindingTestProvider implements AuthorizationProvider, AuthenticationProvider, OutboundSecurityProvider { @Override - protected AuthorizationResponse syncAuthorize(ProviderRequest providerRequest) { + public AuthorizationResponse authorize(ProviderRequest providerRequest) { String path = providerRequest .env().path().orElseThrow(() -> new IllegalArgumentException("Path is a required parameter")); if ("/deny".equals(path)) { @@ -47,7 +46,7 @@ protected AuthorizationResponse syncAuthorize(ProviderRequest providerRequest) { } @Override - protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { List strings = providerRequest.env().headers().get("x-user"); if (null == strings) { @@ -57,9 +56,9 @@ protected AuthenticationResponse syncAuthenticate(ProviderRequest providerReques } @Override - protected OutboundSecurityResponse syncOutbound(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundEndpointConfig) { + public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv, + EndpointConfig outboundEndpointConfig) { return providerRequest.securityContext() .user() .map(user -> OutboundSecurityResponse diff --git a/security/integration/jersey/src/test/java/io/helidon/security/integration/jersey/OptionalSecurityTest.java b/security/integration/jersey/src/test/java/io/helidon/security/integration/jersey/OptionalSecurityTest.java index 901939fd12e..f15fdaee80b 100644 --- a/security/integration/jersey/src/test/java/io/helidon/security/integration/jersey/OptionalSecurityTest.java +++ b/security/integration/jersey/src/test/java/io/helidon/security/integration/jersey/OptionalSecurityTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -143,13 +143,11 @@ public Set> getClasses() { }; } - private static CompletionStage authenticate(ProviderRequest request) { - AuthenticationResponse res = AuthenticationResponse.builder() + private static AuthenticationResponse authenticate(ProviderRequest request) { + return AuthenticationResponse.builder() .status(SecurityResponse.SecurityStatus.FAILURE_FINISH) .statusCode(301) .build(); - - return CompletableFuture.completedFuture(res); } @Path("/") diff --git a/security/integration/nima/src/main/java/io/helidon/security/integration/nima/SecurityHandler.java b/security/integration/nima/src/main/java/io/helidon/security/integration/nima/SecurityHandler.java index 5ef5ed0c161..f1963ec11b9 100644 --- a/security/integration/nima/src/main/java/io/helidon/security/integration/nima/SecurityHandler.java +++ b/security/integration/nima/src/main/java/io/helidon/security/integration/nima/SecurityHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,8 +26,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.logging.Level; @@ -523,14 +521,11 @@ private void processSecurity(SecurityContext securityContext, ServerRequest req, .build()); try { - AtxResult atnResult = processAuthentication(res, securityContext, tracing.atnTracing()).toCompletableFuture() - .get(); + AtxResult atnResult = processAuthentication(res, securityContext, tracing.atnTracing()); AtxResult atzResult; if (atnResult.proceed) { - atzResult = processAuthorization(req, res, securityContext, tracing.atzTracing()) - .toCompletableFuture() - .get(); + atzResult = processAuthorization(req, res, securityContext, tracing.atzTracing()); } else { atzResult = AtxResult.STOP; } @@ -609,20 +604,19 @@ private void processAudit(ServerRequest req, ServerResponse res, SecurityContext securityContext.audit(auditEvent); } - private CompletionStage processAuthentication(ServerResponse res, - SecurityContext securityContext, - AtnTracing atnTracing) { + private AtxResult processAuthentication(ServerResponse res, + SecurityContext securityContext, + AtnTracing atnTracing) { if (!authenticate.orElse(false)) { - return CompletableFuture.completedFuture(AtxResult.PROCEED); + return AtxResult.PROCEED; } - CompletableFuture future = new CompletableFuture<>(); - SecurityClientBuilder clientBuilder = securityContext.atnClientBuilder(); configureSecurityRequest(clientBuilder, atnTracing.findParent().orElse(null)); - clientBuilder.explicitProvider(explicitAuthenticator.orElse(null)).submit().thenAccept(response -> { + try { + AuthenticationResponse response = clientBuilder.explicitProvider(explicitAuthenticator.orElse(null)).submit(); // copy headers to be returned with the current response response.responseHeaders() .forEach((key, value) -> res.headers().set(Http.Header.create(Http.Header.create(key), value))); @@ -632,38 +626,32 @@ private CompletionStage processAuthentication(ServerResponse res, //everything is fine, we can continue with processing break; case FAILURE_FINISH: - if (atnFinishFailure(res, future, response)) { + if (atnFinishFailure(res, response)) { atnSpanFinish(atnTracing, response); - return; + return AtxResult.STOP; } break; case SUCCESS_FINISH: - atnFinish(res, future, response); + atnFinish(res, response); atnSpanFinish(atnTracing, response); - return; + return AtxResult.STOP; case ABSTAIN: case FAILURE: - if (atnAbstainFailure(res, future, response)) { + if (atnAbstainFailure(res, response)) { atnSpanFinish(atnTracing, response); - return; + return AtxResult.STOP; } break; default: - Exception e = new SecurityException("Invalid SecurityStatus returned: " + response.status()); - future.completeExceptionally(e); - atnTracing.error(e); - return; + throw new SecurityException("Invalid SecurityStatus returned: " + response.status()); } atnSpanFinish(atnTracing, response); - future.complete(new AtxResult(clientBuilder.buildRequest())); - }).exceptionally(throwable -> { - atnTracing.error(throwable); - future.completeExceptionally(throwable); - return null; - }); - - return future; + return new AtxResult(clientBuilder.buildRequest()); + } catch (Exception e) { + atnTracing.error(e); + throw e; + } } private void atnSpanFinish(AtnTracing atnTracing, AuthenticationResponse response) { @@ -674,9 +662,7 @@ private void atnSpanFinish(AtnTracing atnTracing, AuthenticationResponse respons atnTracing.finish(); } - private boolean atnAbstainFailure(ServerResponse res, - CompletableFuture future, - AuthenticationResponse response) { + private boolean atnAbstainFailure(ServerResponse res, AuthenticationResponse response) { if (authenticationOptional.orElse(false)) { LOGGER.finest("Authentication failed, but was optional, so assuming anonymous"); return false; @@ -687,14 +673,10 @@ private boolean atnAbstainFailure(ServerResponse res, Http.Status.UNAUTHORIZED_401.code(), Map.of(Http.Header.WWW_AUTHENTICATE, List.of("Basic realm=\"Security Realm\""))); - future.complete(AtxResult.STOP); return true; } - private boolean atnFinishFailure(ServerResponse res, - CompletableFuture future, - AuthenticationResponse response) { - + private boolean atnFinishFailure(ServerResponse res, AuthenticationResponse response) { if (authenticationOptional.orElse(false)) { LOGGER.finest("Authentication failed, but was optional, so assuming anonymous"); return false; @@ -702,19 +684,13 @@ private boolean atnFinishFailure(ServerResponse res, int defaultStatusCode = Http.Status.UNAUTHORIZED_401.code(); abortRequest(res, response, defaultStatusCode, Map.of()); - future.complete(AtxResult.STOP); return true; } } - private void atnFinish(ServerResponse res, - CompletableFuture future, - AuthenticationResponse response) { - + private void atnFinish(ServerResponse res, AuthenticationResponse response) { int defaultStatusCode = Http.Status.OK_200.code(); - abortRequest(res, response, defaultStatusCode, Map.of()); - future.complete(AtxResult.STOP); } private void abortRequest(ServerResponse res, @@ -752,18 +728,15 @@ private void configureSecurityRequest(SecurityRequestBuilder processAuthorization(ServerRequest req, - ServerResponse res, - SecurityContext context, - AtzTracing atzTracing) { - CompletableFuture future = new CompletableFuture<>(); + private AtxResult processAuthorization(ServerRequest req, + ServerResponse res, + SecurityContext context, + AtzTracing atzTracing) { if (!authorize.orElse(false)) { - future.complete(AtxResult.PROCEED); atzTracing.logStatus(SecurityResponse.SecurityStatus.ABSTAIN); atzTracing.finish(); - return future; + return AtxResult.PROCEED; } Set rolesSet = rolesAllowed.orElse(Set.of()); @@ -777,17 +750,15 @@ private CompletionStage processAuthorization(ServerRequest req, if (rolesSet.stream().noneMatch(role -> context.isUserInRole(role, explicitAuthorizer.get()))) { auditRoleMissing(context, req.path(), context.user(), rolesSet); abortRequest(res, null, Http.Status.FORBIDDEN_403.code(), Map.of()); - future.complete(AtxResult.STOP); atzTracing.finish(); - return future; + return AtxResult.STOP; } } else { if (rolesSet.stream().noneMatch(context::isUserInRole)) { auditRoleMissing(context, req.path(), context.user(), rolesSet); abortRequest(res, null, Http.Status.FORBIDDEN_403.code(), Map.of()); - future.complete(AtxResult.STOP); atzTracing.finish(); - return future; + return AtxResult.STOP; } } } @@ -798,7 +769,8 @@ private CompletionStage processAuthorization(ServerRequest req, configureSecurityRequest(client, atzTracing.findParent().orElse(null)); - client.explicitProvider(explicitAuthorizer.orElse(null)).submit().thenAccept(response -> { + try { + AuthorizationResponse response = client.explicitProvider(explicitAuthorizer.orElse(null)).submit(); atzTracing.logStatus(response.status()); switch (response.status()) { @@ -813,31 +785,23 @@ private CompletionStage processAuthorization(ServerRequest req, atzTracing.finish(); abortRequest(res, response, defaultStatus, Map.of()); - future.complete(AtxResult.STOP); - return; + return AtxResult.STOP; case ABSTAIN: case FAILURE: atzTracing.finish(); abortRequest(res, response, Http.Status.FORBIDDEN_403.code(), Map.of()); - future.complete(AtxResult.STOP); - return; + return AtxResult.STOP; default: - SecurityException e = new SecurityException("Invalid SecurityStatus returned: " + response.status()); - atzTracing.error(e); - future.completeExceptionally(e); - return; + throw new SecurityException("Invalid SecurityStatus returned: " + response.status()); } atzTracing.finish(); // everything was OK - future.complete(AtxResult.PROCEED); - }).exceptionally(throwable -> { - atzTracing.error(throwable); - future.completeExceptionally(throwable); - return null; - }); - - return future; + return AtxResult.PROCEED; + } catch (Exception e) { + atzTracing.error(e); + throw e; + } } private void auditRoleMissing(SecurityContext context, diff --git a/security/integration/webserver/pom.xml b/security/integration/webserver/pom.xml index 489fe24169c..9997737482f 100644 --- a/security/integration/webserver/pom.xml +++ b/security/integration/webserver/pom.xml @@ -51,6 +51,12 @@ io.helidon.security helidon-security-util + + io.helidon.config + helidon-config-metadata + provided + true + io.helidon.common.features helidon-common-features-api diff --git a/security/integration/webserver/src/main/java/io/helidon/security/integration/webserver/SecurityHandler.java b/security/integration/webserver/src/main/java/io/helidon/security/integration/webserver/SecurityHandler.java index 5175d81c7f9..54ecff13891 100644 --- a/security/integration/webserver/src/main/java/io/helidon/security/integration/webserver/SecurityHandler.java +++ b/security/integration/webserver/src/main/java/io/helidon/security/integration/webserver/SecurityHandler.java @@ -26,8 +26,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.logging.Level; @@ -341,38 +340,40 @@ private void processSecurity(SecurityContext securityContext, ServerRequest req, .customObjects(customObjects.orElse(new ClassToInstanceStore<>())) .build()); + ExecutorService executorService = req.context().get(SecurityHandler.class, ExecutorService.class) + .orElseThrow(() -> new SecurityException("ExecutorService was found registered in request context.")); + Optional context = Contexts.context(); - processAuthentication(res, securityContext, tracing.atnTracing()) - .thenCompose(atnResult -> { - if (atnResult.proceed) { - // authentication was OK or disabled, we should continue - return processAuthorization(req, res, securityContext, tracing.atzTracing()); - } else { - // authentication told us to stop processing - return CompletableFuture.completedFuture(AtxResult.STOP); - } - }) - .thenAccept(atzResult -> { - if (atzResult.proceed) { - // authorization was OK, we can continue processing - tracing.logProceed(); - tracing.finish(); - - // propagate context information in call to next - context.ifPresentOrElse( - c -> Contexts.runInContext(c, (Runnable) req::next), - req::next); - } else { - tracing.logDeny(); - tracing.finish(); - } - }) - .exceptionally(throwable -> { - tracing.error(throwable); - LOGGER.log(Level.SEVERE, "Unexpected exception during security processing", throwable); - abortRequest(res, null, Http.Status.INTERNAL_SERVER_ERROR_500.code(), Map.of()); - return null; - }); + + executorService.submit(() -> { + try { + AtxResult atnResult = processAuthentication(res, securityContext, tracing.atnTracing()); + AtxResult atzResult; + if (atnResult.proceed) { + // authentication was OK or disabled, we should continue + atzResult = processAuthorization(req, res, securityContext, tracing.atzTracing()); + } else { + // authentication told us to stop processing + atzResult = AtxResult.STOP; + } + if (atzResult.proceed) { + // authorization was OK, we can continue processing + tracing.logProceed(); + tracing.finish(); + + // propagate context information in call to next + context.ifPresentOrElse( + c -> Contexts.runInContext(c, (Runnable) req::next), + req::next); + } else { + tracing.logDeny(); + tracing.finish(); + } + } catch (Exception e) { + tracing.error(e); + LOGGER.log(Level.SEVERE, "Unexpected exception during security processing", e); + } + }); // auditing res.whenSent().thenAccept(sr -> processAudit(req, sr, securityContext)); @@ -428,20 +429,18 @@ private void processAudit(ServerRequest req, ServerResponse res, SecurityContext securityContext.audit(auditEvent); } - private CompletionStage processAuthentication(ServerResponse res, - SecurityContext securityContext, - AtnTracing atnTracing) { + private AtxResult processAuthentication(ServerResponse res, SecurityContext securityContext, AtnTracing atnTracing) { if (!authenticate.orElse(false)) { - return CompletableFuture.completedFuture(AtxResult.PROCEED); + return AtxResult.PROCEED; } - CompletableFuture future = new CompletableFuture<>(); - SecurityClientBuilder clientBuilder = securityContext.atnClientBuilder(); configureSecurityRequest(clientBuilder, atnTracing.findParent().orElse(null)); - clientBuilder.explicitProvider(explicitAuthenticator.orElse(null)).submit().thenAccept(response -> { + try { + AuthenticationResponse response = clientBuilder.explicitProvider(explicitAuthenticator.orElse(null)).submit(); + // copy headers to be returned with the current response response.responseHeaders() .forEach((key, value) -> res.headers().set(Http.Header.create(Http.Header.create(key), value))); @@ -451,38 +450,34 @@ private CompletionStage processAuthentication(ServerResponse res, //everything is fine, we can continue with processing break; case FAILURE_FINISH: - if (atnFinishFailure(res, future, response)) { + if (atnFinishFailure(res, response)) { atnSpanFinish(atnTracing, response); - return; + return AtxResult.STOP; } break; case SUCCESS_FINISH: - atnFinish(res, future, response); + atnFinish(res, response); atnSpanFinish(atnTracing, response); - return; + return AtxResult.STOP; case ABSTAIN: case FAILURE: - if (atnAbstainFailure(res, future, response)) { + if (atnAbstainFailure(res, response)) { atnSpanFinish(atnTracing, response); - return; + return AtxResult.STOP; } break; default: - Exception e = new SecurityException("Invalid SecurityStatus returned: " + response.status()); - future.completeExceptionally(e); + SecurityException e = new SecurityException("Invalid SecurityStatus returned: " + response.status()); atnTracing.error(e); - return; + throw e; } atnSpanFinish(atnTracing, response); - future.complete(new AtxResult(clientBuilder.buildRequest())); - }).exceptionally(throwable -> { - atnTracing.error(throwable); - future.completeExceptionally(throwable); - return null; - }); - - return future; + return new AtxResult(clientBuilder.buildRequest()); + } catch (Exception e) { + atnTracing.error(e); + throw e; + } } private void atnSpanFinish(AtnTracing atnTracing, AuthenticationResponse response) { @@ -493,9 +488,7 @@ private void atnSpanFinish(AtnTracing atnTracing, AuthenticationResponse respons atnTracing.finish(); } - private boolean atnAbstainFailure(ServerResponse res, - CompletableFuture future, - AuthenticationResponse response) { + private boolean atnAbstainFailure(ServerResponse res, AuthenticationResponse response) { if (authenticationOptional.orElse(false)) { LOGGER.finest("Authentication failed, but was optional, so assuming anonymous"); return false; @@ -506,14 +499,10 @@ private boolean atnAbstainFailure(ServerResponse res, Http.Status.UNAUTHORIZED_401.code(), Map.of(Http.Header.WWW_AUTHENTICATE, List.of("Basic realm=\"Security Realm\""))); - future.complete(AtxResult.STOP); return true; } - private boolean atnFinishFailure(ServerResponse res, - CompletableFuture future, - AuthenticationResponse response) { - + private boolean atnFinishFailure(ServerResponse res, AuthenticationResponse response) { if (authenticationOptional.orElse(false)) { LOGGER.finest("Authentication failed, but was optional, so assuming anonymous"); return false; @@ -521,19 +510,15 @@ private boolean atnFinishFailure(ServerResponse res, int defaultStatusCode = Http.Status.UNAUTHORIZED_401.code(); abortRequest(res, response, defaultStatusCode, Map.of()); - future.complete(AtxResult.STOP); return true; } } - private void atnFinish(ServerResponse res, - CompletableFuture future, - AuthenticationResponse response) { + private void atnFinish(ServerResponse res, AuthenticationResponse response) { int defaultStatusCode = Http.Status.OK_200.code(); abortRequest(res, response, defaultStatusCode, Map.of()); - future.complete(AtxResult.STOP); } private void abortRequest(ServerResponse res, @@ -572,17 +557,15 @@ private void configureSecurityRequest(SecurityRequestBuilder processAuthorization(ServerRequest req, - ServerResponse res, - SecurityContext context, - AtzTracing atzTracing) { - CompletableFuture future = new CompletableFuture<>(); + private AtxResult processAuthorization(ServerRequest req, + ServerResponse res, + SecurityContext context, + AtzTracing atzTracing) { if (!authorize.orElse(false)) { - future.complete(AtxResult.PROCEED); atzTracing.logStatus(SecurityResponse.SecurityStatus.ABSTAIN); atzTracing.finish(); - return future; + return AtxResult.PROCEED; } Set rolesSet = rolesAllowed.orElse(Set.of()); @@ -596,17 +579,15 @@ private CompletionStage processAuthorization(ServerRequest req, if (rolesSet.stream().noneMatch(role -> context.isUserInRole(role, explicitAuthorizer.get()))) { auditRoleMissing(context, req.path(), context.user(), rolesSet); abortRequest(res, null, Http.Status.FORBIDDEN_403.code(), Map.of()); - future.complete(AtxResult.STOP); atzTracing.finish(); - return future; + return AtxResult.STOP; } } else { if (rolesSet.stream().noneMatch(context::isUserInRole)) { auditRoleMissing(context, req.path(), context.user(), rolesSet); abortRequest(res, null, Http.Status.FORBIDDEN_403.code(), Map.of()); - future.complete(AtxResult.STOP); atzTracing.finish(); - return future; + return AtxResult.STOP; } } } @@ -617,7 +598,8 @@ private CompletionStage processAuthorization(ServerRequest req, configureSecurityRequest(client, atzTracing.findParent().orElse(null)); - client.explicitProvider(explicitAuthorizer.orElse(null)).submit().thenAccept(response -> { + try { + AuthorizationResponse response = client.explicitProvider(explicitAuthorizer.orElse(null)).submit(); atzTracing.logStatus(response.status()); switch (response.status()) { @@ -632,31 +614,23 @@ private CompletionStage processAuthorization(ServerRequest req, atzTracing.finish(); abortRequest(res, response, defaultStatus, Map.of()); - future.complete(AtxResult.STOP); - return; + return AtxResult.STOP; case ABSTAIN: case FAILURE: atzTracing.finish(); abortRequest(res, response, Http.Status.FORBIDDEN_403.code(), Map.of()); - future.complete(AtxResult.STOP); - return; + return AtxResult.STOP; default: - SecurityException e = new SecurityException("Invalid SecurityStatus returned: " + response.status()); - atzTracing.error(e); - future.completeExceptionally(e); - return; + throw new SecurityException("Invalid SecurityStatus returned: " + response.status()); } atzTracing.finish(); // everything was OK - future.complete(AtxResult.PROCEED); - }).exceptionally(throwable -> { - atzTracing.error(throwable); - future.completeExceptionally(throwable); - return null; - }); - - return future; + return AtxResult.PROCEED; + } catch (Exception e) { + atzTracing.error(e); + throw e; + } } private void auditRoleMissing(SecurityContext context, diff --git a/security/integration/webserver/src/main/java/io/helidon/security/integration/webserver/WebSecurity.java b/security/integration/webserver/src/main/java/io/helidon/security/integration/webserver/WebSecurity.java index a1db8dac85f..cbebd77131a 100644 --- a/security/integration/webserver/src/main/java/io/helidon/security/integration/webserver/WebSecurity.java +++ b/security/integration/webserver/src/main/java/io/helidon/security/integration/webserver/WebSecurity.java @@ -21,10 +21,13 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import java.util.logging.Logger; import java.util.stream.Collectors; +import io.helidon.common.configurable.ThreadPoolSupplier; import io.helidon.common.http.Http; import io.helidon.config.Config; import io.helidon.config.ConfigValue; @@ -99,6 +102,9 @@ public final class WebSecurity implements Service { private final Security security; private final Config config; private final SecurityHandler defaultHandler; + private final Supplier executorService = ThreadPoolSupplier.builder() + .name("security-thread-pool") + .build(); private WebSecurity(Security security, Config config) { this(security, config, SecurityHandler.create()); @@ -363,6 +369,7 @@ private void registerContext(ServerRequest req, ServerResponse res) { SecurityContext context = contextBuilder.build(); req.context().register(context); + req.context().register(SecurityHandler.class, executorService.get()); req.context().register(defaultHandler); } @@ -384,8 +391,8 @@ private void registerRouting(Routing.Rules routing) { String path = pathConfig.get("path") .asString() - .orElseThrow(() -> new SecurityException(pathConfig - .key() + " must contain path key with a path to " + .orElseThrow(() -> new SecurityException(pathConfig.key() + + " must contain path key with a path to " + "register to web server")); if (methods.isEmpty()) { routing.any(path, SecurityHandler.create(pathConfig, defaults)); diff --git a/security/integration/webserver/src/main/java/module-info.java b/security/integration/webserver/src/main/java/module-info.java index f08b6ada097..447c79f1cf0 100644 --- a/security/integration/webserver/src/main/java/module-info.java +++ b/security/integration/webserver/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ requires transitive io.helidon.security.util; requires io.helidon.reactive.webserver; requires io.helidon.security.integration.common; + requires static io.helidon.config.metadata; exports io.helidon.security.integration.webserver; } diff --git a/security/integration/webserver/src/test/java/io/helidon/security/integration/webserver/WebSecurityBuilderGateDefaultsTest.java b/security/integration/webserver/src/test/java/io/helidon/security/integration/webserver/WebSecurityBuilderGateDefaultsTest.java index 0288e33652f..e1758badd7b 100644 --- a/security/integration/webserver/src/test/java/io/helidon/security/integration/webserver/WebSecurityBuilderGateDefaultsTest.java +++ b/security/integration/webserver/src/test/java/io/helidon/security/integration/webserver/WebSecurityBuilderGateDefaultsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/security/integration/webserver/src/test/java/io/helidon/security/integration/webserver/WebSecurityProgrammaticTest.java b/security/integration/webserver/src/test/java/io/helidon/security/integration/webserver/WebSecurityProgrammaticTest.java index 6fd990caebc..6864a6ea3fc 100644 --- a/security/integration/webserver/src/test/java/io/helidon/security/integration/webserver/WebSecurityProgrammaticTest.java +++ b/security/integration/webserver/src/test/java/io/helidon/security/integration/webserver/WebSecurityProgrammaticTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/security/integration/webserver/src/test/java/io/helidon/security/integration/webserver/WebSecurityTest.java b/security/integration/webserver/src/test/java/io/helidon/security/integration/webserver/WebSecurityTest.java index 56b297f2289..230d06f6d0c 100644 --- a/security/integration/webserver/src/test/java/io/helidon/security/integration/webserver/WebSecurityTest.java +++ b/security/integration/webserver/src/test/java/io/helidon/security/integration/webserver/WebSecurityTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package io.helidon.security.integration.webserver; +import java.util.concurrent.ExecutorService; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; @@ -45,9 +46,8 @@ public void testExecutorService() throws TimeoutException, InterruptedException Routing routing = Routing.builder() .register(WebSecurity.create(security)) .get("/unit_test", (req, res) -> { - req.context() - .get(SecurityContext.class) - .ifPresent(context -> execClassHolder.set(context.executorService().getClass())); + req.context().get(SecurityHandler.class, ExecutorService.class) + .ifPresent(executorService -> execClassHolder.set(executorService.getClass())); req.next(); }) .build(); diff --git a/security/providers/abac/src/main/java/io/helidon/security/providers/abac/AbacProvider.java b/security/providers/abac/src/main/java/io/helidon/security/providers/abac/AbacProvider.java index 2179a12492e..35acab918fb 100644 --- a/security/providers/abac/src/main/java/io/helidon/security/providers/abac/AbacProvider.java +++ b/security/providers/abac/src/main/java/io/helidon/security/providers/abac/AbacProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,6 @@ import io.helidon.security.providers.abac.spi.AbacValidatorService; import io.helidon.security.spi.AuthorizationProvider; import io.helidon.security.spi.SecurityProvider; -import io.helidon.security.spi.SynchronousProvider; import jakarta.annotation.security.DenyAll; import jakarta.annotation.security.PermitAll; @@ -58,7 +57,7 @@ * @see #builder() * @see #create(Config) */ -public final class AbacProvider extends SynchronousProvider implements AuthorizationProvider { +public final class AbacProvider implements AuthorizationProvider { private final List> validators = new ArrayList<>(); private final Set> supportedAnnotations; @@ -128,7 +127,7 @@ public Collection> supportedAnnotations() { } @Override - protected AuthorizationResponse syncAuthorize(ProviderRequest providerRequest) { + public AuthorizationResponse authorize(ProviderRequest providerRequest) { //let's find attributes to be validated Errors.Collector collector = Errors.collector(); List attributes = new ArrayList<>(); diff --git a/security/providers/abac/src/test/java/io/helidon/security/providers/abac/AbacProviderTest.java b/security/providers/abac/src/test/java/io/helidon/security/providers/abac/AbacProviderTest.java index 34a361c1853..2482d09ddec 100644 --- a/security/providers/abac/src/test/java/io/helidon/security/providers/abac/AbacProviderTest.java +++ b/security/providers/abac/src/test/java/io/helidon/security/providers/abac/AbacProviderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,7 +58,7 @@ public void testMissingValidator() { ProviderRequest request = Mockito.mock(ProviderRequest.class); when(request.endpointConfig()).thenReturn(ec); - AuthorizationResponse response = provider.syncAuthorize(request); + AuthorizationResponse response = provider.authorize(request); assertThat(response.status(), is(SecurityResponse.SecurityStatus.FAILURE)); assertThat(response.description(), not(Optional.empty())); @@ -86,7 +86,7 @@ public void testMissingRoleValidator() { ProviderRequest request = Mockito.mock(ProviderRequest.class); when(request.endpointConfig()).thenReturn(ec); - AuthorizationResponse response = provider.syncAuthorize(request); + AuthorizationResponse response = provider.authorize(request); assertThat(response.status(), is(SecurityResponse.SecurityStatus.FAILURE)); assertThat(response.description(), not(Optional.empty())); @@ -115,7 +115,7 @@ public void testExistingValidatorFail() { ProviderRequest request = Mockito.mock(ProviderRequest.class); when(request.endpointConfig()).thenReturn(ec); - AuthorizationResponse response = provider.syncAuthorize(request); + AuthorizationResponse response = provider.authorize(request); assertThat(response.status(), is(SecurityResponse.SecurityStatus.FAILURE)); assertThat(response.description(), not(Optional.empty())); @@ -144,7 +144,7 @@ public void testExistingValidatorSucceed() { ProviderRequest request = Mockito.mock(ProviderRequest.class); when(request.endpointConfig()).thenReturn(ec); - AuthorizationResponse response = provider.syncAuthorize(request); + AuthorizationResponse response = provider.authorize(request); assertThat(response.description().orElse("Attrib1 value is true, so the authorization should succeed"), response.status(), diff --git a/security/providers/config-vault/src/main/java/io/helidon/security/providers/config/vault/ConfigVaultProvider.java b/security/providers/config-vault/src/main/java/io/helidon/security/providers/config/vault/ConfigVaultProvider.java index eb324a0c141..301452f33e3 100644 --- a/security/providers/config-vault/src/main/java/io/helidon/security/providers/config/vault/ConfigVaultProvider.java +++ b/security/providers/config-vault/src/main/java/io/helidon/security/providers/config/vault/ConfigVaultProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import io.helidon.common.Base64Value; import io.helidon.common.crypto.SymmetricCipher; -import io.helidon.common.reactive.Single; import io.helidon.config.Config; import io.helidon.config.encryption.ConfigProperties; import io.helidon.config.metadata.Configured; @@ -78,13 +77,12 @@ public static ConfigVaultProvider create(Config config) { } @Override - public Supplier>> secret(Config config) { - Supplier> supplier = config.get("value").asString().optionalSupplier(); - return () -> Single.just(supplier.get()); + public Supplier> secret(Config config) { + return config.get("value").asString().optionalSupplier(); } @Override - public Supplier>> secret(SecretConfig providerConfig) { + public Supplier> secret(SecretConfig providerConfig) { return providerConfig.value(); } @@ -140,12 +138,8 @@ public static EncryptionConfig create(Config config) { private static EncryptionSupport encryptionSupport(char[] password) { SymmetricCipher symmetricCipher = SymmetricCipher.create(password); - Function> encrypt = bytes -> { - return Single.just(symmetricCipher.encryptToString(Base64Value.create(bytes))); - }; - Function> decrypt = cipherText -> { - return Single.just(symmetricCipher.decryptFromString(cipherText).toBytes()); - }; + Function encrypt = bytes -> symmetricCipher.encryptToString(Base64Value.create(bytes)); + Function decrypt = cipherText -> symmetricCipher.decryptFromString(cipherText).toBytes(); return EncryptionSupport.create(encrypt, decrypt); } @@ -160,24 +154,12 @@ Optional aesEncryption() { @Configured(description = "Provider of secrets defined in configuration itself", provides = SecretsProviderConfig.class) public static class SecretConfig implements SecretsProviderConfig { - private final Supplier>> value; + private final Supplier> value; - private SecretConfig(Supplier>> value) { + private SecretConfig(Supplier> value) { this.value = value; } - /** - * Create a new secret configuration with a supplier of a future ({@link io.helidon.common.reactive.Single}), - * such as when retrieving the secret from a remote service. - * The supplier must be thread safe. - * - * @param valueSupplier supplier of a value - * @return a new secret configuration - */ - public static SecretConfig createSingleSupplier(Supplier>> valueSupplier) { - return new SecretConfig(valueSupplier); - } - /** * Create a new secret configuration with a supplier of an {@link Optional}, such as when retrieving * the secret from some local information that may change. @@ -187,7 +169,7 @@ public static SecretConfig createSingleSupplier(Supplier * @return a new secret configuration */ public static SecretConfig createOptionalSupplier(Supplier> valueSupplier) { - return new SecretConfig(() -> Single.just(valueSupplier.get())); + return new SecretConfig(valueSupplier); } /** @@ -198,7 +180,7 @@ public static SecretConfig createOptionalSupplier(Supplier> val * @return a new secret configuration */ public static SecretConfig create(Supplier valueSupplier) { - return new SecretConfig(() -> Single.just(Optional.of(valueSupplier.get()))); + return new SecretConfig(() -> Optional.of(valueSupplier.get())); } /** @@ -210,10 +192,10 @@ public static SecretConfig create(Supplier valueSupplier) { @ConfiguredOption(key = "value", description = "Value of the secret, can be a reference to another configuration key" + ", such as ${app.secret}") public static SecretConfig create(String value) { - return new SecretConfig(() -> Single.just(Optional.of(value))); + return new SecretConfig(() -> Optional.of(value)); } - Supplier>> value() { + Supplier> value() { return value; } } diff --git a/security/providers/config-vault/src/test/java/io/helidon/security/providers/config/vault/ConfigVaultProviderTest.java b/security/providers/config-vault/src/test/java/io/helidon/security/providers/config/vault/ConfigVaultProviderTest.java index b71cb7172e5..00f780158e7 100644 --- a/security/providers/config-vault/src/test/java/io/helidon/security/providers/config/vault/ConfigVaultProviderTest.java +++ b/security/providers/config-vault/src/test/java/io/helidon/security/providers/config/vault/ConfigVaultProviderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,35 +58,35 @@ void testEncryptionFromConfig() { String secretString = "my secret"; byte[] secret = secretString.getBytes(StandardCharsets.UTF_8); - String encryptedDefault = security.encrypt("config-vault-default", secret).await(); - String encryptedOverride = security.encrypt("config-vault-override", secret).await(); + String encryptedDefault = security.encrypt("config-vault-default", secret); + String encryptedOverride = security.encrypt("config-vault-override", secret); assertThat(encryptedOverride, not(encryptedDefault)); - byte[] decrypted = security.decrypt("config-vault-default", encryptedDefault).await(); + byte[] decrypted = security.decrypt("config-vault-default", encryptedDefault); assertThat(new String(decrypted), is(secretString)); - decrypted = security.decrypt("config-vault-override", encryptedOverride).await(); + decrypted = security.decrypt("config-vault-override", encryptedOverride); assertThat(new String(decrypted), is(secretString)); // now make sure we used a different password Assertions.assertThrows(CryptoException.class, - () -> security.decrypt("config-vault-override", encryptedDefault).await()); + () -> security.decrypt("config-vault-override", encryptedDefault)); Assertions.assertThrows(CryptoException.class, - () -> security.decrypt("config-vault-default", encryptedOverride).await()); + () -> security.decrypt("config-vault-default", encryptedOverride)); } @Test void testSecretFromConfig() { - String password = security.secret("password", "default-value").await(); + String password = security.secret("password", "default-value"); assertThat(password, is("secret-password")); } @Test void testSecretFromBuilt() { - String password = builtSecurity.secret("password", "default-value").await(); + String password = builtSecurity.secret("password", "default-value"); assertThat(password, is("configured-password")); } diff --git a/security/providers/google-login/src/main/java/io/helidon/security/providers/google/login/GoogleTokenProvider.java b/security/providers/google-login/src/main/java/io/helidon/security/providers/google/login/GoogleTokenProvider.java index df6a18940ab..cc5af4a4177 100644 --- a/security/providers/google-login/src/main/java/io/helidon/security/providers/google/login/GoogleTokenProvider.java +++ b/security/providers/google-login/src/main/java/io/helidon/security/providers/google/login/GoogleTokenProvider.java @@ -49,7 +49,6 @@ import io.helidon.security.spi.AuthenticationProvider; import io.helidon.security.spi.OutboundSecurityProvider; import io.helidon.security.spi.SecurityProvider; -import io.helidon.security.spi.SynchronousProvider; import io.helidon.security.util.TokenHandler; import io.helidon.tracing.Span; import io.helidon.tracing.SpanContext; @@ -73,7 +72,7 @@ * * See google-login example. */ -public final class GoogleTokenProvider extends SynchronousProvider implements AuthenticationProvider, OutboundSecurityProvider { +public final class GoogleTokenProvider implements AuthenticationProvider, OutboundSecurityProvider { private static final System.Logger LOGGER = System.getLogger(GoogleTokenProvider.class.getName()); private static final String HEADER_AUTHENTICATION_REQUIRED = "WWW-Authenticate"; @@ -157,7 +156,7 @@ public static Builder builder() { } @Override - protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { Optional maybeToken; try { maybeToken = tokenHandler.extractToken(providerRequest.env().headers()); @@ -272,9 +271,9 @@ public boolean isOutboundSupported(ProviderRequest providerRequest, } @Override - protected OutboundSecurityResponse syncOutbound(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundEndpointConfig) { + public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv, + EndpointConfig outboundEndpointConfig) { return providerRequest.securityContext() .user() .flatMap(subject -> subject.publicCredential(TokenCredential.class)) diff --git a/security/providers/google-login/src/test/java/io/helidon/security/providers/google/login/GoogleTokenProviderTest.java b/security/providers/google-login/src/test/java/io/helidon/security/providers/google/login/GoogleTokenProviderTest.java index 053436b7acb..abb0cc35089 100644 --- a/security/providers/google-login/src/test/java/io/helidon/security/providers/google/login/GoogleTokenProviderTest.java +++ b/security/providers/google-login/src/test/java/io/helidon/security/providers/google/login/GoogleTokenProviderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -108,7 +108,7 @@ public static void initClass() throws GeneralSecurityException, IOException { @Test public void testInbound() { ProviderRequest inboundRequest = createInboundRequest("Authorization", "bearer " + TOKEN_VALUE); - AuthenticationResponse response = provider.syncAuthenticate(inboundRequest); + AuthenticationResponse response = provider.authenticate(inboundRequest); assertThat(response.user(), is(not(Optional.empty()))); response.user().ifPresent(subject -> { Principal principal = subject.principal(); @@ -120,7 +120,7 @@ public void testInbound() { @Test public void testInboundIncorrectToken() throws ExecutionException, InterruptedException { ProviderRequest inboundRequest = createInboundRequest("Authorization", "tearer " + TOKEN_VALUE); - AuthenticationResponse response = provider.authenticate(inboundRequest).toCompletableFuture().get(); + AuthenticationResponse response = provider.authenticate(inboundRequest); assertThat(response.status(), is(SecurityResponse.SecurityStatus.FAILURE)); assertThat(response.statusCode().orElse(200), is(400)); @@ -130,7 +130,7 @@ public void testInboundIncorrectToken() throws ExecutionException, InterruptedEx @Test public void testInboundMissingToken() throws ExecutionException, InterruptedException { ProviderRequest inboundRequest = createInboundRequest("OtherHeader", "tearer " + TOKEN_VALUE); - AuthenticationResponse response = provider.authenticate(inboundRequest).toCompletableFuture().get(); + AuthenticationResponse response = provider.authenticate(inboundRequest); assertThat(response.status(), is(SecurityResponse.SecurityStatus.FAILURE)); assertThat(response.statusCode().orElse(200), is(401)); @@ -144,7 +144,7 @@ public void testInboundInvalidToken() throws ExecutionException, InterruptedExce GoogleTokenProvider provider = GoogleTokenProvider.builder().clientId("clientId").verifier(verifier).build(); ProviderRequest inboundRequest = createInboundRequest("Authorization", "bearer " + TOKEN_VALUE); - AuthenticationResponse response = provider.authenticate(inboundRequest).toCompletableFuture().get(); + AuthenticationResponse response = provider.authenticate(inboundRequest); assertThat(response.status(), is(SecurityResponse.SecurityStatus.FAILURE)); assertThat(response.statusCode().orElse(200), is(401)); @@ -159,7 +159,7 @@ public void testInboundVerificationException() GoogleTokenProvider provider = GoogleTokenProvider.builder().clientId("clientId").verifier(verifier).build(); ProviderRequest inboundRequest = createInboundRequest("Authorization", "bearer " + TOKEN_VALUE); - AuthenticationResponse response = provider.authenticate(inboundRequest).toCompletableFuture().get(); + AuthenticationResponse response = provider.authenticate(inboundRequest); assertThat(response.status(), is(SecurityResponse.SecurityStatus.FAILURE)); assertThat(response.statusCode().orElse(200), is(401)); @@ -174,7 +174,6 @@ private ProviderRequest createInboundRequest(String headerName, String headerVal Span secSpan = Tracer.global().spanBuilder("security").start(); SecurityContext context = mock(SecurityContext.class); - when(context.executorService()).thenReturn(ForkJoinPool.commonPool()); when(context.tracer()).thenReturn(Tracer.global()); when(context.tracingSpan()).thenReturn(secSpan.context()); @@ -199,7 +198,7 @@ public void testOutbound() { provider.isOutboundSupported(outboundRequest, outboundEnv, outboundEp), is(true)); - OutboundSecurityResponse response = provider.syncOutbound(outboundRequest, outboundEnv, outboundEp); + OutboundSecurityResponse response = provider.outboundSecurity(outboundRequest, outboundEnv, outboundEp); List authorization = response.requestHeaders().get("Authorization"); @@ -223,7 +222,6 @@ private ProviderRequest buildOutboundRequest() { when(context.user()).thenReturn(Optional.of(subject)); ProviderRequest request = mock(ProviderRequest.class); when(request.securityContext()).thenReturn(context); - when(context.executorService()).thenReturn(ForkJoinPool.commonPool()); return request; } diff --git a/security/providers/header/src/main/java/io/helidon/security/providers/header/HeaderAtnProvider.java b/security/providers/header/src/main/java/io/helidon/security/providers/header/HeaderAtnProvider.java index 92581d3a336..1b7f0f7bc56 100644 --- a/security/providers/header/src/main/java/io/helidon/security/providers/header/HeaderAtnProvider.java +++ b/security/providers/header/src/main/java/io/helidon/security/providers/header/HeaderAtnProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,14 +37,13 @@ import io.helidon.security.spi.AuthenticationProvider; import io.helidon.security.spi.OutboundSecurityProvider; import io.helidon.security.spi.SecurityProvider; -import io.helidon.security.spi.SynchronousProvider; import io.helidon.security.util.TokenHandler; /** * Security provider that extracts a username (or service name) from a header. * This provider also supports propagation of identity through a header. */ -public class HeaderAtnProvider extends SynchronousProvider implements AuthenticationProvider, OutboundSecurityProvider { +public class HeaderAtnProvider implements AuthenticationProvider, OutboundSecurityProvider { private final boolean optional; private final boolean authenticate; private final boolean propagate; @@ -86,7 +85,7 @@ public static Builder builder() { } @Override - protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { if (!authenticate) { return AuthenticationResponse.abstain(); } @@ -129,9 +128,9 @@ public boolean isOutboundSupported(ProviderRequest providerRequest, } @Override - protected OutboundSecurityResponse syncOutbound(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundEndpointConfig) { + public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv, + EndpointConfig outboundEndpointConfig) { Optional toPropagate; if (subjectType == SubjectType.USER) { diff --git a/security/providers/header/src/test/java/io/helidon/security/providers/header/HeaderAtnProviderTest.java b/security/providers/header/src/test/java/io/helidon/security/providers/header/HeaderAtnProviderTest.java index 5a29370bd1a..f9bcf2daa93 100644 --- a/security/providers/header/src/test/java/io/helidon/security/providers/header/HeaderAtnProviderTest.java +++ b/security/providers/header/src/test/java/io/helidon/security/providers/header/HeaderAtnProviderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2020 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ public void testExtraction() { ProviderRequest request = mock(ProviderRequest.class); when(request.env()).thenReturn(env); - AuthenticationResponse response = provider.syncAuthenticate(request); + AuthenticationResponse response = provider.authenticate(request); assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); assertThat(response.user(), is(not(Optional.empty()))); @@ -81,7 +81,7 @@ public void testExtractionNoHeader() { ProviderRequest request = mock(ProviderRequest.class); when(request.env()).thenReturn(env); - AuthenticationResponse response = provider.syncAuthenticate(request); + AuthenticationResponse response = provider.authenticate(request); assertThat(response.status(), is(SecurityResponse.SecurityStatus.ABSTAIN)); assertThat(response.user(), is(Optional.empty())); @@ -109,7 +109,7 @@ public void testOutbound() { assertThat("Outbound should be supported", provider.isOutboundSupported(request, outboundEnv, outboundEp), is(true)); - OutboundSecurityResponse response = provider.syncOutbound(request, outboundEnv, outboundEp); + OutboundSecurityResponse response = provider.outboundSecurity(request, outboundEnv, outboundEp); List custom = response.requestHeaders().get("Custom"); assertThat(custom, notNullValue()); @@ -132,7 +132,7 @@ public void testServiceExtraction() { ProviderRequest request = mock(ProviderRequest.class); when(request.env()).thenReturn(env); - AuthenticationResponse response = provider.syncAuthenticate(request); + AuthenticationResponse response = provider.authenticate(request); assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); assertThat(response.user(), is(Optional.empty())); @@ -153,7 +153,7 @@ public void testServiceNoHeaderExtraction() { ProviderRequest request = mock(ProviderRequest.class); when(request.env()).thenReturn(env); - AuthenticationResponse response = provider.syncAuthenticate(request); + AuthenticationResponse response = provider.authenticate(request); assertThat(response.status(), is(SecurityResponse.SecurityStatus.FAILURE)); assertThat(response.service(), is(Optional.empty())); @@ -180,7 +180,7 @@ public void testServiceOutbound() { EndpointConfig outboundEp = EndpointConfig.create(); assertThat("Outbound should be supported", provider.isOutboundSupported(request, outboundEnv, outboundEp), is(true)); - OutboundSecurityResponse response = provider.syncOutbound(request, outboundEnv, outboundEp); + OutboundSecurityResponse response = provider.outboundSecurity(request, outboundEnv, outboundEp); List custom = response.requestHeaders().get("Authorization"); assertThat(custom, notNullValue()); @@ -201,7 +201,7 @@ public void testNoAtn() { ProviderRequest request = mock(ProviderRequest.class); when(request.env()).thenReturn(env); - AuthenticationResponse response = provider.syncAuthenticate(request); + AuthenticationResponse response = provider.authenticate(request); assertThat(response.status(), is(SecurityResponse.SecurityStatus.ABSTAIN)); assertThat(response.user(), is(Optional.empty())); diff --git a/security/providers/http-auth/src/main/java/io/helidon/security/providers/httpauth/HttpBasicAuthProvider.java b/security/providers/http-auth/src/main/java/io/helidon/security/providers/httpauth/HttpBasicAuthProvider.java index c7189b84aec..b6e3badbd02 100644 --- a/security/providers/http-auth/src/main/java/io/helidon/security/providers/httpauth/HttpBasicAuthProvider.java +++ b/security/providers/http-auth/src/main/java/io/helidon/security/providers/httpauth/HttpBasicAuthProvider.java @@ -49,14 +49,13 @@ import io.helidon.security.spi.AuthenticationProvider; import io.helidon.security.spi.OutboundSecurityProvider; import io.helidon.security.spi.SecurityProvider; -import io.helidon.security.spi.SynchronousProvider; import io.helidon.security.util.TokenHandler; /** * Http authentication security provider. * Provides support for username and password authentication, with support for roles list. */ -public class HttpBasicAuthProvider extends SynchronousProvider implements AuthenticationProvider, OutboundSecurityProvider { +public class HttpBasicAuthProvider implements AuthenticationProvider, OutboundSecurityProvider { /** * Configure this for outbound requests to override user to use. */ @@ -138,9 +137,9 @@ public boolean isOutboundSupported(ProviderRequest providerRequest, } @Override - protected OutboundSecurityResponse syncOutbound(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundEp) { + public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv, + EndpointConfig outboundEp) { // explicit username in request properties Optional maybeUsername = outboundEp.abacAttribute(EP_PROPERTY_OUTBOUND_USER); @@ -209,7 +208,7 @@ private char[] passwordFromEndpoint(EndpointConfig outboundEp) { } @Override - protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { Map> headers = providerRequest.env().headers(); List authorizationHeader = headers.get(HEADER_AUTHENTICATION); diff --git a/security/providers/http-auth/src/main/java/io/helidon/security/providers/httpauth/HttpDigestAuthProvider.java b/security/providers/http-auth/src/main/java/io/helidon/security/providers/httpauth/HttpDigestAuthProvider.java index 8885428ba6b..e9e8f643bb3 100644 --- a/security/providers/http-auth/src/main/java/io/helidon/security/providers/httpauth/HttpDigestAuthProvider.java +++ b/security/providers/http-auth/src/main/java/io/helidon/security/providers/httpauth/HttpDigestAuthProvider.java @@ -45,13 +45,12 @@ import io.helidon.security.SubjectType; import io.helidon.security.spi.AuthenticationProvider; import io.helidon.security.spi.SecurityProvider; -import io.helidon.security.spi.SynchronousProvider; /** * Http authentication security provider. * Provides support for username and password authentication, with support for roles list. */ -public final class HttpDigestAuthProvider extends SynchronousProvider implements AuthenticationProvider { +public final class HttpDigestAuthProvider implements AuthenticationProvider { static final String HEADER_AUTHENTICATION_REQUIRED = "WWW-Authenticate"; static final String HEADER_AUTHENTICATION = "authorization"; static final String DIGEST_PREFIX = "digest "; @@ -132,7 +131,7 @@ static String nonce(long timeInMillis, Random random, char[] serverSecret) { } @Override - protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { Map> headers = providerRequest.env().headers(); List authorizationHeader = headers.get(HEADER_AUTHENTICATION); diff --git a/security/providers/http-sign/src/main/java/io/helidon/security/providers/httpsign/HttpSignProvider.java b/security/providers/http-sign/src/main/java/io/helidon/security/providers/httpsign/HttpSignProvider.java index f5429771d4e..27d1b7eab14 100644 --- a/security/providers/http-sign/src/main/java/io/helidon/security/providers/httpsign/HttpSignProvider.java +++ b/security/providers/http-sign/src/main/java/io/helidon/security/providers/httpsign/HttpSignProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,8 +23,6 @@ import java.util.Optional; import java.util.Set; import java.util.TreeMap; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; import io.helidon.config.Config; import io.helidon.config.metadata.Configured; @@ -121,26 +119,21 @@ public static Builder builder() { } @Override - public CompletionStage authenticate(ProviderRequest providerRequest) { + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { Map> headers = providerRequest.env().headers(); if ((headers.get("Signature") != null) && acceptHeaders.contains(HttpSignHeader.SIGNATURE)) { - return CompletableFuture - .supplyAsync(() -> signatureHeader(headers.get("Signature"), providerRequest.env()), - providerRequest.securityContext().executorService()); + return signatureHeader(headers.get("Signature"), providerRequest.env()); } else if ((headers.get("Authorization") != null) && acceptHeaders.contains(HttpSignHeader.AUTHORIZATION)) { // TODO when authorization header in use and "authorization" is also a // required header to be signed, we must either fail or ignore, as we cannot sign ourselves - return CompletableFuture - .supplyAsync(() -> authorizeHeader(providerRequest.env()), - providerRequest.securityContext().executorService()); + return authorizeHeader(providerRequest.env()); } if (optional) { - return CompletableFuture.completedFuture(AuthenticationResponse.abstain()); + return AuthenticationResponse.abstain(); } - return CompletableFuture - .completedFuture(AuthenticationResponse.failed("Missing header. Accepted headers: " + acceptHeaders)); + return AuthenticationResponse.failed("Missing header. Accepted headers: " + acceptHeaders); } private AuthenticationResponse authorizeHeader(SecurityEnvironment env) { @@ -256,12 +249,10 @@ public boolean isOutboundSupported(ProviderRequest providerRequest, } @Override - public CompletionStage outboundSecurity(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundConfig) { - - return CompletableFuture.supplyAsync(() -> signRequest(outboundEnv), - providerRequest.securityContext().executorService()); + public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv, + EndpointConfig outboundConfig) { + return signRequest(outboundEnv); } private OutboundSecurityResponse signRequest(SecurityEnvironment outboundEnv) { diff --git a/security/providers/http-sign/src/test/java/io/helidon/security/providers/httpsign/CurrentHttpSignProviderTest.java b/security/providers/http-sign/src/test/java/io/helidon/security/providers/httpsign/CurrentHttpSignProviderTest.java index 34f278bced5..1f7942ebac2 100644 --- a/security/providers/http-sign/src/test/java/io/helidon/security/providers/httpsign/CurrentHttpSignProviderTest.java +++ b/security/providers/http-sign/src/test/java/io/helidon/security/providers/httpsign/CurrentHttpSignProviderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,7 +72,6 @@ void testInboundSignatureRsa() throws ExecutionException, InterruptedException { HttpSignProvider provider = getProvider(); SecurityContext context = mock(SecurityContext.class); - when(context.executorService()).thenReturn(ForkJoinPool.commonPool()); SecurityEnvironment se = SecurityEnvironment.builder() .path("/my/resource") .headers(headers) @@ -84,7 +83,7 @@ void testInboundSignatureRsa() throws ExecutionException, InterruptedException { when(request.env()).thenReturn(se); when(request.endpointConfig()).thenReturn(ep); - AuthenticationResponse atnResponse = Single.create(provider.authenticate(request)).await(TIMEOUT); + AuthenticationResponse atnResponse = provider.authenticate(request); assertThat(atnResponse.description().orElse("Unknown problem"), atnResponse.status(), @@ -114,7 +113,6 @@ void testInboundSignatureHmac() throws InterruptedException, ExecutionException HttpSignProvider provider = getProvider(); SecurityContext context = mock(SecurityContext.class); - when(context.executorService()).thenReturn(ForkJoinPool.commonPool()); SecurityEnvironment se = SecurityEnvironment.builder() .path("/my/resource") .headers(headers) @@ -126,7 +124,7 @@ void testInboundSignatureHmac() throws InterruptedException, ExecutionException when(request.env()).thenReturn(se); when(request.endpointConfig()).thenReturn(ep); - AuthenticationResponse atnResponse = Single.create(provider.authenticate(request)).await(TIMEOUT); + AuthenticationResponse atnResponse = provider.authenticate(request); assertThat(atnResponse.description().orElse("Unknown problem"), atnResponse.status(), @@ -150,7 +148,6 @@ void testOutboundSignatureRsa() throws ExecutionException, InterruptedException headers.put("authorization", List.of("basic dXNlcm5hbWU6cGFzc3dvcmQ=")); SecurityContext context = mock(SecurityContext.class); - when(context.executorService()).thenReturn(ForkJoinPool.commonPool()); ProviderRequest request = mock(ProviderRequest.class); when(request.securityContext()).thenReturn(context); SecurityEnvironment outboundEnv = SecurityEnvironment.builder() @@ -164,8 +161,7 @@ void testOutboundSignatureRsa() throws ExecutionException, InterruptedException boolean outboundSupported = getProvider().isOutboundSupported(request, outboundEnv, outboundEp); assertThat("Outbound should be supported", outboundSupported, is(true)); - OutboundSecurityResponse response = Single.create(getProvider().outboundSecurity(request, outboundEnv, outboundEp)) - .await(TIMEOUT); + OutboundSecurityResponse response = getProvider().outboundSecurity(request, outboundEnv, outboundEp); assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); @@ -194,7 +190,6 @@ void testOutboundSignatureHmac() throws ExecutionException, InterruptedException headers.put("date", List.of("Thu, 08 Jun 2014 18:32:30 GMT")); SecurityContext context = mock(SecurityContext.class); - when(context.executorService()).thenReturn(ForkJoinPool.commonPool()); ProviderRequest request = mock(ProviderRequest.class); when(request.securityContext()).thenReturn(context); @@ -209,8 +204,7 @@ void testOutboundSignatureHmac() throws ExecutionException, InterruptedException boolean outboundSupported = getProvider().isOutboundSupported(request, outboundEnv, outboundEp); assertThat("Outbound should be supported", outboundSupported, is(true)); - OutboundSecurityResponse response = Single.create(getProvider().outboundSecurity(request, outboundEnv, outboundEp)) - .await(TIMEOUT); + OutboundSecurityResponse response = getProvider().outboundSecurity(request, outboundEnv, outboundEp); assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); diff --git a/security/providers/http-sign/src/test/java/io/helidon/security/providers/httpsign/OldHttpSignProviderTest.java b/security/providers/http-sign/src/test/java/io/helidon/security/providers/httpsign/OldHttpSignProviderTest.java index 77a8cf51b35..8c106203696 100644 --- a/security/providers/http-sign/src/test/java/io/helidon/security/providers/httpsign/OldHttpSignProviderTest.java +++ b/security/providers/http-sign/src/test/java/io/helidon/security/providers/httpsign/OldHttpSignProviderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,7 +75,6 @@ void testInboundSignatureRsa() throws ExecutionException, InterruptedException { HttpSignProvider provider = getProvider(); SecurityContext context = mock(SecurityContext.class); - when(context.executorService()).thenReturn(ForkJoinPool.commonPool()); SecurityEnvironment se = SecurityEnvironment.builder() .path("/my/resource") .headers(headers) @@ -87,7 +86,7 @@ void testInboundSignatureRsa() throws ExecutionException, InterruptedException { when(request.env()).thenReturn(se); when(request.endpointConfig()).thenReturn(ep); - AuthenticationResponse atnResponse = Single.create(provider.authenticate(request)).await(TIMEOUT); + AuthenticationResponse atnResponse = provider.authenticate(request); assertThat(atnResponse.description().orElse("Unknown problem"), atnResponse.status(), @@ -117,7 +116,6 @@ void testInboundSignatureHmac() throws InterruptedException, ExecutionException HttpSignProvider provider = getProvider(); SecurityContext context = mock(SecurityContext.class); - when(context.executorService()).thenReturn(ForkJoinPool.commonPool()); SecurityEnvironment se = SecurityEnvironment.builder() .path("/my/resource") .headers(headers) @@ -129,7 +127,7 @@ void testInboundSignatureHmac() throws InterruptedException, ExecutionException when(request.env()).thenReturn(se); when(request.endpointConfig()).thenReturn(ep); - AuthenticationResponse atnResponse = Single.create(provider.authenticate(request)).await(TIMEOUT); + AuthenticationResponse atnResponse = provider.authenticate(request); assertThat(atnResponse.description().orElse("Unknown problem"), atnResponse.status(), @@ -153,7 +151,6 @@ void testOutboundSignatureRsa() throws ExecutionException, InterruptedException headers.put("authorization", List.of("basic dXNlcm5hbWU6cGFzc3dvcmQ=")); SecurityContext context = mock(SecurityContext.class); - when(context.executorService()).thenReturn(ForkJoinPool.commonPool()); ProviderRequest request = mock(ProviderRequest.class); when(request.securityContext()).thenReturn(context); SecurityEnvironment outboundEnv = SecurityEnvironment.builder() @@ -167,8 +164,7 @@ void testOutboundSignatureRsa() throws ExecutionException, InterruptedException boolean outboundSupported = getProvider().isOutboundSupported(request, outboundEnv, outboundEp); assertThat("Outbound should be supported", outboundSupported, is(true)); - OutboundSecurityResponse response = Single.create(getProvider().outboundSecurity(request, outboundEnv, outboundEp)) - .await(TIMEOUT); + OutboundSecurityResponse response = getProvider().outboundSecurity(request, outboundEnv, outboundEp); assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); @@ -200,7 +196,6 @@ void testOutboundSignatureHmac() throws ExecutionException, InterruptedException headers.put("date", List.of("Thu, 08 Jun 2014 18:32:30 GMT")); SecurityContext context = mock(SecurityContext.class); - when(context.executorService()).thenReturn(ForkJoinPool.commonPool()); ProviderRequest request = mock(ProviderRequest.class); when(request.securityContext()).thenReturn(context); @@ -215,8 +210,7 @@ void testOutboundSignatureHmac() throws ExecutionException, InterruptedException boolean outboundSupported = getProvider().isOutboundSupported(request, outboundEnv, outboundEp); assertThat("Outbound should be supported", outboundSupported, is(true)); - OutboundSecurityResponse response = Single.create(getProvider().outboundSecurity(request, outboundEnv, outboundEp)) - .await(TIMEOUT); + OutboundSecurityResponse response = getProvider().outboundSecurity(request, outboundEnv, outboundEp); assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); diff --git a/security/providers/idcs-mapper/pom.xml b/security/providers/idcs-mapper/pom.xml index 79b4093be1c..25078bae624 100644 --- a/security/providers/idcs-mapper/pom.xml +++ b/security/providers/idcs-mapper/pom.xml @@ -59,11 +59,6 @@ helidon-common-features-api true - - io.helidon.jersey - helidon-jersey-client - provided - io.helidon.jersey helidon-jersey-media-jsonp diff --git a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsMtRoleMapperProvider.java b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsMtRoleMapperProvider.java index b1385d190fb..95697e13fca 100644 --- a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsMtRoleMapperProvider.java +++ b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsMtRoleMapperProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,22 @@ package io.helidon.security.providers.idcs.mapper; import java.lang.System.Logger.Level; +import java.net.URI; +import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import io.helidon.common.context.Context; +import io.helidon.common.context.Contexts; +import io.helidon.common.http.Http; import io.helidon.config.Config; +import io.helidon.config.metadata.Configured; +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.nima.webclient.http1.Http1Client; +import io.helidon.nima.webclient.http1.Http1ClientRequest; import io.helidon.security.AuthenticationResponse; import io.helidon.security.Grant; import io.helidon.security.ProviderRequest; @@ -34,28 +42,21 @@ import io.helidon.security.providers.common.EvictableCache; import io.helidon.security.providers.oidc.common.OidcConfig; import io.helidon.security.spi.SecurityProvider; +import io.helidon.security.spi.SubjectMappingProvider; import io.helidon.security.util.TokenHandler; import jakarta.json.Json; import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonBuilderFactory; import jakarta.json.JsonObjectBuilder; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.client.Invocation; -import jakarta.ws.rs.client.WebTarget; -import jakarta.ws.rs.core.Response; /** * {@link io.helidon.security.spi.SubjectMappingProvider} to obtain roles from IDCS server for a user. * Supports multi tenancy in IDCS. - * - * @deprecated use {@link io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperRxProvider} instead */ -@Deprecated(forRemoval = true, since = "2.4.0") public class IdcsMtRoleMapperProvider extends IdcsRoleMapperProviderBase { /** - * Name of the header containing the IDCS tenant. This is the default used, can be overriden + * Name of the header containing the IDCS tenant. This is the default used, can be overridden * in builder by {@link IdcsMtRoleMapperProvider.Builder#idcsTenantTokenHandler(io.helidon.security.util.TokenHandler)} */ protected static final String IDCS_TENANT_HEADER = "X-USER-IDENTITY-SERVICE-GUID"; @@ -77,7 +78,7 @@ public class IdcsMtRoleMapperProvider extends IdcsRoleMapperProviderBase { /** * Configure instance from any descendant of - * {@link io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider.Builder}. + * {@link IdcsMtRoleMapperProvider.Builder}. * * @param builder containing the required configuration */ @@ -123,18 +124,19 @@ public static SecurityProvider create(Config config) { /** * Enhance the subject with appropriate roles from IDCS. * - * @param subject subject of the user (never null) * @param request provider request * @param previousResponse authenticated response (never null) - * @return enhanced subject + * @param subject subject of the user (never null) + * @return future with enhanced subject */ - protected Subject enhance(Subject subject, - ProviderRequest request, - AuthenticationResponse previousResponse) { + @Override + protected Subject enhance(ProviderRequest request, + AuthenticationResponse previousResponse, + Subject subject) { Optional maybeIdcsMtContext = extractIdcsMtContext(subject, request); - if (!maybeIdcsMtContext.isPresent()) { + if (maybeIdcsMtContext.isEmpty()) { LOGGER.log(Level.TRACE, () -> "Missing multitenant information IDCS CONTEXT: " + maybeIdcsMtContext + ", subject: " @@ -147,33 +149,48 @@ protected Subject enhance(Subject subject, MtCacheKey cacheKey = new MtCacheKey(idcsMtContext, name); // double cache - List serverGrants = cache.computeValue(cacheKey, - () -> computeGrants(idcsMtContext.tenantId(), - idcsMtContext.appId(), - subject)) - .orElseGet(List::of); - - List grants = new LinkedList<>(serverGrants); - - // additional grants may not be cached (leave this decision to overriding class) - addAdditionalGrants(idcsMtContext.tenantId(), idcsMtContext.appId(), subject) - .map(grants::addAll); - - return buildSubject(subject, grants); + Optional> grants = cache.computeValue(cacheKey, Optional::empty); + if (grants.isPresent()) { + List additionalGrants = addAdditionalGrants(idcsMtContext.tenantId(), + idcsMtContext.appId(), + subject, + grants.get()); + List allGrants = new ArrayList<>(grants.get()); + allGrants.addAll(additionalGrants); + return buildSubject(subject, allGrants); + } + // we do not have a cached value, we must request it from remote server + // this may trigger multiple times in parallel - rather than creating a map of future for each user + // we leave this be (as the map of futures may be unlimited) + + List result = new ArrayList<>(computeGrants(idcsMtContext.tenantId(), idcsMtContext.appId(), subject)); + List fromCache = cache.computeValue(cacheKey, () -> Optional.of(List.copyOf(result))).orElseGet(List::of); + List additionalGrants = addAdditionalGrants(idcsMtContext.tenantId(), + idcsMtContext.appId(), + subject, + fromCache); + result.addAll(additionalGrants); + return buildSubject(subject, result); } - private Optional> computeGrants(String idcsTenantId, String idcsAppName, Subject subject) { - return getGrantsFromServer(idcsTenantId, idcsAppName, subject) - .map(grants -> Collections.unmodifiableList(new LinkedList<>(grants))); - + /** + * Compute grants for the provided MT information. + * + * @param idcsTenantId tenant id + * @param idcsAppName app name + * @param subject subject + * @return future with grants to be added to the subject + */ + protected List computeGrants(String idcsTenantId, String idcsAppName, Subject subject) { + return getGrantsFromServer(idcsTenantId, idcsAppName, subject); } /** * Extract IDCS multitenancy context form the the request. * *

    By default, the context is extracted from the headers using token handlers for - * {@link Builder#idcsTenantTokenHandler(TokenHandler) tenant} and - * {@link Builder#idcsAppNameTokenHandler(TokenHandler) app}. + * {@link IdcsMtRoleMapperProvider.Builder#idcsTenantTokenHandler(io.helidon.security.util.TokenHandler) tenant} and + * {@link IdcsMtRoleMapperProvider.Builder#idcsAppNameTokenHandler(io.helidon.security.util.TokenHandler) app}. * @param subject Subject that is being mapped * @param request ProviderRequest context that is being mapped. * @return Optional with the context, empty if the context is not present in the request. @@ -190,10 +207,14 @@ protected Optional extractIdcsMtContext(Subject subject, Provider * @param idcsTenantId IDCS tenant id * @param idcsAppName IDCS application name * @param subject subject of the user/service + * @param idcsGrants Roles already retrieved from IDCS * @return list with new grants to add to the enhanced subject */ - protected Optional> addAdditionalGrants(String idcsTenantId, String idcsAppName, Subject subject) { - return Optional.empty(); + protected List addAdditionalGrants(String idcsTenantId, + String idcsAppName, + Subject subject, + List idcsGrants) { + return List.of(); } /** @@ -204,34 +225,43 @@ protected Optional> addAdditionalGrants(String idcsTenantI * @param subject subject to get grants for * @return optional list of grants from server */ - protected Optional> getGrantsFromServer(String idcsTenantId, String idcsAppName, Subject subject) { + protected List getGrantsFromServer(String idcsTenantId, + String idcsAppName, + Subject subject) { String subjectName = subject.principal().getName(); String subjectType = (String) subject.principal().abacAttribute("sub_type").orElse(defaultIdcsSubjectType()); RoleMapTracing tracing = SecurityTracing.get().roleMapTracing("idcs"); - - return getAppToken(idcsTenantId, tracing).flatMap(appToken -> { - JsonObjectBuilder requestBuilder = JSON.createObjectBuilder() - .add("mappingAttributeValue", subjectName) - .add("subjectType", subjectType) - .add("appName", idcsAppName) - .add("includeMemberships", true); - - JsonArrayBuilder arrayBuilder = JSON.createArrayBuilder(); - arrayBuilder.add("urn:ietf:params:scim:schemas:oracle:idcs:Asserter"); - requestBuilder.add("schemas", arrayBuilder); - - Invocation.Builder reqBuilder = multitenantEndpoints.assertEndpoint(idcsTenantId).request(); - - tracing.findParent() - .ifPresent(spanContext -> reqBuilder.property(PARENT_CONTEXT_CLIENT_PROPERTY, spanContext)); - - Response groupResponse = reqBuilder - .header("Authorization", "Bearer " + appToken) - .post(Entity.json(requestBuilder.build())); - - return processServerResponse(groupResponse, subjectName); - }); + Optional maybeAppToken = getAppToken(idcsTenantId, tracing); + String appToken = maybeAppToken.orElseThrow(() -> new SecurityException("Application token not available")); + + JsonObjectBuilder requestBuilder = JSON.createObjectBuilder() + .add("mappingAttributeValue", subjectName) + .add("subjectType", subjectType) + .add("appName", idcsAppName) + .add("includeMemberships", true); + + JsonArrayBuilder arrayBuilder = JSON.createArrayBuilder(); + arrayBuilder.add("urn:ietf:params:scim:schemas:oracle:idcs:Asserter"); + requestBuilder.add("schemas", arrayBuilder); + + Context parentContext = Contexts.context().orElseGet(Contexts::globalContext); + Context childContext = Context.builder() + .parent(parentContext) + .build(); + + tracing.findParent() + .ifPresent(childContext::register); + + Http1ClientRequest post = oidcConfig().generalWebClient() + .post() + .uri(multitenantEndpoints.assertEndpoint(idcsTenantId)) + .headers(it -> { + it.add(Http.Header.AUTHORIZATION, "Bearer " + appToken); + return it; + }); + + return processRoleRequest(post, requestBuilder.build(), subjectName); } /** @@ -243,12 +273,14 @@ protected Optional> getGrantsFromServer(String idcsTenantI */ protected Optional getAppToken(String idcsTenantId, RoleMapTracing tracing) { // if cached and valid, use the cached token - return tokenCache.computeIfAbsent(idcsTenantId, key -> new AppToken(multitenantEndpoints.tokenEndpoint(idcsTenantId))) + return tokenCache.computeIfAbsent(idcsTenantId, key -> new AppToken(oidcConfig().appWebClient(), + multitenantEndpoints.tokenEndpoint(idcsTenantId), + oidcConfig().tokenRefreshSkew())) .getToken(tracing); } /** - * Get the {@link io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider.MultitenancyEndpoints} used + * Get the {@link IdcsMtRoleMapperProvider.MultitenancyEndpoints} used * to get assertion and token endpoints of a multitenant IDCS. * * @return endpoints to use by this implementation @@ -258,10 +290,41 @@ protected MultitenancyEndpoints multitenancyEndpoints() { } /** - * Fluent API builder for {@link io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider}. + * Multitenant endpoints for accessing IDCS services. + */ + public interface MultitenancyEndpoints { + /** + * The tenant id of the infrastructure tenant. + * + * @return id of the tenant + */ + String idcsInfraTenantId(); + + /** + * Asserter endpoint URI for a specific tenant. + * + * @param tenantId id of tenant to get the endpoint for + * @return URI for the tenant + */ + URI assertEndpoint(String tenantId); + + /** + * Token endpoint URI for a specific tenant. + * + * @param tenantId id of tenant to get the endpoint for + * @return URI for the tenant + */ + URI tokenEndpoint(String tenantId); + } + + /** + * Fluent API builder for {@link IdcsMtRoleMapperProvider}. * * @param type of a descendant of this builder */ + @Configured(prefix = IdcsRoleMapperProviderService.PROVIDER_CONFIG_KEY, + description = "Multitenant IDCS role mapping provider", + provides = {SecurityProvider.class, SubjectMappingProvider.class}) public static class Builder> extends IdcsRoleMapperProviderBase.Builder> implements io.helidon.common.Builder, IdcsMtRoleMapperProvider> { @@ -305,6 +368,7 @@ public B config(Config config) { * @param idcsAppNameTokenHandler new token handler to extract IDCS application name * @return updated builder instance */ + @ConfiguredOption(key = "idcs-app-name-handler") public B idcsAppNameTokenHandler(TokenHandler idcsAppNameTokenHandler) { this.idcsAppNameTokenHandler = idcsAppNameTokenHandler; return me; @@ -317,7 +381,7 @@ public B idcsAppNameTokenHandler(TokenHandler idcsAppNameTokenHandler) { * @param idcsTenantTokenHandler new token handler to extract IDCS tenant ID * @return updated builder instance */ - + @ConfiguredOption(key = "idcs-tenant-handler") public B idcsTenantTokenHandler(TokenHandler idcsTenantTokenHandler) { this.idcsTenantTokenHandler = idcsTenantTokenHandler; return me; @@ -340,43 +404,16 @@ public B multitenantEndpoints(MultitenancyEndpoints endpoints) { * @param roleCache cache to use * @return updated builder instance */ + @ConfiguredOption(key = "cache-config", type = EvictableCache.class) public B cache(EvictableCache> roleCache) { this.cache = roleCache; return me; } } - /** - * Multitenant endpoints for accessing IDCS services. - */ - public interface MultitenancyEndpoints { - /** - * The tenant id of the infrastructure tenant. - * - * @return id of the tenant - */ - String idcsInfraTenantId(); - - /** - * Asserter endpoint for a specific tenant. - * - * @param tenantId id of tenant to get the endpoint for - * @return web target for the tenant - */ - WebTarget assertEndpoint(String tenantId); - - /** - * Token endpoint for a specific tenant. - * - * @param tenantId id of tenant to get the endpoint for - * @return web target for the tenant - */ - WebTarget tokenEndpoint(String tenantId); - } - /** * Default implementation of the - * {@link io.helidon.security.providers.idcs.mapper.IdcsMtRoleMapperProvider.MultitenancyEndpoints}. + * {@link IdcsMtRoleMapperProvider.MultitenancyEndpoints}. * Caches the endpoints per tenant. */ protected static class DefaultMultitenancyEndpoints implements MultitenancyEndpoints { @@ -385,12 +422,12 @@ protected static class DefaultMultitenancyEndpoints implements MultitenancyEndpo private final String urlPrefix; private final String assertUrlSuffix; private final String tokenUrlSuffix; - private final Client appClient; - private final Client generalClient; + private final Http1Client appClient; + private final Http1Client generalClient; // we want to cache endpoints for each tenant - private final ConcurrentHashMap assertEndpointCache = new ConcurrentHashMap<>(); - private final ConcurrentHashMap tokenEndpointCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap assertEndpointCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap tokenEndpointCache = new ConcurrentHashMap<>(); /** * Creates endpoints from provided OIDC configuration using default URIs. @@ -415,8 +452,8 @@ protected DefaultMultitenancyEndpoints(OidcConfig config) { urlPrefix = config.identityUri().getScheme() + "://"; this.assertUrlSuffix = "/admin/v1/Asserter"; this.tokenUrlSuffix = "/oauth2/v1/token?IDCS_CLIENT_TENANT="; - this.generalClient = config.generalClient(); - this.appClient = config.appClient(); + this.generalClient = config.generalWebClient(); + this.appClient = config.appWebClient(); } @Override @@ -425,7 +462,7 @@ public String idcsInfraTenantId() { } @Override - public WebTarget assertEndpoint(String tenantId) { + public URI assertEndpoint(String tenantId) { return assertEndpointCache.computeIfAbsent(tenantId, theKey -> { String url = urlPrefix + idcsInfraHostName.replaceAll(idcsInfraTenantId, tenantId) @@ -433,12 +470,12 @@ public WebTarget assertEndpoint(String tenantId) { LOGGER.log(Level.TRACE, () -> "MT Asserter endpoint: " + url); - return generalClient.target(url); + return URI.create(url); }); } @Override - public WebTarget tokenEndpoint(String tenantId) { + public URI tokenEndpoint(String tenantId) { return tokenEndpointCache.computeIfAbsent(tenantId, theKey -> { String url = urlPrefix + idcsInfraHostName.replaceAll(idcsInfraTenantId, tenantId) @@ -446,7 +483,7 @@ public WebTarget tokenEndpoint(String tenantId) { + idcsInfraTenantId; LOGGER.log(Level.TRACE, () -> "MT Token endpoint: " + url); - return appClient.target(url); + return URI.create(url); }); } } diff --git a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsMtRoleMapperRxProvider.java b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsMtRoleMapperRxProvider.java deleted file mode 100644 index 2b9ad94739e..00000000000 --- a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsMtRoleMapperRxProvider.java +++ /dev/null @@ -1,600 +0,0 @@ -/* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.security.providers.idcs.mapper; - -import java.lang.System.Logger.Level; -import java.net.URI; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -import io.helidon.common.context.Context; -import io.helidon.common.context.Contexts; -import io.helidon.common.http.Http; -import io.helidon.common.reactive.Single; -import io.helidon.config.Config; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.reactive.webclient.WebClient; -import io.helidon.reactive.webclient.WebClientRequestBuilder; -import io.helidon.security.AuthenticationResponse; -import io.helidon.security.Grant; -import io.helidon.security.ProviderRequest; -import io.helidon.security.SecurityException; -import io.helidon.security.Subject; -import io.helidon.security.integration.common.RoleMapTracing; -import io.helidon.security.integration.common.SecurityTracing; -import io.helidon.security.providers.common.EvictableCache; -import io.helidon.security.providers.oidc.common.OidcConfig; -import io.helidon.security.spi.SecurityProvider; -import io.helidon.security.spi.SubjectMappingProvider; -import io.helidon.security.util.TokenHandler; - -import jakarta.json.Json; -import jakarta.json.JsonArrayBuilder; -import jakarta.json.JsonBuilderFactory; -import jakarta.json.JsonObjectBuilder; - -/** - * {@link io.helidon.security.spi.SubjectMappingProvider} to obtain roles from IDCS server for a user. - * Supports multi tenancy in IDCS. - */ -public class IdcsMtRoleMapperRxProvider extends IdcsRoleMapperRxProviderBase { - /** - * Name of the header containing the IDCS tenant. This is the default used, can be overridden - * in builder by {@link IdcsMtRoleMapperRxProvider.Builder#idcsTenantTokenHandler(io.helidon.security.util.TokenHandler)} - */ - protected static final String IDCS_TENANT_HEADER = "X-USER-IDENTITY-SERVICE-GUID"; - /** - * Name of the header containing the IDCS app. This is the default used, can be overriden - * in builder by {@link IdcsMtRoleMapperRxProvider.Builder#idcsAppNameTokenHandler(io.helidon.security.util.TokenHandler)} - */ - protected static final String IDCS_APP_HEADER = "X-RESOURCE-SERVICE-INSTANCE-IDENTITY-APPNAME"; - - private static final System.Logger LOGGER = System - .getLogger(IdcsMtRoleMapperRxProvider.class.getName()); - private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); - - private final TokenHandler idcsTenantTokenHandler; - private final TokenHandler idcsAppNameTokenHandler; - private final EvictableCache> cache; - private final MultitenancyEndpoints multitenantEndpoints; - private final ConcurrentHashMap tokenCache = new ConcurrentHashMap<>(); - - /** - * Configure instance from any descendant of - * {@link IdcsMtRoleMapperRxProvider.Builder}. - * - * @param builder containing the required configuration - */ - protected IdcsMtRoleMapperRxProvider(Builder builder) { - super(builder); - - this.idcsTenantTokenHandler = builder.idcsTenantTokenHandler; - this.idcsAppNameTokenHandler = builder.idcsAppNameTokenHandler; - this.cache = builder.cache; - if (null == builder.multitentantEndpoints) { - this.multitenantEndpoints = new DefaultMultitenancyEndpoints(builder.oidcConfig()); - } else { - this.multitenantEndpoints = builder.multitentantEndpoints; - } - } - - /** - * Creates a new builder to build instances of this class. - * - * @return a new fluent API builder. - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Creates an instance from configuration. - *

    - * Expects: - *

      - *
    • oidc-config to load an instance of {@link io.helidon.security.providers.oidc.common.OidcConfig}
    • - *
    • cache-config (optional) to load an instance of {@link io.helidon.security.providers.common.EvictableCache} for role - * caching
    • - *
    - * - * @param config configuration of this provider - * @return a new instance configured from config - */ - public static SecurityProvider create(Config config) { - return builder().config(config).build(); - } - - /** - * Enhance the subject with appropriate roles from IDCS. - * - * @param request provider request - * @param previousResponse authenticated response (never null) - * @param subject subject of the user (never null) - * @return future with enhanced subject - */ - @Override - protected Single enhance(ProviderRequest request, - AuthenticationResponse previousResponse, - Subject subject) { - - Optional maybeIdcsMtContext = extractIdcsMtContext(subject, request); - - if (maybeIdcsMtContext.isEmpty()) { - LOGGER.log(Level.TRACE, () -> "Missing multitenant information IDCS CONTEXT: " - + maybeIdcsMtContext - + ", subject: " - + subject); - return Single.just(subject); - } - - IdcsMtContext idcsMtContext = maybeIdcsMtContext.get(); - String name = subject.principal().getName(); - MtCacheKey cacheKey = new MtCacheKey(idcsMtContext, name); - - // double cache - Optional> grants = cache.computeValue(cacheKey, Optional::empty); - if (grants.isPresent()) { - return addAdditionalGrants(idcsMtContext.tenantId(), - idcsMtContext.appId(), - subject, - grants.get()) - .map(it -> { - List allGrants = new LinkedList<>(grants.get()); - allGrants.addAll(it); - return buildSubject(subject, allGrants); - }); - } - // we do not have a cached value, we must request it from remote server - // this may trigger multiple times in parallel - rather than creating a map of future for each user - // we leave this be (as the map of futures may be unlimited) - List result = new LinkedList<>(); - - return computeGrants(idcsMtContext.tenantId(), idcsMtContext.appId(), subject) - .map(it -> { - result.addAll(it); - return result; - }) - .map(newGrants -> cache.computeValue(cacheKey, () -> Optional.of(List.copyOf(newGrants))) - .orElseGet(List::of)) - // additional grants may not be cached (leave this decision to overriding class) - .flatMapSingle(it -> addAdditionalGrants(idcsMtContext.tenantId(), idcsMtContext.appId(), subject, it)) - .map(newGrants -> { - result.addAll(newGrants); - return result; - }) - .map(it -> buildSubject(subject, it)); - } - - /** - * Compute grants for the provided MT information. - * - * @param idcsTenantId tenant id - * @param idcsAppName app name - * @param subject subject - * @return future with grants to be added to the subject - */ - protected Single> computeGrants(String idcsTenantId, String idcsAppName, Subject subject) { - return getGrantsFromServer(idcsTenantId, idcsAppName, subject); - } - - /** - * Extract IDCS multitenancy context form the the request. - * - *

    By default, the context is extracted from the headers using token handlers for - * {@link IdcsMtRoleMapperRxProvider.Builder#idcsTenantTokenHandler(io.helidon.security.util.TokenHandler) tenant} and - * {@link IdcsMtRoleMapperRxProvider.Builder#idcsAppNameTokenHandler(io.helidon.security.util.TokenHandler) app}. - * @param subject Subject that is being mapped - * @param request ProviderRequest context that is being mapped. - * @return Optional with the context, empty if the context is not present in the request. - */ - protected Optional extractIdcsMtContext(Subject subject, ProviderRequest request) { - return idcsTenantTokenHandler.extractToken(request.env().headers()) - .flatMap(tenant -> idcsAppNameTokenHandler.extractToken(request.env().headers()) - .map(app -> new IdcsMtContext(tenant, app))); - } - - /** - * Extension point to add additional grants to the subject being created. - * - * @param idcsTenantId IDCS tenant id - * @param idcsAppName IDCS application name - * @param subject subject of the user/service - * @param idcsGrants Roles already retrieved from IDCS - * @return list with new grants to add to the enhanced subject - */ - protected Single> addAdditionalGrants(String idcsTenantId, - String idcsAppName, - Subject subject, - List idcsGrants) { - return Single.just(List.of()); - } - - /** - * Get grants from IDCS server. The result is cached. - * - * @param idcsTenantId ID of the IDCS tenant - * @param idcsAppName Name of IDCS application - * @param subject subject to get grants for - * @return optional list of grants from server - */ - protected Single> getGrantsFromServer(String idcsTenantId, - String idcsAppName, - Subject subject) { - String subjectName = subject.principal().getName(); - String subjectType = (String) subject.principal().abacAttribute("sub_type").orElse(defaultIdcsSubjectType()); - - RoleMapTracing tracing = SecurityTracing.get().roleMapTracing("idcs"); - - return Single.create(getAppToken(idcsTenantId, tracing)) - .flatMapSingle(maybeAppToken -> { - if (maybeAppToken.isEmpty()) { - return Single.error(new SecurityException("Application token not available")); - } - return Single.just(maybeAppToken.get()); - }) - .flatMapSingle(appToken -> { - JsonObjectBuilder requestBuilder = JSON.createObjectBuilder() - .add("mappingAttributeValue", subjectName) - .add("subjectType", subjectType) - .add("appName", idcsAppName) - .add("includeMemberships", true); - - JsonArrayBuilder arrayBuilder = JSON.createArrayBuilder(); - arrayBuilder.add("urn:ietf:params:scim:schemas:oracle:idcs:Asserter"); - requestBuilder.add("schemas", arrayBuilder); - - Context parentContext = Contexts.context().orElseGet(Contexts::globalContext); - Context childContext = Context.builder() - .parent(parentContext) - .build(); - - tracing.findParent() - .ifPresent(childContext::register); - - WebClientRequestBuilder post = oidcConfig().generalWebClient() - .post() - .context(childContext) - .uri(multitenantEndpoints.assertEndpoint(idcsTenantId)) - .headers(it -> { - it.add(Http.Header.AUTHORIZATION, "Bearer " + appToken); - return it; - }); - - return processRoleRequest(post, requestBuilder.build(), subjectName); - }); - } - - /** - * Gets token from cache or from server. - * - * @param idcsTenantId id of tenant - * @param tracing Role mapping tracing instance to correctly trace outbound calls - * @return the token to be used to authenticate this service - */ - protected Single> getAppToken(String idcsTenantId, RoleMapTracing tracing) { - // if cached and valid, use the cached token - return tokenCache.computeIfAbsent(idcsTenantId, key -> new AppTokenRx(oidcConfig().appWebClient(), - multitenantEndpoints.tokenEndpoint(idcsTenantId), - oidcConfig().tokenRefreshSkew())) - .getToken(tracing); - } - - /** - * Get the {@link IdcsMtRoleMapperRxProvider.MultitenancyEndpoints} used - * to get assertion and token endpoints of a multitenant IDCS. - * - * @return endpoints to use by this implementation - */ - protected MultitenancyEndpoints multitenancyEndpoints() { - return multitenantEndpoints; - } - - /** - * Multitenant endpoints for accessing IDCS services. - */ - public interface MultitenancyEndpoints { - /** - * The tenant id of the infrastructure tenant. - * - * @return id of the tenant - */ - String idcsInfraTenantId(); - - /** - * Asserter endpoint URI for a specific tenant. - * - * @param tenantId id of tenant to get the endpoint for - * @return URI for the tenant - */ - URI assertEndpoint(String tenantId); - - /** - * Token endpoint URI for a specific tenant. - * - * @param tenantId id of tenant to get the endpoint for - * @return URI for the tenant - */ - URI tokenEndpoint(String tenantId); - } - - /** - * Fluent API builder for {@link IdcsMtRoleMapperRxProvider}. - * - * @param type of a descendant of this builder - */ - @Configured(prefix = IdcsRoleMapperProviderService.PROVIDER_CONFIG_KEY, - description = "Multitenant IDCS role mapping provider", - provides = {SecurityProvider.class, SubjectMappingProvider.class}) - public static class Builder> - extends IdcsRoleMapperRxProviderBase.Builder> - implements io.helidon.common.Builder, IdcsMtRoleMapperRxProvider> { - private TokenHandler idcsAppNameTokenHandler = TokenHandler.forHeader(IDCS_APP_HEADER); - private TokenHandler idcsTenantTokenHandler = TokenHandler.forHeader(IDCS_TENANT_HEADER); - private MultitenancyEndpoints multitentantEndpoints; - private EvictableCache> cache; - - @SuppressWarnings("unchecked") - private B me = (B) this; - - /** - * Default constructor. - */ - protected Builder() { - } - - @Override - public IdcsMtRoleMapperRxProvider build() { - if (null == cache) { - cache = EvictableCache.create(); - } - return new IdcsMtRoleMapperRxProvider(this); - } - - @Override - public B config(Config config) { - super.config(config); - - config.get("cache-config").as(EvictableCache::>create).ifPresent(this::cache); - config.get("idcs-tenant-handler").as(TokenHandler::create).ifPresent(this::idcsTenantTokenHandler); - config.get("idcs-app-name-handler").as(TokenHandler::create).ifPresent(this::idcsAppNameTokenHandler); - - return me; - } - - /** - * Configure token handler for IDCS Application name. - * By default the header {@value IdcsMtRoleMapperRxProvider#IDCS_APP_HEADER} is used. - * - * @param idcsAppNameTokenHandler new token handler to extract IDCS application name - * @return updated builder instance - */ - @ConfiguredOption(key = "idcs-app-name-handler") - public B idcsAppNameTokenHandler(TokenHandler idcsAppNameTokenHandler) { - this.idcsAppNameTokenHandler = idcsAppNameTokenHandler; - return me; - } - - /** - * Configure token handler for IDCS Tenant ID. - * By default the header {@value IdcsMtRoleMapperRxProvider#IDCS_TENANT_HEADER} is used. - * - * @param idcsTenantTokenHandler new token handler to extract IDCS tenant ID - * @return updated builder instance - */ - @ConfiguredOption(key = "idcs-tenant-handler") - public B idcsTenantTokenHandler(TokenHandler idcsTenantTokenHandler) { - this.idcsTenantTokenHandler = idcsTenantTokenHandler; - return me; - } - - /** - * Replace default endpoint provider in multitenant IDCS setup. - * - * @param endpoints endpoints to retrieve tenant specific token and asserter endpoints - * @return updated builder instance - */ - public B multitenantEndpoints(MultitenancyEndpoints endpoints) { - this.multitentantEndpoints = endpoints; - return me; - } - - /** - * Use explicit {@link io.helidon.security.providers.common.EvictableCache} for role caching. - * - * @param roleCache cache to use - * @return updated builder instance - */ - @ConfiguredOption(key = "cache-config", type = EvictableCache.class) - public B cache(EvictableCache> roleCache) { - this.cache = roleCache; - return me; - } - } - - /** - * Default implementation of the - * {@link IdcsMtRoleMapperRxProvider.MultitenancyEndpoints}. - * Caches the endpoints per tenant. - */ - protected static class DefaultMultitenancyEndpoints implements MultitenancyEndpoints { - private final String idcsInfraTenantId; - private final String idcsInfraHostName; - private final String urlPrefix; - private final String assertUrlSuffix; - private final String tokenUrlSuffix; - private final WebClient appClient; - private final WebClient generalClient; - - // we want to cache endpoints for each tenant - private final ConcurrentHashMap assertEndpointCache = new ConcurrentHashMap<>(); - private final ConcurrentHashMap tokenEndpointCache = new ConcurrentHashMap<>(); - - /** - * Creates endpoints from provided OIDC configuration using default URIs. - *
    - *

      - *
    • For Asserter endpoint: {@code /admin/v1/Asserter}
    • - *
    • For Token endpoint: {@code /oauth2/v1/token?IDCS_CLIENT_TENANT=}
    • - *
    - * - * @param config IDCS base configuration - */ - protected DefaultMultitenancyEndpoints(OidcConfig config) { - idcsInfraHostName = config.identityUri().getHost(); - int index = idcsInfraHostName.indexOf('.'); - - if (index == -1) { - throw new SecurityException("Configuration of multitenant IDCS is invalid. The identity host name should be " - + "'tenant-id.identityServer' but is " + idcsInfraHostName); - } - - idcsInfraTenantId = idcsInfraHostName.substring(0, index); - urlPrefix = config.identityUri().getScheme() + "://"; - this.assertUrlSuffix = "/admin/v1/Asserter"; - this.tokenUrlSuffix = "/oauth2/v1/token?IDCS_CLIENT_TENANT="; - this.generalClient = config.generalWebClient(); - this.appClient = config.appWebClient(); - } - - @Override - public String idcsInfraTenantId() { - return idcsInfraTenantId; - } - - @Override - public URI assertEndpoint(String tenantId) { - return assertEndpointCache.computeIfAbsent(tenantId, theKey -> { - String url = urlPrefix - + idcsInfraHostName.replaceAll(idcsInfraTenantId, tenantId) - + assertUrlSuffix; - - LOGGER.log(Level.TRACE, () -> "MT Asserter endpoint: " + url); - - return URI.create(url); - }); - } - - @Override - public URI tokenEndpoint(String tenantId) { - return tokenEndpointCache.computeIfAbsent(tenantId, theKey -> { - String url = urlPrefix - + idcsInfraHostName.replaceAll(idcsInfraTenantId, tenantId) - + tokenUrlSuffix - + idcsInfraTenantId; - LOGGER.log(Level.TRACE, () -> "MT Token endpoint: " + url); - - return URI.create(url); - }); - } - } - - /** - * Cache key for multitenant environments. - * Used when caching user grants. - * Suitable for use in maps and sets. - */ - public static class MtCacheKey { - private final IdcsMtContext idcsMtContext; - private final String username; - - /** - * New (immutable) cache key. - * - * @param idcsTenantId IDCS tenant ID - * @param idcsAppName IDCS application name - * @param username username - */ - protected MtCacheKey(String idcsTenantId, String idcsAppName, String username) { - this(new IdcsMtContext( - Objects.requireNonNull(idcsTenantId, "IDCS Tenant id is mandatory"), - Objects.requireNonNull(idcsAppName, "IDCS App id is mandatory")), - username); - } - - /** - * New (immutable) cache key. - * - * @param idcsMtContext IDCS multitenancy context - * @param username username - */ - protected MtCacheKey(IdcsMtContext idcsMtContext, String username) { - Objects.requireNonNull(idcsMtContext, "IDCS Multitenancy Context is mandatory"); - Objects.requireNonNull(username, "username is mandatory"); - - this.idcsMtContext = idcsMtContext; - this.username = username; - } - - /** - * IDCS Tenant ID. - * - * @return tenant id of the cache record - */ - public String idcsTenantId() { - return idcsMtContext.tenantId(); - } - - /** - * Username. - * - * @return username of the cache record - */ - public String username() { - return username; - } - - /** - * IDCS Application ID. - * - * @return application id of the cache record - */ - public String idcsAppName() { - return idcsMtContext.appId(); - } - - /** - * IDCS Multitenancy context. - * - * @return IDCS multitenancy context of the cache record - */ - public IdcsMtContext idcsMtContext() { - return idcsMtContext; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof MtCacheKey)) { - return false; - } - MtCacheKey cacheKey = (MtCacheKey) o; - return idcsMtContext.equals(cacheKey.idcsMtContext) - && username.equals(cacheKey.username); - } - - @Override - public int hashCode() { - return Objects.hash(idcsMtContext, username); - } - } -} diff --git a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperProvider.java b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperProvider.java index 4e60b3b0fa0..286505053a6 100644 --- a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperProvider.java +++ b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,15 +15,23 @@ */ package io.helidon.security.providers.idcs.mapper; +import java.net.URI; +import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.Optional; +import io.helidon.common.context.Context; +import io.helidon.common.context.Contexts; +import io.helidon.common.http.Http; import io.helidon.config.Config; +import io.helidon.config.metadata.Configured; +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.nima.webclient.http1.Http1ClientRequest; import io.helidon.security.AuthenticationResponse; import io.helidon.security.Grant; import io.helidon.security.ProviderRequest; +import io.helidon.security.SecurityException; import io.helidon.security.Subject; import io.helidon.security.integration.common.RoleMapTracing; import io.helidon.security.integration.common.SecurityTracing; @@ -36,29 +44,23 @@ import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonBuilderFactory; import jakarta.json.JsonObjectBuilder; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.client.Invocation; -import jakarta.ws.rs.client.WebTarget; -import jakarta.ws.rs.core.Response; /** - * {@link SubjectMappingProvider} to obtain roles from IDCS server for a user. + * {@link io.helidon.security.spi.SubjectMappingProvider} to obtain roles from IDCS server for a user. * Supports multi tenancy in IDCS. - * - * @deprecated use {@link io.helidon.security.providers.idcs.mapper.IdcsRoleMapperRxProvider} instead */ -@Deprecated(forRemoval = true, since = "2.4.0") public class IdcsRoleMapperProvider extends IdcsRoleMapperProviderBase implements SubjectMappingProvider { private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); private final EvictableCache> roleCache; - private final WebTarget assertEndpoint; + private final String asserterUri; + private final URI tokenEndpointUri; // caching application token (as that can be re-used for group requests) private final AppToken appToken; /** - * Constructor that accepts any {@link io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProvider.Builder} descendant. + * Constructor that accepts any {@link IdcsRoleMapperProvider.Builder} descendant. * * @param builder used to configure this instance */ @@ -68,10 +70,10 @@ protected IdcsRoleMapperProvider(Builder builder) { this.roleCache = builder.roleCache; OidcConfig oidcConfig = builder.oidcConfig(); - this.assertEndpoint = oidcConfig.generalClient().target(oidcConfig.identityUri() + "/admin/v1/Asserter"); - WebTarget tokenEndpoint = oidcConfig.tokenEndpoint(); + this.asserterUri = oidcConfig.identityUri() + "/admin/v1/Asserter"; + this.tokenEndpointUri = oidcConfig.tokenEndpointUri(); - appToken = new IdcsMtRoleMapperProvider.AppToken(tokenEndpoint); + this.appToken = new AppToken(oidcConfig.appWebClient(), tokenEndpointUri, oidcConfig.tokenRefreshSkew()); } /** @@ -88,8 +90,9 @@ public static Builder builder() { *

    * Expects: *

      - *
    • oidc-config to load an instance of {@link OidcConfig}
    • - *
    • cache-config (optional) to load an instance of {@link EvictableCache} for role caching
    • + *
    • oidc-config to load an instance of {@link io.helidon.security.providers.oidc.common.OidcConfig}
    • + *
    • cache-config (optional) to load an instance of {@link io.helidon.security.providers.common.EvictableCache} for role + * caching
    • *
    * * @param config configuration of this provider @@ -100,20 +103,24 @@ public static SecurityProvider create(Config config) { } @Override - protected Subject enhance(Subject subject, ProviderRequest request, AuthenticationResponse previousResponse) { + protected Subject enhance(ProviderRequest request, + AuthenticationResponse previousResponse, + Subject subject) { String username = subject.principal().getName(); - List grants = roleCache.computeValue(username, () -> computeGrants(subject)) - .orElseGet(LinkedList::new); - - List result = addAdditionalGrants(subject) - .map(newGrants -> { - List newList = new LinkedList<>(grants); - newList.addAll(newGrants); - return newList; - }) - .orElseGet(() -> new LinkedList<>(grants)); - + Optional> grants = roleCache.computeValue(username, Optional::empty); + if (grants.isPresent()) { + List allGrants = new ArrayList<>(grants.get()); + allGrants.addAll(addAdditionalGrants(subject, grants.get())); + return buildSubject(subject, allGrants); + } + // we do not have a cached value, we must request it from remote server + // this may trigger multiple times in parallel - rather than creating a map of future for each user + // we leave this be (as the map of futures may be unlimited) + List result = new ArrayList<>(computeGrants(subject)); + List fromCache = roleCache.computeValue(username, () -> Optional.of(List.copyOf(result))) + .orElseGet(List::of); + result.addAll(addAdditionalGrants(subject, fromCache)); return buildSubject(subject, result); } @@ -122,25 +129,21 @@ protected Subject enhance(Subject subject, ProviderRequest request, Authenticati * This implementation gets grants from server {@link #getGrantsFromServer(io.helidon.security.Subject)}. * * @param subject to retrieve roles (or in general {@link io.helidon.security.Grant grants}) - * @return An optional list of grants to be added to the subject + * @return future with grants to be added to the subject */ - protected Optional> computeGrants(Subject subject) { - List result = new LinkedList<>(); - - getGrantsFromServer(subject) - .map(result::addAll); - - return (result.isEmpty() ? Optional.empty() : Optional.of(result)); + protected List computeGrants(Subject subject) { + return getGrantsFromServer(subject); } /** * Extension point to add additional grants that are not retrieved from IDCS. * * @param subject subject to enhance + * @param idcsGrants grants obtained from IDCS * @return grants to add to the subject */ - protected Optional> addAdditionalGrants(Subject subject) { - return Optional.empty(); + protected List addAdditionalGrants(Subject subject, List idcsGrants) { + return List.of(); } /** @@ -149,39 +152,55 @@ protected Optional> addAdditionalGrants(Subject subject) { * @param subject to get grants for * @return optional list of grants to be added */ - protected Optional> getGrantsFromServer(Subject subject) { + protected List getGrantsFromServer(Subject subject) { String subjectName = subject.principal().getName(); String subjectType = (String) subject.principal().abacAttribute("sub_type").orElse(defaultIdcsSubjectType()); RoleMapTracing tracing = SecurityTracing.get().roleMapTracing("idcs"); - return appToken.getToken(tracing).flatMap(appToken -> { - JsonObjectBuilder requestBuilder = JSON.createObjectBuilder() - .add("mappingAttributeValue", subjectName) - .add("subjectType", subjectType) - .add("includeMemberships", true); - - JsonArrayBuilder arrayBuilder = JSON.createArrayBuilder(); - arrayBuilder.add("urn:ietf:params:scim:schemas:oracle:idcs:Asserter"); - requestBuilder.add("schemas", arrayBuilder); - - try { - Invocation.Builder reqBuilder = assertEndpoint.request(); - tracing.findParent() - .ifPresent(spanContext -> reqBuilder.property(PARENT_CONTEXT_CLIENT_PROPERTY, spanContext)); - - Response groupResponse = reqBuilder - .header("Authorization", "Bearer " + appToken) - .post(Entity.json(requestBuilder.build())); - - return processServerResponse(groupResponse, subjectName); - } catch (Exception e) { - tracing.error(e); - throw e; - } finally { - tracing.finish(); - } - }); + try { + List grants = appToken.getToken(tracing) + .map(appToken -> obtainGrantsFromServer(subjectName, subjectType, tracing)) + .orElseThrow(() -> new SecurityException("Application token not available")); + + tracing.finish(); + + return grants; + } catch (Exception e) { + tracing.error(e); + throw e; + } + } + + private List obtainGrantsFromServer(String subjectName, String subjectType, RoleMapTracing tracing) { + JsonObjectBuilder requestBuilder = JSON.createObjectBuilder() + .add("mappingAttributeValue", subjectName) + .add("subjectType", subjectType) + .add("includeMemberships", true); + + JsonArrayBuilder arrayBuilder = JSON.createArrayBuilder(); + arrayBuilder.add("urn:ietf:params:scim:schemas:oracle:idcs:Asserter"); + requestBuilder.add("schemas", arrayBuilder); + + // use current span context as a parent for client outbound + // using a custom child context, so we do not replace the parent in the current context + Context parentContext = Contexts.context().orElseGet(Contexts::globalContext); + Context childContext = Context.builder() + .parent(parentContext) + .build(); + + tracing.findParent() + .ifPresent(childContext::register); + + Http1ClientRequest request = oidcConfig().generalWebClient() + .post() + .uri(asserterUri) + .headers(it -> { + it.add(Http.Header.AUTHORIZATION, "Bearer " + appToken); + return it; + }); + + return processRoleRequest(request, requestBuilder.build(), subjectName); } /** @@ -189,6 +208,9 @@ protected Optional> getGrantsFromServer(Subject subject) { * * @param type of builder extending this builder */ + @Configured(prefix = IdcsRoleMapperProviderService.PROVIDER_CONFIG_KEY, + description = "IDCS role mapping provider", + provides = {SecurityProvider.class, SubjectMappingProvider.class}) public static class Builder> extends IdcsRoleMapperProviderBase.Builder> implements io.helidon.common.Builder, IdcsRoleMapperProvider> { private EvictableCache> roleCache; @@ -214,8 +236,9 @@ public IdcsRoleMapperProvider build() { * Update this builder state from configuration. * Expects: *
      - *
    • oidc-config to load an instance of {@link OidcConfig}
    • - *
    • cache-config (optional) to load an instance of {@link EvictableCache} for role caching
    • + *
    • oidc-config to load an instance of {@link io.helidon.security.providers.oidc.common.OidcConfig}
    • + *
    • cache-config (optional) to load an instance of {@link io.helidon.security.providers.common.EvictableCache} for + * role caching
    • *
    * * @param config current node must have "oidc-config" as one of its children @@ -229,11 +252,12 @@ public B config(Config config) { } /** - * Use explicit {@link EvictableCache} for role caching. + * Use explicit {@link io.helidon.security.providers.common.EvictableCache} for role caching. * * @param roleCache cache to use * @return update builder instance */ + @ConfiguredOption(key = "cache-config", type = EvictableCache.class) public B roleCache(EvictableCache> roleCache) { this.roleCache = roleCache; return me; diff --git a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperProviderBase.java b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperProviderBase.java index de49e77f5b4..24913c4c374 100644 --- a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperProviderBase.java +++ b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperProviderBase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,28 @@ package io.helidon.security.providers.idcs.mapper; import java.lang.System.Logger.Level; +import java.net.URI; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; -import java.util.LinkedList; import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicReference; +import io.helidon.common.LazyValue; +import io.helidon.common.context.Context; +import io.helidon.common.context.Contexts; +import io.helidon.common.http.Http; +import io.helidon.common.parameters.Parameters; import io.helidon.config.Config; +import io.helidon.config.metadata.Configured; +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.nima.webclient.http1.Http1Client; +import io.helidon.nima.webclient.http1.Http1ClientRequest; +import io.helidon.nima.webclient.http1.Http1ClientResponse; import io.helidon.security.AuthenticationResponse; import io.helidon.security.Grant; import io.helidon.security.ProviderRequest; @@ -35,41 +47,29 @@ import io.helidon.security.integration.common.RoleMapTracing; import io.helidon.security.jwt.Jwt; import io.helidon.security.jwt.SignedJwt; +import io.helidon.security.jwt.Validator; import io.helidon.security.providers.oidc.common.OidcConfig; import io.helidon.security.spi.SubjectMappingProvider; import jakarta.json.JsonArray; import jakarta.json.JsonObject; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.client.Invocation; -import jakarta.ws.rs.client.WebTarget; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.MultivaluedHashMap; -import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.core.Response; /** - * Common functionality for IDCS role mapping. - * - * @deprecated use {@link io.helidon.security.providers.idcs.mapper.IdcsRoleMapperRxProviderBase} instead + * Common functionality for IDCS role mapping using {@link io.helidon.nima.webclient.http1.Http1Client}. */ -@Deprecated(forRemoval = true, since = "2.4.0") public abstract class IdcsRoleMapperProviderBase implements SubjectMappingProvider { /** * User subject type used when requesting roles from IDCS. * An attempt is made to obtain it from JWT claim {@code sub_type}. If not defined, - * default is used as configured in {@link io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProviderBase.Builder}. + * default is used as configured in {@link IdcsRoleMapperProviderBase.Builder}. */ public static final String IDCS_SUBJECT_TYPE_USER = "user"; /** * Client subject type used when requesting roles from IDCS. * An attempt is made to obtain it from JWT claim {@code sub_type}. If not defined, - * default is used as configured in {@link io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProviderBase.Builder}. + * default is used as configured in {@link IdcsRoleMapperProviderBase.Builder}. */ public static final String IDCS_SUBJECT_TYPE_CLIENT = "client"; - - private static final System.Logger LOGGER = System.getLogger(IdcsRoleMapperProviderBase.class.getName()); - /** * Json key for group roles to be retrieved from IDCS response. */ @@ -87,8 +87,7 @@ public abstract class IdcsRoleMapperProviderBase implements SubjectMappingProvid * We cannot use the constant declared in {@code ClientTracingFilter}, as it is not a required dependency. */ protected static final String PARENT_CONTEXT_CLIENT_PROPERTY = "io.helidon.tracing.span-context"; - - private static final int STATUS_NOT_AUTHENTICATED = 401; + private static final System.Logger LOGGER = System.getLogger(IdcsRoleMapperProviderBase.class.getName()); private final Set supportedTypes = EnumSet.noneOf(SubjectType.class); private final OidcConfig oidcConfig; @@ -101,6 +100,7 @@ public abstract class IdcsRoleMapperProviderBase implements SubjectMappingProvid */ protected IdcsRoleMapperProviderBase(Builder builder) { this.oidcConfig = builder.oidcConfig; + this.oidcConfig.tokenEndpointUri(); //Remove once IDCS is rewritten to be lazily loaded this.defaultIdcsSubjectType = builder.defaultIdcsSubjectType; if (builder.supportedTypes.isEmpty()) { this.supportedTypes.add(SubjectType.USER); @@ -110,64 +110,50 @@ protected IdcsRoleMapperProviderBase(Builder builder) { } @Override - public CompletionStage map(ProviderRequest authenticatedRequest, - AuthenticationResponse previousResponse) { + public AuthenticationResponse map(ProviderRequest authenticatedRequest, AuthenticationResponse previousResponse) { Optional maybeUser = previousResponse.user(); Optional maybeService = previousResponse.service(); - if (!maybeService.isPresent() && !maybeUser.isPresent()) { - return complete(previousResponse); + if (maybeService.isEmpty() && maybeUser.isEmpty()) { + return previousResponse; } // create a new response - AuthenticationResponse.Builder builder = AuthenticationResponse.builder(); - - maybeUser - .map(subject -> { - if (supportedTypes.contains(SubjectType.USER)) { - return enhance(subject, authenticatedRequest, previousResponse); - } else { - return subject; - } - }) - .ifPresent(builder::user); - - maybeService - .map(subject -> { - if (supportedTypes.contains(SubjectType.SERVICE)) { - return enhance(subject, authenticatedRequest, previousResponse); - } else { - return subject; - } - }) - .ifPresent(builder::service); - + AuthenticationResponse.Builder builder = AuthenticationResponse.builder() + .requestHeaders(previousResponse.requestHeaders()); previousResponse.description().ifPresent(builder::description); - builder.requestHeaders(previousResponse.requestHeaders()); - return complete(builder.build()); - } + if (maybeUser.isPresent()) { + if (supportedTypes.contains(SubjectType.USER)) { + Subject subject = enhance(authenticatedRequest, previousResponse, maybeUser.get()); + builder.user(subject); + } else { + builder.service(maybeUser.get()); + } + } - /** - * Create a {@link java.util.concurrent.CompletionStage} with the provided response as its completion. - * - * @param response authentication response to complete with - * @return stage completed with the response - */ - protected CompletionStage complete(AuthenticationResponse response) { - return CompletableFuture.completedFuture(response); + if (maybeService.isPresent()) { + if (supportedTypes.contains(SubjectType.SERVICE)) { + Subject subject = enhance(authenticatedRequest, previousResponse, maybeService.get()); + builder.user(subject); + } else { + builder.service(maybeService.get()); + } + } + + return builder.build(); } /** - * Enhance subject with IDCS roles. + * Enhance subject with IDCS roles, reactive. * - * @param subject subject of the user (never null) - * @param request provider request - * @param previousResponse authenticated response (never null) - * @return stage with the new authentication response + * @param request provider request + * @param previousResponse authenticated response + * @param subject subject to enhance + * @return future with enhanced subject */ - protected abstract Subject enhance(Subject subject, ProviderRequest request, AuthenticationResponse previousResponse); + protected abstract Subject enhance(ProviderRequest request, AuthenticationResponse previousResponse, Subject subject); /** * Updates original subject with the list of grants. @@ -185,70 +171,40 @@ protected Subject buildSubject(Subject originalSubject, List gr return builder.build(); } - /** - * Process the server response to retrieve groups and app roles from it. - * - * @param groupResponse response from IDCS - * @param subjectName name of the subject - * @return list of grants obtained from the IDCS response - */ - protected Optional> processServerResponse(Response groupResponse, String subjectName) { - Response.StatusType statusInfo = groupResponse.getStatusInfo(); - if (statusInfo.getFamily() == Response.Status.Family.SUCCESSFUL) { - JsonObject jsonObject = groupResponse.readEntity(JsonObject.class); - JsonArray groups = jsonObject.getJsonArray("groups"); - JsonArray appRoles = jsonObject.getJsonArray("appRoles"); - - if ((null == groups) && (null == appRoles)) { - LOGGER.log(Level.TRACE, () -> "Neither groups nor app roles found for user " + subjectName); - return Optional.empty(); - } - - List result = new LinkedList<>(); - for (String type : Arrays.asList(ROLE_GROUP, ROLE_APPROLE)) { - JsonArray types = jsonObject.getJsonArray(type); - if (null != types) { - for (int i = 0; i < types.size(); i++) { - JsonObject typeJson = types.getJsonObject(i); - String name = typeJson.getString("display"); - String id = typeJson.getString("value"); - String ref = typeJson.getString("$ref"); - - Role role = Role.builder() - .name(name) - .addAttribute("type", type) - .addAttribute("id", id) - .addAttribute("ref", ref) - .build(); - - result.add(role); - } + protected List processRoleRequest(Http1ClientRequest request, Object entity, String subjectName) { + try (Http1ClientResponse response = request.submit(entity)) { + if (response.status().family() == Http.Status.Family.SUCCESSFUL) { + try { + JsonObject jsonObject = response.as(JsonObject.class); + return processServerResponse(jsonObject, subjectName); + } catch (Exception e) { + LOGGER.log(Level.WARNING, + "Cannot read groups for user \"" + subjectName + "\". " + + "Error message: Failed to read JSON from response", + e); } - } - - return Optional.of(result); - } else { - if (statusInfo.getStatusCode() == STATUS_NOT_AUTHENTICATED) { - // most likely not allowed to do this - LOGGER.log(Level.WARNING, "Cannot read groups for user \"" - + subjectName - + "\". Response code: " - + groupResponse.getStatus() - + ", make sure your IDCS client has role \"Authenticator Client\" added on the client" - + " configuration page" - + ", entity: " - + groupResponse.readEntity(String.class)); } else { - LOGGER.log(Level.WARNING, "Cannot read groups for user \"" - + subjectName - + "\". Response code: " - + groupResponse.getStatus() - + ", entity: " - + groupResponse.readEntity(String.class)); + String message; + try { + message = response.as(String.class); + LOGGER.log(Level.WARNING, "Cannot read groups for user \"" + subjectName + "\". " + + "Response code: " + response.status() + + (response.status() == Http.Status.UNAUTHORIZED_401 ? ", make sure your IDCS client has role " + + "\"Authenticator Client\" added on the client configuration page" : "") + + ", error entity: " + message); + } catch (Exception e) { + LOGGER.log(Level.WARNING, + "Cannot read groups for user \"" + subjectName + "\". " + + "Error message: Failed to process error entity", + e); + } } - - return Optional.empty(); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Cannot read groups for user \"" + subjectName + "\". " + + "Error message: Failed to invoke request", + e); } + return List.of(); } /** @@ -270,20 +226,53 @@ protected String defaultIdcsSubjectType() { return defaultIdcsSubjectType; } + private List processServerResponse(JsonObject jsonObject, String subjectName) { + JsonArray groups = jsonObject.getJsonArray("groups"); + JsonArray appRoles = jsonObject.getJsonArray("appRoles"); + + if ((null == groups) && (null == appRoles)) { + LOGGER.log(Level.TRACE, () -> "Neither groups nor app roles found for user " + subjectName); + return List.of(); + } + + List result = new ArrayList<>(); + for (String type : Arrays.asList(ROLE_GROUP, ROLE_APPROLE)) { + JsonArray types = jsonObject.getJsonArray(type); + if (null != types) { + for (int i = 0; i < types.size(); i++) { + JsonObject typeJson = types.getJsonObject(i); + String name = typeJson.getString("display"); + String id = typeJson.getString("value"); + String ref = typeJson.getString("$ref"); + + Role role = Role.builder() + .name(name) + .addAttribute("type", type) + .addAttribute("id", id) + .addAttribute("ref", ref) + .build(); + + result.add(role); + } + } + } + + return result; + } + /** - * Fluent API builder for {@link io.helidon.security.providers.idcs.mapper.IdcsRoleMapperProviderBase}. + * Fluent API builder for {@link IdcsRoleMapperProviderBase}. * @param Type of the extending builder */ + @Configured public static class Builder> { private final Set supportedTypes = EnumSet.noneOf(SubjectType.class); + @SuppressWarnings("unchecked") + private final B me = (B) this; private String defaultIdcsSubjectType = IDCS_SUBJECT_TYPE_USER; - private OidcConfig oidcConfig; - @SuppressWarnings("unchecked") - private B me = (B) this; - /** * Default constructor. */ @@ -329,6 +318,7 @@ public B config(Config config) { * @param config oidc specific configuration, must have at least identity endpoint and client credentials configured * @return updated builder instance */ + @ConfiguredOption public B oidcConfig(OidcConfig config) { this.oidcConfig = config; return me; @@ -363,6 +353,7 @@ public B subjectTypes(SubjectType... types) { * @param subjectType type of subject to use when requesting roles from IDCS * @return updated builder instance */ + @ConfiguredOption(IDCS_SUBJECT_TYPE_USER) public B defaultIdcsSubjectType(String subjectType) { this.defaultIdcsSubjectType = subjectType; return me; @@ -378,6 +369,7 @@ public B defaultIdcsSubjectType(String subjectType) { * @param type subject type to add to the list of supported types * @return updated builder instance */ + @ConfiguredOption(key = "subject-types", kind = ConfiguredOption.Kind.LIST, value = "USER") public B addSubjectType(SubjectType type) { this.supportedTypes.add(type); return me; @@ -385,69 +377,135 @@ public B addSubjectType(SubjectType type) { } /** - * A token for app access to IDCS. + * Reactive token for app access to IDCS. */ protected static class AppToken { - private final WebTarget tokenEndpoint; - // caching application token (as that can be re-used for group requests) - private Optional tokenContent = Optional.empty(); - private Jwt appJwt; + private static final List> TIME_VALIDATORS = Jwt.defaultTimeValidators(); - /** - * Create a new token with a token endpoint. - * - * @param tokenEndpoint used to get a new token from IDCS - */ - protected AppToken(WebTarget tokenEndpoint) { - this.tokenEndpoint = tokenEndpoint; + private final AtomicReference> token = new AtomicReference<>(); + private final Http1Client webClient; + private final URI tokenEndpointUri; + private final Duration tokenRefreshSkew; + + protected AppToken(Http1Client webClient, URI tokenEndpointUri, Duration tokenRefreshSkew) { + this.webClient = webClient; + this.tokenEndpointUri = tokenEndpointUri; + this.tokenRefreshSkew = tokenRefreshSkew; } - /** - * Get the token to use for requests to IDCS. - * @param tracing tracing to use when requesting a new token from server - * @return token content or empty if it could not be obtained - */ - protected synchronized Optional getToken(RoleMapTracing tracing) { - if (null == appJwt) { - fromServer(tracing); - } else { - if (!appJwt.validate(Jwt.defaultTimeValidators()).isValid()) { - fromServer(tracing); + protected Optional getToken(RoleMapTracing tracing) { + LazyValue currentTokenData = token.get(); + if (currentTokenData == null) { + LazyValue newLazyValue = LazyValue.create(() -> fromServer(tracing)); + if (token.compareAndSet(null, newLazyValue)) { + currentTokenData = newLazyValue; + } else { + // another thread "stole" the data, return its future + currentTokenData = token.get(); } + return currentTokenData.get().tokenContent(); } - return tokenContent; + + AppTokenData tokenData = currentTokenData.get(); + Jwt jwt = tokenData.appJwt(); + if (jwt == null + || !jwt.validate(TIME_VALIDATORS).isValid() + || isNearExpiration(jwt)) { + // it is not valid or is very close to expiration - we must get a new value + LazyValue newLazyValue = LazyValue.create(() -> fromServer(tracing)); + if (token.compareAndSet(currentTokenData, newLazyValue)) { + currentTokenData = newLazyValue; + } else { + // another thread "stole" the data, return its future + currentTokenData = token.get(); + } + return currentTokenData.get().tokenContent(); + } else { + // present and valid + return tokenData.tokenContent(); + } + } + + private boolean isNearExpiration(Jwt jwt) { + return jwt.expirationTime() + .map(exp -> exp.minus(tokenRefreshSkew).isBefore(Instant.now())) + .orElse(false); } - private void fromServer(RoleMapTracing tracing) { - MultivaluedMap formData = new MultivaluedHashMap<>(); - formData.putSingle("grant_type", "client_credentials"); - formData.putSingle("scope", "urn:opc:idm:__myscopes__"); + private AppTokenData fromServer(RoleMapTracing tracing) { + Parameters params = Parameters.builder("idcs-form-params") + .add("grant_type", "client_credentials") + .add("scope", "urn:opc:idm:__myscopes__") + .build(); - Invocation.Builder reqBuilder = tokenEndpoint.request(); + // use current span context as a parent for client outbound + // using a custom child context, so we do not replace the parent in the current context + Context parentContext = Contexts.context().orElseGet(Contexts::globalContext); + Context childContext = Context.builder() + .parent(parentContext) + .build(); tracing.findParent() - .ifPresent(spanContext -> reqBuilder.property(PARENT_CONTEXT_CLIENT_PROPERTY, spanContext)); + .ifPresent(childContext::register); + + Http1ClientRequest request = webClient.post() + .uri(tokenEndpointUri) + .header(Http.HeaderValues.ACCEPT_JSON); + + try (Http1ClientResponse response = request.submit(params)) { + if (response.status().family() == Http.Status.Family.SUCCESSFUL) { + try { + JsonObject jsonObject = response.as(JsonObject.class); + String accessToken = jsonObject.getString(ACCESS_TOKEN_KEY); + LOGGER.log(Level.TRACE, () -> "Access token: " + accessToken); + SignedJwt signedJwt = SignedJwt.parseToken(accessToken); + return new AppTokenData(accessToken, signedJwt.getJwt()); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Failed to obtain access token for application to read " + + "groups from IDCS. Failed with exception: Failed to read JSON from response", + e); + } + } else { + String message; + try { + message = response.as(String.class); + LOGGER.log(Level.WARNING, "Failed to obtain access token for application to read " + + "groups from IDCS. Status: " + response.status() + ", error message: " + message); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Failed to obtain access token for application to read " + + "groups from IDCS. Failed with exception: Failed to process error entity", + e); + } + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Failed to obtain access token for application to read " + + "groups from IDCS. Failed with exception: Failed to invoke request", + e); + } + return new AppTokenData(); + } + } - Response tokenResponse = reqBuilder - .accept(MediaType.APPLICATION_JSON_TYPE) - .post(Entity.form(formData)); + private static final class AppTokenData { + private final Optional tokenContent; + private final Jwt appJwt; - if (tokenResponse.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) { - JsonObject response = tokenResponse.readEntity(JsonObject.class); - String accessToken = response.getString(ACCESS_TOKEN_KEY); - LOGGER.log(Level.TRACE, () -> "Access token: " + accessToken); - SignedJwt signedJwt = SignedJwt.parseToken(accessToken); + AppTokenData() { + this.tokenContent = Optional.empty(); + this.appJwt = null; + } - this.tokenContent = Optional.of(accessToken); - this.appJwt = signedJwt.getJwt(); - } else { - LOGGER.log(Level.ERROR, "Failed to obtain access token for application to read groups" - + " from IDCS. Response code: " + tokenResponse.getStatus() + ", entity: " - + tokenResponse.readEntity(String.class)); - this.tokenContent = Optional.empty(); - this.appJwt = null; - } + AppTokenData(String tokenContent, Jwt appJwt) { + this.tokenContent = Optional.ofNullable(tokenContent); + this.appJwt = appJwt; } - } + Optional tokenContent() { + return tokenContent; + } + + Jwt appJwt() { + return appJwt; + } + } } diff --git a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperProviderService.java b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperProviderService.java index 23656c844d4..2a0d071d478 100644 --- a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperProviderService.java +++ b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperProviderService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import io.helidon.security.spi.SecurityProviderService; /** - * Service for {@link IdcsRoleMapperRxProvider}. + * Service for {@link IdcsRoleMapperProvider}. */ public class IdcsRoleMapperProviderService implements SecurityProviderService { @@ -34,16 +34,16 @@ public String providerConfigKey() { // This is for backward compatibility only. This will be changed in 3.x @Override public Class providerClass() { - return IdcsRoleMapperProvider.class; + return IdcsMtRoleMapperProvider.class; } @Override public SecurityProvider providerInstance(Config config) { if (config.get("multitenant").asBoolean().orElse(true)) { - return IdcsMtRoleMapperRxProvider.create(config); + return IdcsMtRoleMapperProvider.create(config); } // we now use the new reactive implementation by default // the behavior is backward compatible (and configuration as well) - return IdcsRoleMapperRxProvider.create(config); + return IdcsRoleMapperProvider.create(config); } } diff --git a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProvider.java b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProvider.java deleted file mode 100644 index c4d401e9773..00000000000 --- a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProvider.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.security.providers.idcs.mapper; - -import java.net.URI; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; - -import io.helidon.common.context.Context; -import io.helidon.common.context.Contexts; -import io.helidon.common.http.Http; -import io.helidon.common.reactive.Single; -import io.helidon.config.Config; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.reactive.webclient.WebClientRequestBuilder; -import io.helidon.security.AuthenticationResponse; -import io.helidon.security.Grant; -import io.helidon.security.ProviderRequest; -import io.helidon.security.SecurityException; -import io.helidon.security.Subject; -import io.helidon.security.integration.common.RoleMapTracing; -import io.helidon.security.integration.common.SecurityTracing; -import io.helidon.security.providers.common.EvictableCache; -import io.helidon.security.providers.oidc.common.OidcConfig; -import io.helidon.security.spi.SecurityProvider; -import io.helidon.security.spi.SubjectMappingProvider; - -import jakarta.json.Json; -import jakarta.json.JsonArrayBuilder; -import jakarta.json.JsonBuilderFactory; -import jakarta.json.JsonObjectBuilder; - -/** - * {@link io.helidon.security.spi.SubjectMappingProvider} to obtain roles from IDCS server for a user. - * Supports multi tenancy in IDCS. - */ -public class IdcsRoleMapperRxProvider extends IdcsRoleMapperRxProviderBase implements SubjectMappingProvider { - private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); - - private final EvictableCache> roleCache; - private final String asserterUri; - private final URI tokenEndpointUri; - - // caching application token (as that can be re-used for group requests) - private final AppTokenRx appToken; - - /** - * Constructor that accepts any {@link IdcsRoleMapperRxProvider.Builder} descendant. - * - * @param builder used to configure this instance - */ - protected IdcsRoleMapperRxProvider(Builder builder) { - super(builder); - - this.roleCache = builder.roleCache; - OidcConfig oidcConfig = builder.oidcConfig(); - - this.asserterUri = oidcConfig.identityUri() + "/admin/v1/Asserter"; - this.tokenEndpointUri = oidcConfig.tokenEndpointUri(); - - this.appToken = new AppTokenRx(oidcConfig.appWebClient(), tokenEndpointUri, oidcConfig.tokenRefreshSkew()); - } - - /** - * Creates a new builder to build instances of this class. - * - * @return a new fluent API builder. - */ - public static Builder builder() { - return new Builder<>(); - } - - /** - * Creates an instance from configuration. - *

    - * Expects: - *

      - *
    • oidc-config to load an instance of {@link io.helidon.security.providers.oidc.common.OidcConfig}
    • - *
    • cache-config (optional) to load an instance of {@link io.helidon.security.providers.common.EvictableCache} for role - * caching
    • - *
    - * - * @param config configuration of this provider - * @return a new instance configured from config - */ - public static SecurityProvider create(Config config) { - return builder().config(config).build(); - } - - @Override - protected Single enhance(ProviderRequest request, - AuthenticationResponse previousResponse, - Subject subject) { - String username = subject.principal().getName(); - - Optional> grants = roleCache.computeValue(username, Optional::empty); - if (grants.isPresent()) { - return addAdditionalGrants(subject, grants.get()) - .map(it -> { - List allGrants = new LinkedList<>(grants.get()); - allGrants.addAll(it); - return buildSubject(subject, allGrants); - }); - } - // we do not have a cached value, we must request it from remote server - // this may trigger multiple times in parallel - rather than creating a map of future for each user - // we leave this be (as the map of futures may be unlimited) - List result = new LinkedList<>(); - return computeGrants(subject) - .map(it -> { - result.addAll(it); - return result; - }) - .map(newGrants -> roleCache.computeValue(username, () -> Optional.of(List.copyOf(newGrants))) - .orElseGet(List::of)) - // additional grants may not be cached (leave this decision to overriding class) - .flatMapSingle(it -> addAdditionalGrants(subject, it)) - .map(newGrants -> { - result.addAll(newGrants); - return result; - }) - .map(it -> buildSubject(subject, it)); - } - - /** - * Compute grants for the provided subject. - * This implementation gets grants from server {@link #getGrantsFromServer(io.helidon.security.Subject)}. - * - * @param subject to retrieve roles (or in general {@link io.helidon.security.Grant grants}) - * @return future with grants to be added to the subject - */ - protected Single> computeGrants(Subject subject) { - return getGrantsFromServer(subject); - } - - /** - * Extension point to add additional grants that are not retrieved from IDCS. - * - * @param subject subject to enhance - * @param idcsGrants grants obtained from IDCS - * @return grants to add to the subject - */ - protected Single> addAdditionalGrants(Subject subject, - List idcsGrants) { - return Single.just(List.of()); - } - - /** - * Retrieves grants from IDCS server. - * - * @param subject to get grants for - * @return optional list of grants to be added - */ - protected Single> getGrantsFromServer(Subject subject) { - String subjectName = subject.principal().getName(); - String subjectType = (String) subject.principal().abacAttribute("sub_type").orElse(defaultIdcsSubjectType()); - - RoleMapTracing tracing = SecurityTracing.get().roleMapTracing("idcs"); - - return Single.create(appToken.getToken(tracing)) - .flatMapSingle(maybeAppToken -> { - if (maybeAppToken.isEmpty()) { - return Single.error(new SecurityException("Application token not available")); - } - String appToken = maybeAppToken.get(); - JsonObjectBuilder requestBuilder = JSON.createObjectBuilder() - .add("mappingAttributeValue", subjectName) - .add("subjectType", subjectType) - .add("includeMemberships", true); - - JsonArrayBuilder arrayBuilder = JSON.createArrayBuilder(); - arrayBuilder.add("urn:ietf:params:scim:schemas:oracle:idcs:Asserter"); - requestBuilder.add("schemas", arrayBuilder); - - // use current span context as a parent for client outbound - // using a custom child context, so we do not replace the parent in the current context - Context parentContext = Contexts.context().orElseGet(Contexts::globalContext); - Context childContext = Context.builder() - .parent(parentContext) - .build(); - - tracing.findParent() - .ifPresent(childContext::register); - - WebClientRequestBuilder request = oidcConfig().generalWebClient() - .post() - .uri(asserterUri) - .context(childContext) - .headers(it -> { - it.add(Http.Header.AUTHORIZATION, "Bearer " + appToken); - return it; - }); - - return processRoleRequest(request, - requestBuilder.build(), - subjectName); - }) - .peek(ignored -> tracing.finish()) - .onError(tracing::error); - } - - /** - * Fluent API builder for {@link IdcsRoleMapperRxProvider}. - * - * @param type of builder extending this builder - */ - @Configured(prefix = IdcsRoleMapperProviderService.PROVIDER_CONFIG_KEY, - description = "IDCS role mapping provider", - provides = {SecurityProvider.class, SubjectMappingProvider.class}) - public static class Builder> extends IdcsRoleMapperRxProviderBase.Builder> - implements io.helidon.common.Builder, IdcsRoleMapperRxProvider> { - private EvictableCache> roleCache; - - @SuppressWarnings("unchecked") - private B me = (B) this; - - /** - * Default contructor. - */ - protected Builder() { - } - - @Override - public IdcsRoleMapperRxProvider build() { - if (null == roleCache) { - roleCache = EvictableCache.create(); - } - return new IdcsRoleMapperRxProvider(this); - } - - /** - * Update this builder state from configuration. - * Expects: - *
      - *
    • oidc-config to load an instance of {@link io.helidon.security.providers.oidc.common.OidcConfig}
    • - *
    • cache-config (optional) to load an instance of {@link io.helidon.security.providers.common.EvictableCache} for - * role caching
    • - *
    - * - * @param config current node must have "oidc-config" as one of its children - * @return updated builder instance - */ - public B config(Config config) { - super.config(config); - config.get("cache-config").as(EvictableCache::>create).ifPresent(this::roleCache); - - return me; - } - - /** - * Use explicit {@link io.helidon.security.providers.common.EvictableCache} for role caching. - * - * @param roleCache cache to use - * @return update builder instance - */ - @ConfiguredOption(key = "cache-config", type = EvictableCache.class) - public B roleCache(EvictableCache> roleCache) { - this.roleCache = roleCache; - return me; - } - } -} diff --git a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProviderBase.java b/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProviderBase.java deleted file mode 100644 index 6817c8f56fd..00000000000 --- a/security/providers/idcs-mapper/src/main/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProviderBase.java +++ /dev/null @@ -1,503 +0,0 @@ -/* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.security.providers.idcs.mapper; - -import java.lang.System.Logger.Level; -import java.net.URI; -import java.time.Duration; -import java.time.Instant; -import java.util.Arrays; -import java.util.EnumSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; - -import io.helidon.common.context.Context; -import io.helidon.common.context.Contexts; -import io.helidon.common.http.Http; -import io.helidon.common.http.HttpMediaType; -import io.helidon.common.parameters.Parameters; -import io.helidon.common.reactive.Single; -import io.helidon.config.Config; -import io.helidon.config.metadata.Configured; -import io.helidon.config.metadata.ConfiguredOption; -import io.helidon.reactive.webclient.WebClient; -import io.helidon.reactive.webclient.WebClientRequestBuilder; -import io.helidon.security.AuthenticationResponse; -import io.helidon.security.Grant; -import io.helidon.security.ProviderRequest; -import io.helidon.security.Role; -import io.helidon.security.Subject; -import io.helidon.security.SubjectType; -import io.helidon.security.integration.common.RoleMapTracing; -import io.helidon.security.jwt.Jwt; -import io.helidon.security.jwt.SignedJwt; -import io.helidon.security.jwt.Validator; -import io.helidon.security.providers.oidc.common.OidcConfig; -import io.helidon.security.spi.SubjectMappingProvider; - -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; - -import static io.helidon.security.providers.oidc.common.OidcConfig.postJsonResponse; - -/** - * Common functionality for IDCS role mapping using reactive {@link io.helidon.reactive.webclient.WebClient}. - */ -public abstract class IdcsRoleMapperRxProviderBase implements SubjectMappingProvider { - /** - * User subject type used when requesting roles from IDCS. - * An attempt is made to obtain it from JWT claim {@code sub_type}. If not defined, - * default is used as configured in {@link IdcsRoleMapperRxProviderBase.Builder}. - */ - public static final String IDCS_SUBJECT_TYPE_USER = "user"; - /** - * Client subject type used when requesting roles from IDCS. - * An attempt is made to obtain it from JWT claim {@code sub_type}. If not defined, - * default is used as configured in {@link IdcsRoleMapperRxProviderBase.Builder}. - */ - public static final String IDCS_SUBJECT_TYPE_CLIENT = "client"; - /** - * Json key for group roles to be retrieved from IDCS response. - */ - protected static final String ROLE_GROUP = "groups"; - /** - * Json key for app roles to be retrieved from IDCS response. - */ - protected static final String ROLE_APPROLE = "appRoles"; - /** - * Json key for token to be retrieved from IDCS response when requesting application token. - */ - protected static final String ACCESS_TOKEN_KEY = "access_token"; - /** - * Property sent with JAX-RS requests to override parent span context in outbound calls. - * We cannot use the constant declared in {@code ClientTracingFilter}, as it is not a required dependency. - */ - protected static final String PARENT_CONTEXT_CLIENT_PROPERTY = "io.helidon.tracing.span-context"; - private static final System.Logger LOGGER = System.getLogger(IdcsRoleMapperRxProviderBase.class.getName()); - - private final Set supportedTypes = EnumSet.noneOf(SubjectType.class); - private final OidcConfig oidcConfig; - private final String defaultIdcsSubjectType; - - /** - * Configures the needed fields from the provided builder. - * - * @param builder builder with oidcConfig and other needed fields. - */ - protected IdcsRoleMapperRxProviderBase(Builder builder) { - this.oidcConfig = builder.oidcConfig; - this.oidcConfig.tokenEndpointUri(); //Remove once IDCS is rewritten to be lazily loaded - this.defaultIdcsSubjectType = builder.defaultIdcsSubjectType; - if (builder.supportedTypes.isEmpty()) { - this.supportedTypes.add(SubjectType.USER); - } else { - this.supportedTypes.addAll(builder.supportedTypes); - } - } - - @Override - public Single map(ProviderRequest authenticatedRequest, - AuthenticationResponse previousResponse) { - - Optional maybeUser = previousResponse.user(); - Optional maybeService = previousResponse.service(); - - if (maybeService.isEmpty() && maybeUser.isEmpty()) { - return Single.just(previousResponse); - } - - // create a new response - AuthenticationResponse.Builder builder = AuthenticationResponse.builder() - .requestHeaders(previousResponse.requestHeaders()); - previousResponse.description().ifPresent(builder::description); - - Single result = Single.just(builder); - - if (maybeUser.isPresent()) { - if (supportedTypes.contains(SubjectType.USER)) { - // service will be done after use - result = result.flatMapSingle(it -> enhance(authenticatedRequest, previousResponse, maybeUser.get()) - .peek(it::user) - .map(ignored -> it)); - } else { - result = result.peek(it -> it.service(maybeUser.get())); - } - } - - if (maybeService.isPresent()) { - if (supportedTypes.contains(SubjectType.SERVICE)) { - result = result.flatMapSingle(it -> enhance(authenticatedRequest, previousResponse, maybeService.get()) - .peek(it::user) - .map(ignored -> it)); - } else { - result = result.peek(it -> it.service(maybeService.get())); - } - } - - return result.map(AuthenticationResponse.Builder::build); - } - - /** - * Enhance subject with IDCS roles, reactive. - * - * @param request provider request - * @param previousResponse authenticated response - * @param subject subject to enhance - * @return future with enhanced subject - */ - protected abstract Single enhance(ProviderRequest request, - AuthenticationResponse previousResponse, - Subject subject); - - /** - * Updates original subject with the list of grants. - * - * @param originalSubject as was created by authentication provider - * @param grants grants added by this role mapper - * @return new subject - */ - protected Subject buildSubject(Subject originalSubject, List grants) { - Subject.Builder builder = Subject.builder(); - builder.update(originalSubject); - - grants.forEach(builder::addGrant); - - return builder.build(); - } - - protected Single> processRoleRequest(WebClientRequestBuilder request, - Object entity, - String subjectName) { - return postJsonResponse(request, - entity, - json -> processServerResponse(json, subjectName), - (status, errorEntity) -> { - LOGGER.log(Level.WARNING, "Cannot read groups for user \"" - + subjectName - + "\". Response code: " - + status - + ( - status == Http.Status.UNAUTHORIZED_401 ? ", make sure your IDCS client has role " - + "\"Authenticator Client\" added on the client configuration page" : "") - + ", error entity: " + errorEntity); - return Optional.of(List.of()); - }, - (t, errorMessage) -> { - LOGGER.log(Level.WARNING, "Cannot read groups for user \"" - + subjectName - + "\". Error message: " + errorMessage, - t); - return Optional.of(List.of()); - }); - } - - /** - * Access to {@link io.helidon.security.providers.oidc.common.OidcConfig} so the field is not duplicated by - * classes that extend this provider. - * - * @return open ID Connect configuration (also used to configure access to IDCS) - */ - protected OidcConfig oidcConfig() { - return oidcConfig; - } - - /** - * Default subject type to use when requesting data from IDCS. - * - * @return configured default subject type or {@link #IDCS_SUBJECT_TYPE_USER} - */ - protected String defaultIdcsSubjectType() { - return defaultIdcsSubjectType; - } - - private List processServerResponse(JsonObject jsonObject, String subjectName) { - JsonArray groups = jsonObject.getJsonArray("groups"); - JsonArray appRoles = jsonObject.getJsonArray("appRoles"); - - if ((null == groups) && (null == appRoles)) { - LOGGER.log(Level.TRACE, () -> "Neither groups nor app roles found for user " + subjectName); - return List.of(); - } - - List result = new LinkedList<>(); - for (String type : Arrays.asList(ROLE_GROUP, ROLE_APPROLE)) { - JsonArray types = jsonObject.getJsonArray(type); - if (null != types) { - for (int i = 0; i < types.size(); i++) { - JsonObject typeJson = types.getJsonObject(i); - String name = typeJson.getString("display"); - String id = typeJson.getString("value"); - String ref = typeJson.getString("$ref"); - - Role role = Role.builder() - .name(name) - .addAttribute("type", type) - .addAttribute("id", id) - .addAttribute("ref", ref) - .build(); - - result.add(role); - } - } - } - - return result; - } - - /** - * Fluent API builder for {@link IdcsRoleMapperRxProviderBase}. - * @param Type of the extending builder - */ - @Configured - public static class Builder> { - - private final Set supportedTypes = EnumSet.noneOf(SubjectType.class); - @SuppressWarnings("unchecked") - private final B me = (B) this; - private String defaultIdcsSubjectType = IDCS_SUBJECT_TYPE_USER; - private OidcConfig oidcConfig; - - /** - * Default constructor. - */ - protected Builder() { - } - - /** - * Update this builder state from configuration. - * Expects: - *
      - *
    • oidc-config to load an instance of {@link io.helidon.security.providers.oidc.common.OidcConfig}
    • - *
    • cache-config (optional) to load instances of {@link io.helidon.security.providers.common.EvictableCache} for - * caching
    • - *
    • default-idcs-subject-type to use when not defined in a JWT, either {@value #IDCS_SUBJECT_TYPE_USER} or - * {@link #IDCS_SUBJECT_TYPE_CLIENT}, defaults to {@value #IDCS_SUBJECT_TYPE_USER}
    • - *
    - * - * @param config current node must have "oidc-config" as one of its children - * @return updated builder instance - */ - public B config(Config config) { - config.get("oidc-config").ifExists(it -> { - OidcConfig.Builder builder = OidcConfig.builder(); - // we do not need JWT validation at all - builder.validateJwtWithJwk(false); - // this is an IDCS specific extension - builder.serverType("idcs"); - builder.config(it); - - oidcConfig(builder.build()); - }); - - config.get("subject-types").asList(cfg -> cfg.asString().map(SubjectType::valueOf).get()) - .ifPresent(list -> list.forEach(this::addSubjectType)); - config.get("default-idcs-subject-type").asString().ifPresent(this::defaultIdcsSubjectType); - return me; - } - - /** - * Use explicit {@link io.helidon.security.providers.oidc.common.OidcConfig} instance, e.g. when using it also for OIDC - * provider. - * - * @param config oidc specific configuration, must have at least identity endpoint and client credentials configured - * @return updated builder instance - */ - @ConfiguredOption - public B oidcConfig(OidcConfig config) { - this.oidcConfig = config; - return me; - } - - /** - * Get the configuration to access IDCS instance. - * @return oidc config - */ - protected OidcConfig oidcConfig() { - return oidcConfig; - } - - /** - * Configure supported subject types. - * By default {@link io.helidon.security.SubjectType#USER} is used if none configured. - * - * @param types types to configure as supported for mapping - * @return updated builder instance - */ - public B subjectTypes(SubjectType... types) { - this.supportedTypes.clear(); - this.supportedTypes.addAll(Arrays.asList(types)); - return me; - } - - /** - * Configure subject type to use when requesting roles from IDCS. - * Can be either {@link #IDCS_SUBJECT_TYPE_USER} or {@link #IDCS_SUBJECT_TYPE_CLIENT}. - * Defaults to {@link #IDCS_SUBJECT_TYPE_USER}. - * - * @param subjectType type of subject to use when requesting roles from IDCS - * @return updated builder instance - */ - @ConfiguredOption(IDCS_SUBJECT_TYPE_USER) - public B defaultIdcsSubjectType(String subjectType) { - this.defaultIdcsSubjectType = subjectType; - return me; - } - - /** - * Add a supported subject type. - * If none added, {@link io.helidon.security.SubjectType#USER} is used. - * If any added, only the ones added will be used (e.g. if you want to use - * both {@link io.helidon.security.SubjectType#USER} and {@link io.helidon.security.SubjectType#SERVICE}, - * both need to be added. - * - * @param type subject type to add to the list of supported types - * @return updated builder instance - */ - @ConfiguredOption(key = "subject-types", kind = ConfiguredOption.Kind.LIST, value = "USER") - public B addSubjectType(SubjectType type) { - this.supportedTypes.add(type); - return me; - } - } - - /** - * Reactive token for app access to IDCS. - */ - protected static class AppTokenRx { - private static final List> TIME_VALIDATORS = Jwt.defaultTimeValidators(); - - private final AtomicReference> token = new AtomicReference<>(); - private final WebClient webClient; - private final URI tokenEndpointUri; - private final Duration tokenRefreshSkew; - - protected AppTokenRx(WebClient webClient, URI tokenEndpointUri, Duration tokenRefreshSkew) { - this.webClient = webClient; - this.tokenEndpointUri = tokenEndpointUri; - this.tokenRefreshSkew = tokenRefreshSkew; - } - - protected Single> getToken(RoleMapTracing tracing) { - final CompletableFuture currentTokenData = token.get(); - if (currentTokenData == null) { - CompletableFuture future = new CompletableFuture<>(); - if (token.compareAndSet(null, future)) { - fromServer(tracing, future); - } else { - // another thread "stole" the data, return its future - future = token.get(); - } - return Single.create(future).map(AppTokenData::tokenContent); - } - // there is an existing value - return Single.create(currentTokenData) - .flatMapSingle(tokenData -> { - Jwt jwt = tokenData.appJwt(); - if (jwt == null - || !jwt.validate(TIME_VALIDATORS).isValid() - || isNearExpiration(jwt)) { - // it is not valid or is very close to expiration - we must get a new value - CompletableFuture future = new CompletableFuture<>(); - if (token.compareAndSet(currentTokenData, future)) { - fromServer(tracing, future); - } else { - future = token.get(); - } - return Single.create(future) - .map(AppTokenData::tokenContent); - } else { - // present and valid - return Single.just(tokenData.tokenContent()); - } - }); - } - - private boolean isNearExpiration(Jwt jwt) { - return jwt.expirationTime() - .map(exp -> exp.minus(tokenRefreshSkew).isBefore(Instant.now())) - .orElse(false); - } - - private void fromServer(RoleMapTracing tracing, CompletableFuture future) { - Parameters params = Parameters.builder("idcs-form-params") - .add("grant_type", "client_credentials") - .add("scope", "urn:opc:idm:__myscopes__") - .build(); - - // use current span context as a parent for client outbound - // using a custom child context, so we do not replace the parent in the current context - Context parentContext = Contexts.context().orElseGet(Contexts::globalContext); - Context childContext = Context.builder() - .parent(parentContext) - .build(); - - tracing.findParent() - .ifPresent(childContext::register); - - WebClientRequestBuilder request = webClient.post() - .uri(tokenEndpointUri) - .context(childContext) - .accept(HttpMediaType.APPLICATION_JSON); - - postJsonResponse(request, - params, - json -> { - String accessToken = json.getString(ACCESS_TOKEN_KEY); - LOGGER.log(Level.TRACE, () -> "Access token: " + accessToken); - SignedJwt signedJwt = SignedJwt.parseToken(accessToken); - return new AppTokenData(accessToken, signedJwt.getJwt()); - }, - (status, message) -> { - LOGGER.log(Level.ERROR, "Failed to obtain access token for application to read " - + "groups from IDCS. Status: " + status + ", error message: " + message); - return Optional.of(new AppTokenData()); - }, - (t, message) -> { - LOGGER.log(Level.ERROR, "Failed to obtain access token for application to read " - + "groups from IDCS. Failed with exception: " + message, t); - return Optional.of(new AppTokenData()); - }) - .forSingle(future::complete); - } - } - - private static final class AppTokenData { - private final Optional tokenContent; - private final Jwt appJwt; - - AppTokenData() { - this.tokenContent = Optional.empty(); - this.appJwt = null; - } - - AppTokenData(String tokenContent, Jwt appJwt) { - this.tokenContent = Optional.ofNullable(tokenContent); - this.appJwt = appJwt; - } - - Optional tokenContent() { - return tokenContent; - } - - Jwt appJwt() { - return appJwt; - } - } -} diff --git a/security/providers/idcs-mapper/src/main/java/module-info.java b/security/providers/idcs-mapper/src/main/java/module-info.java index 3921de5adf4..9469247cc9a 100644 --- a/security/providers/idcs-mapper/src/main/java/module-info.java +++ b/security/providers/idcs-mapper/src/main/java/module-info.java @@ -32,6 +32,7 @@ requires transitive io.helidon.config; requires transitive io.helidon.common; + requires transitive io.helidon.common.context; requires transitive io.helidon.security; requires transitive io.helidon.security.providers.common; requires transitive io.helidon.security.jwt; @@ -41,9 +42,7 @@ requires io.helidon.security.integration.common; requires io.helidon.security.util; - requires io.helidon.reactive.webclient; - requires jersey.client; requires jakarta.ws.rs; exports io.helidon.security.providers.idcs.mapper; diff --git a/security/providers/idcs-mapper/src/test/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProviderTest.java b/security/providers/idcs-mapper/src/test/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperProviderTest.java similarity index 84% rename from security/providers/idcs-mapper/src/test/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProviderTest.java rename to security/providers/idcs-mapper/src/test/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperProviderTest.java index 015861dddcd..7f394374439 100644 --- a/security/providers/idcs-mapper/src/test/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperRxProviderTest.java +++ b/security/providers/idcs-mapper/src/test/java/io/helidon/security/providers/idcs/mapper/IdcsRoleMapperProviderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import io.helidon.common.reactive.Single; import io.helidon.security.AuthenticationResponse; import io.helidon.security.Grant; import io.helidon.security.Principal; @@ -41,12 +40,12 @@ import static org.hamcrest.collection.IsIterableWithSize.iterableWithSize; import static org.junit.jupiter.api.Assertions.fail; -class IdcsRoleMapperRxProviderTest { +class IdcsRoleMapperProviderTest { private static TestProvider provider; @BeforeAll static void prepareProvider() { - IdcsRoleMapperRxProvider.Builder builder = IdcsRoleMapperRxProvider.builder(); + IdcsRoleMapperProvider.Builder builder = IdcsRoleMapperProvider.builder(); builder.oidcConfig(OidcConfig.builder() .oidcMetadataWellKnown(false) .clientId("client-id") @@ -71,9 +70,7 @@ void testCacheUsed() { .user(Subject.builder() .principal(Principal.create(username)) .build()) - .build()) - .toCompletableFuture() - .join(); + .build()); Subject subject = response.user() .get(); @@ -89,9 +86,7 @@ void testCacheUsed() { .user(Subject.builder() .principal(Principal.create(username)) .build()) - .build()) - .toCompletableFuture() - .join(); + .build()); grants = response.user().get().grants(Role.class); assertThat(grants, iterableWithSize(5)); Role counted2 = findCounted(grants); @@ -120,24 +115,24 @@ private Role findAdditionalCounted(List grants) { return null; } - private static final class TestProvider extends IdcsRoleMapperRxProvider { + private static final class TestProvider extends IdcsRoleMapperProvider { private static final AtomicInteger COUNTER = new AtomicInteger(); private TestProvider(Builder builder) { super(builder); } @Override - protected Single> getGrantsFromServer(Subject subject) { + protected List getGrantsFromServer(Subject subject) { String id = subject.principal().id(); - return Single.just(List.of(Role.create("counted_"+ COUNTER.incrementAndGet()), - Role.create("fixed"), - Role.create(id))); + return List.of(Role.create("counted_"+ COUNTER.incrementAndGet()), + Role.create("fixed"), + Role.create(id)); } @Override - protected Single> addAdditionalGrants(Subject subject, List idcsGrants) { - return Single.just(List.of(Role.create("additional_"+ COUNTER.incrementAndGet()), - Role.create("additional-fixed"))); + protected List addAdditionalGrants(Subject subject, List idcsGrants) { + return List.of(Role.create("additional_"+ COUNTER.incrementAndGet()), + Role.create("additional-fixed")); } } } \ No newline at end of file diff --git a/security/providers/jwt/src/main/java/io/helidon/security/providers/jwt/JwtProvider.java b/security/providers/jwt/src/main/java/io/helidon/security/providers/jwt/JwtProvider.java index f1a28a3af11..9ccb3b83d68 100644 --- a/security/providers/jwt/src/main/java/io/helidon/security/providers/jwt/JwtProvider.java +++ b/security/providers/jwt/src/main/java/io/helidon/security/providers/jwt/JwtProvider.java @@ -53,7 +53,6 @@ import io.helidon.security.spi.AuthenticationProvider; import io.helidon.security.spi.OutboundSecurityProvider; import io.helidon.security.spi.SecurityProvider; -import io.helidon.security.spi.SynchronousProvider; import io.helidon.security.util.TokenHandler; /** @@ -64,7 +63,7 @@ * Verification and signatures of tokens is done through JWK standard - two separate * JWK files are expected (one for verification, one for signatures). */ -public final class JwtProvider extends SynchronousProvider implements AuthenticationProvider, OutboundSecurityProvider { +public final class JwtProvider implements AuthenticationProvider, OutboundSecurityProvider { private static final System.Logger LOGGER = System.getLogger(JwtProvider.class.getName()); /** @@ -144,7 +143,7 @@ public static JwtProvider create(Config config) { } @Override - protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { if (!authenticate) { return AuthenticationResponse.abstain(); } @@ -267,9 +266,9 @@ public boolean isOutboundSupported(ProviderRequest providerRequest, } @Override - protected OutboundSecurityResponse syncOutbound(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundEndpointConfig) { + public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv, + EndpointConfig outboundEndpointConfig) { Optional maybeUsername = outboundEndpointConfig.abacAttribute(EP_PROPERTY_OUTBOUND_USER); return maybeUsername diff --git a/security/providers/jwt/src/test/java/io/helidon/security/providers/jwt/JwtProviderTest.java b/security/providers/jwt/src/test/java/io/helidon/security/providers/jwt/JwtProviderTest.java index 4417808406a..818c4196318 100644 --- a/security/providers/jwt/src/test/java/io/helidon/security/providers/jwt/JwtProviderTest.java +++ b/security/providers/jwt/src/test/java/io/helidon/security/providers/jwt/JwtProviderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,7 +85,7 @@ public void testWrongToken() { when(atnRequest.env()).thenReturn(se); - AuthenticationResponse authenticationResponse = provider.syncAuthenticate(atnRequest); + AuthenticationResponse authenticationResponse = provider.authenticate(atnRequest); assertThat(authenticationResponse.service(), is(Optional.empty())); assertThat(authenticationResponse.user(), is(Optional.empty())); @@ -132,7 +132,7 @@ public void testEcBothWays() { assertThat(provider.isOutboundSupported(request, outboundEnv, outboundEp), is(true)); - OutboundSecurityResponse response = provider.syncOutbound(request, outboundEnv, outboundEp); + OutboundSecurityResponse response = provider.outboundSecurity(request, outboundEnv, outboundEp); String signedToken = response.requestHeaders().get("Authorization").get(0); signedToken = signedToken.substring("bearer ".length()); @@ -168,7 +168,7 @@ public void testEcBothWays() { when(atnRequest.env()).thenReturn(se); - AuthenticationResponse authenticationResponse = provider.syncAuthenticate(atnRequest); + AuthenticationResponse authenticationResponse = provider.authenticate(atnRequest); authenticationResponse.user() .map(Subject::principal) .ifPresentOrElse(atnPrincipal -> { @@ -224,7 +224,7 @@ public void testInvalidSignatureOk() { assertThat(provider.isOutboundSupported(request, outboundEnv, outboundEp), is(true)); - OutboundSecurityResponse response = provider.syncOutbound(request, outboundEnv, outboundEp); + OutboundSecurityResponse response = provider.outboundSecurity(request, outboundEnv, outboundEp); String signedToken = response.requestHeaders().get("Authorization").get(0); signedToken = signedToken.substring("bearer ".length()); @@ -242,7 +242,7 @@ public void testInvalidSignatureOk() { .build(); when(atnRequest.env()).thenReturn(se); - AuthenticationResponse authenticationResponse = provider.syncAuthenticate(atnRequest); + AuthenticationResponse authenticationResponse = provider.authenticate(atnRequest); authenticationResponse.user() .map(Subject::principal) .ifPresentOrElse(atnPrincipal -> { @@ -298,7 +298,7 @@ public void testInvalidSignatureFail() { assertThat(provider.isOutboundSupported(request, outboundEnv, outboundEp), is(true)); - OutboundSecurityResponse response = provider.syncOutbound(request, outboundEnv, outboundEp); + OutboundSecurityResponse response = provider.outboundSecurity(request, outboundEnv, outboundEp); String signedToken = response.requestHeaders().get("Authorization").get(0); signedToken = signedToken.substring("bearer ".length()); @@ -316,7 +316,7 @@ public void testInvalidSignatureFail() { .build(); when(atnRequest.env()).thenReturn(se); - AuthenticationResponse authenticationResponse = provider.syncAuthenticate(atnRequest); + AuthenticationResponse authenticationResponse = provider.authenticate(atnRequest); assertThat(authenticationResponse.status(), is(SecurityResponse.SecurityStatus.FAILURE)); } @@ -344,7 +344,7 @@ public void testOctBothWays() { assertThat(provider.isOutboundSupported(request, outboundEnv, outboundEp), is(true)); - OutboundSecurityResponse response = provider.syncOutbound(request, outboundEnv, outboundEp); + OutboundSecurityResponse response = provider.outboundSecurity(request, outboundEnv, outboundEp); String signedToken = response.requestHeaders().get("Authorization").get(0); signedToken = signedToken.substring("bearer ".length()); @@ -380,7 +380,7 @@ public void testOctBothWays() { .build(); when(atnRequest.env()).thenReturn(se); - AuthenticationResponse authenticationResponse = provider.syncAuthenticate(atnRequest); + AuthenticationResponse authenticationResponse = provider.authenticate(atnRequest); authenticationResponse.user() .map(Subject::principal) .ifPresentOrElse(atnPrincipal -> { @@ -436,7 +436,7 @@ public void testRsaBothWays() { assertThat(provider.isOutboundSupported(request, outboundEnv, outboundEp), is(true)); - OutboundSecurityResponse response = provider.syncOutbound(request, outboundEnv, outboundEp); + OutboundSecurityResponse response = provider.outboundSecurity(request, outboundEnv, outboundEp); String signedToken = response.requestHeaders().get("Authorization").get(0); signedToken = signedToken.substring("bearer ".length()); @@ -475,7 +475,7 @@ public void testRsaBothWays() { .build(); when(atnRequest.env()).thenReturn(se); - AuthenticationResponse authenticationResponse = provider.syncAuthenticate(atnRequest); + AuthenticationResponse authenticationResponse = provider.authenticate(atnRequest); authenticationResponse.user() .map(Subject::principal) .ifPresentOrElse(atnPrincipal -> { diff --git a/security/providers/oidc-common/pom.xml b/security/providers/oidc-common/pom.xml index 1290011d50a..6ad7b3c0fd7 100644 --- a/security/providers/oidc-common/pom.xml +++ b/security/providers/oidc-common/pom.xml @@ -51,24 +51,20 @@ helidon-cors - io.helidon.reactive.webclient - helidon-reactive-webclient + io.helidon.nima.webclient + helidon-nima-webclient - io.helidon.reactive.webclient - helidon-reactive-webclient-security + io.helidon.nima.webclient + helidon-nima-webclient-tracing - io.helidon.reactive.webclient - helidon-reactive-webclient-tracing + io.helidon.nima.http.media + helidon-nima-http-media-jsonp - io.helidon.reactive.webclient - helidon-reactive-webclient-jaxrs - - - io.helidon.reactive.media - helidon-reactive-media-jsonp + io.helidon.jersey + helidon-jersey-client io.helidon.jersey diff --git a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/IdcsSupport.java b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/IdcsSupport.java index 4b6da9365f4..ae1c846c915 100644 --- a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/IdcsSupport.java +++ b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/IdcsSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,11 @@ import java.net.URI; import java.time.Duration; -import java.util.concurrent.TimeUnit; import io.helidon.common.http.Http; -import io.helidon.common.http.HttpMediaType; import io.helidon.common.parameters.Parameters; -import io.helidon.reactive.webclient.WebClient; -import io.helidon.reactive.webclient.WebClientResponse; +import io.helidon.nima.webclient.http1.Http1Client; +import io.helidon.nima.webclient.http1.Http1ClientResponse; import io.helidon.security.SecurityException; import io.helidon.security.jwt.jwk.JwkKeys; @@ -39,8 +37,8 @@ private IdcsSupport() { } // load signature jwk with a token, blocking operation - static JwkKeys signJwk(WebClient appWebClient, - WebClient generalClient, + static JwkKeys signJwk(Http1Client appWebClient, + Http1Client generalClient, URI tokenEndpointUri, URI signJwkUri, Duration clientTimeout) { @@ -50,35 +48,25 @@ static JwkKeys signJwk(WebClient appWebClient, .add("scope", "urn:opc:idm:__myscopes__") .build(); - try { - WebClientResponse response = appWebClient.post() - .uri(tokenEndpointUri) - .accept(HttpMediaType.APPLICATION_JSON) - .submit(form) - .await(clientTimeout.toMillis(), TimeUnit.MILLISECONDS); + try (Http1ClientResponse response = appWebClient.post() + .uri(tokenEndpointUri) + .header(Http.HeaderValues.ACCEPT_JSON) + .submit(form)) { if (response.status().family() == Http.Status.Family.SUCCESSFUL) { - JsonObject json = response.content() - .as(JsonObject.class) - .await(clientTimeout.toMillis(), TimeUnit.MILLISECONDS); + JsonObject json = response.as(JsonObject.class); String accessToken = json.getString("access_token"); // get the jwk from server JsonObject jwkJson = generalClient.get() .uri(signJwkUri) - .headers(it -> { - it.add(Http.Header.AUTHORIZATION, "Bearer " + accessToken); - return it; - }) - .request(JsonObject.class) - .await(clientTimeout.toMillis(), TimeUnit.MILLISECONDS); + .header(Http.Header.AUTHORIZATION, "Bearer " + accessToken) + .request(JsonObject.class); return JwkKeys.create(jwkJson); } else { - String errorEntity = response.content() - .as(String.class) - .await(clientTimeout.toMillis(), TimeUnit.MILLISECONDS); + String errorEntity = response.as(String.class); throw new SecurityException("Failed to read JWK from IDCS. Status: " + response.status() + ", entity: " + errorEntity); } diff --git a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java index 4d8bb38fa74..678234320a2 100644 --- a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java +++ b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcConfig.java @@ -22,34 +22,23 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; -import java.util.Optional; -import java.util.function.BiFunction; -import java.util.function.Function; import java.util.function.Supplier; import io.helidon.common.Errors; import io.helidon.common.LazyValue; import io.helidon.common.configurable.Resource; -import io.helidon.common.http.Http; import io.helidon.common.http.SetCookie; -import io.helidon.common.reactive.Single; import io.helidon.config.Config; import io.helidon.config.metadata.Configured; import io.helidon.config.metadata.ConfiguredOption; import io.helidon.cors.CrossOriginConfig; -import io.helidon.reactive.webclient.WebClient; -import io.helidon.reactive.webclient.WebClientRequestBuilder; +import io.helidon.nima.webclient.http1.Http1Client; import io.helidon.security.Security; import io.helidon.security.SecurityException; import io.helidon.security.jwt.jwk.JwkKeys; import io.helidon.security.providers.oidc.common.spi.TenantConfigFinder; import io.helidon.security.util.TokenHandler; -import jakarta.json.JsonObject; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.WebTarget; - /** * Configuration of OIDC usable from all resources that utilize OIDC specification, such as security provider, web server * extension and IDCS connectivity. @@ -361,11 +350,8 @@ public final class OidcConfig extends TenantConfigImpl { private final boolean forceHttpsRedirects; private final Duration tokenRefreshSkew; private final boolean relativeUris; - private final Client generalClient; - private final WebClient webClient; - private final LazyValue> introspectEndpoint; - private final Supplier webClientBuilderSupplier; - private final Supplier jaxrsClientBuilderSupplier; + private final Http1Client webClient; + private final Supplier webClientBuilderSupplier; private final LazyValue defaultTenant; private final boolean useParam; private final String paramName; @@ -392,7 +378,6 @@ private OidcConfig(Builder builder) { this.tokenRefreshSkew = builder.tokenRefreshSkew; this.tenantConfigurations = Map.copyOf(builder.tenantConfigurations); this.webClient = builder.webClient; - this.generalClient = builder.generalClient; this.relativeUris = builder.relativeUris; this.useParam = builder.useParam; @@ -405,14 +390,7 @@ private OidcConfig(Builder builder) { this.idTokenCookieHandler = builder.idTokenCookieBuilder.build(); this.tenantCookieHandler = builder.tenantCookieBuilder.build(); - if (builder.validateJwtWithJwk()) { - this.introspectEndpoint = LazyValue.create(Optional.empty()); - } else { - this.introspectEndpoint = LazyValue.create(() -> Optional.of(appClient().target(builder.introspectUri()))); - } - this.webClientBuilderSupplier = builder.webClientBuilderSupplier; - this.jaxrsClientBuilderSupplier = builder.jaxrsClientBuilderSupplier; this.defaultTenant = LazyValue.create(() -> Tenant.create(this, this)); LOGGER.log(Level.TRACE, () -> "Redirect URI with host: " + frontendUri + redirectUri); @@ -440,50 +418,6 @@ public static OidcConfig create(Config config) { .build(); } - /** - * Processing of {@link io.helidon.reactive.webclient.WebClient} submit using a POST method. - * This is a helper method to handle possible cases (success, failure with readable entity, failure). - * - * @param requestBuilder WebClient request builder - * @param toSubmit object to submit (such as {@link io.helidon.common.parameters.Parameters} - * @param jsonProcessor processor of successful JSON response - * @param errorEntityProcessor processor of an error that has an entity, to fail the single - * @param errorProcessor processor of an error that does not have an entity - * @param type of the result the call - * @return a future that completes successfully if processed from json, or if an error processor returns a non-empty value, - * completes with error otherwise - */ - public static Single postJsonResponse(WebClientRequestBuilder requestBuilder, - Object toSubmit, - Function jsonProcessor, - BiFunction> errorEntityProcessor, - BiFunction> errorProcessor) { - return requestBuilder.submit(toSubmit) - .flatMapSingle(response -> { - if (response.status().family() == Http.Status.Family.SUCCESSFUL) { - return response.content() - .as(JsonObject.class) - .map(jsonProcessor) - .onErrorResumeWithSingle(t -> errorProcessor.apply(t, "Failed to read JSON from response") - .map(Single::just) - .orElseGet(() -> Single.error(t))); - } else { - return response.content() - .as(String.class) - .flatMapSingle(it -> errorEntityProcessor.apply(response.status(), it) - .map(Single::just) - .orElseGet(() -> Single.error(new SecurityException("Failed to process request: " + it)))) - .onErrorResumeWithSingle(t -> errorProcessor.apply(t, "Failed to process error entity") - .map(Single::just) - .orElseGet(() -> Single.error(t))); - } - }) - .onErrorResumeWithSingle(t -> errorProcessor.apply(t, "Failed to invoke request") - .map(Single::just) - .orElseGet(() -> Single.error(t))); - - } - /** * Whether to use query parameter to get the information from request. * @@ -571,7 +505,6 @@ public OidcCookieHandler tenantCookieHandler() { return tenantCookieHandler; } - /** * Redirection URI. * @@ -635,7 +568,7 @@ public String redirectUriWithHost() { /** * Redirect URI with host information taken from request, - * unless an explicit frontend uri is defined in configuration. + * unless an explicit frontend uri is defined in configuration. * * @param frontendUri the frontend uri * @return redirect URI @@ -724,7 +657,7 @@ public String cookieOptions() { * @return prefix of cookie value * @see OidcConfig.Builder#cookieName(String) * @deprecated use {@link io.helidon.security.providers.oidc.common.OidcCookieHandler} instead, this method - * will no longer be avilable + * will no longer be avilable */ @Deprecated(forRemoval = true, since = "2.4.0") public String cookieValuePrefix() { @@ -741,72 +674,24 @@ public boolean relativeUris() { return relativeUris; } - /** - * Client with configured proxy with no security. - * - * @return client for general use. - * @deprecated Use {@link #generalWebClient()} instead - */ - @Deprecated(forRemoval = true, since = "2.4.0") - public Client generalClient() { - return generalClient; - } - /** * Client with configured proxy with no security. * * @return client for general use. */ - public WebClient generalWebClient() { + public Http1Client generalWebClient() { return webClient; } - /** - * Client with configured proxy and security of this OIDC client. - * - * @return client for communication with OIDC server - * @deprecated Use {@link #appWebClient()} - */ - @Deprecated(forRemoval = true, since = "2.4.0") - public Client appClient() { - return defaultTenant.get().appClient(); - } - /** * Client with configured proxy and security. * * @return client for communicating with OIDC identity server */ - public WebClient appWebClient() { + public Http1Client appWebClient() { return defaultTenant.get().appWebClient(); } - /** - * Token endpoint of the OIDC server. - * - * @return target the endpoint is on - * @see OidcConfig.Builder#tokenEndpointUri(URI) - * @deprecated Please use {@link #appWebClient()} and {@link #tokenEndpointUri()} instead; result of moving to - * reactive webclient from JAX-RS client - */ - @Deprecated(forRemoval = true, since = "2.4.0") - public WebTarget tokenEndpoint() { - return defaultTenant.get().tokenEndpoint(); - } - - /** - * Token introspection endpoint. - * - * @return introspection endpoint - * @see OidcConfig.Builder#introspectEndpointUri(URI) - *@deprecated Please use {@link #appWebClient()} and {@link #introspectUri()} instead; result of moving to - * reactive webclient from JAX-RS client - */ - @Deprecated(forRemoval = true, since = "2.4.0") - public WebTarget introspectEndpoint() { - return introspectEndpoint.get().orElse(null); - } - /** * Return {@link TenantConfig} bound to the provided tenant id. * If no {@link TenantConfig} found, default OIDC configuration should be returned. @@ -879,14 +764,10 @@ public URI introspectUri() { return defaultTenant.get().introspectUri(); } - Supplier webClientBuilderSupplier() { + Supplier webClientBuilderSupplier() { return webClientBuilderSupplier; } - Supplier jaxrsClientBuilderSupplier() { - return jaxrsClientBuilderSupplier; - } - /** * Client Authentication methods that are used by Clients to authenticate to the Authorization * Server when using the Token Endpoint. @@ -919,8 +800,6 @@ public enum ClientAuthentication { *

    * Optional: * {@code iat} - * - * */ CLIENT_SECRET_JWT, /** @@ -984,11 +863,8 @@ public static class Builder extends BaseBuilder { private String proxyHost; private String proxyProtocol = DEFAULT_PROXY_PROTOCOL; private int proxyPort = DEFAULT_PROXY_PORT; - @Deprecated - private Client generalClient; - private WebClient webClient; - private Supplier webClientBuilderSupplier; - private Supplier jaxrsClientBuilderSupplier; + private Http1Client webClient; + private Supplier webClientBuilderSupplier; private String paramName = DEFAULT_PARAM_NAME; private String tenantParamName = DEFAULT_TENANT_PARAM_NAME; private boolean useHeader = DEFAULT_HEADER_USE; @@ -1034,8 +910,8 @@ public OidcConfig build() { String frontendHost = URI.create(frontendUri).getHost(); if (identityHost.equals(frontendHost)) { LOGGER.log(Level.INFO, "As frontend host and identity host are equal, setting Same-Site policy" - + " to Strict this can be overridden using configuration option of OIDC: " - + "\"cookie-same-site\""); + + " to Strict this can be overridden using configuration option of OIDC: " + + "\"cookie-same-site\""); this.tenantCookieBuilder.sameSite(SetCookie.SameSite.STRICT); this.tokenCookieBuilder.sameSite(SetCookie.SameSite.STRICT); this.idTokenCookieBuilder.sameSite(SetCookie.SameSite.STRICT); @@ -1048,13 +924,11 @@ public OidcConfig build() { idTokenCookieBuilder.encryptionEnabled(true); } - this.webClientBuilderSupplier = () -> OidcUtil.webClientBaseBuilder(proxyHost, + this.webClientBuilderSupplier = () -> OidcUtil.webClientBaseBuilder(proxyProtocol, + proxyHost, proxyPort, relativeUris, clientTimeout()); - this.jaxrsClientBuilderSupplier = () -> OidcUtil.clientBaseBuilder(proxyProtocol, proxyHost, proxyPort); - - this.generalClient = jaxrsClientBuilderSupplier.get().build(); this.webClient = webClientBuilderSupplier.get().build(); return new OidcConfig(this); @@ -1217,7 +1091,7 @@ public Builder forceHttpsRedirects(boolean forceHttpsRedirects) { * if the host is unable to accept absolute URIs. * Defaults to {@value #DEFAULT_RELATIVE_URIS}. * - * @param relativeUris relative URIs flag + * @param relativeUris relative URIs flag * @return updated builder instance */ @ConfiguredOption("false") @@ -1280,6 +1154,7 @@ public Builder postLogoutUri(URI uri) { * Configure the parameter used to store the number of attempts in redirect. *

    * Defaults to {@value #DEFAULT_ATTEMPT_PARAM} + * * @param paramName name of the parameter used in the state parameter * @return updated builder instance */ @@ -1294,6 +1169,7 @@ public Builder redirectAttemptParam(String paramName) { * attempt. *

    * Defaults to {@value #DEFAULT_MAX_REDIRECTS} + * * @param maxRedirects maximal number of redirects from Helidon to OIDC provider * @return updated builder instance */ @@ -1348,8 +1224,6 @@ public Builder proxyPort(int proxyPort) { return this; } - - /** * A {@link TokenHandler} to * process header containing a JWT. @@ -1451,7 +1325,7 @@ public Builder cookieEncryptionPassword(char[] cookieEncryptionPassword) { * Defaults to {@code false}. * * @param cookieEncryptionEnabled whether cookie should be encrypted {@code true}, or as obtained from - * OIDC server {@code false} + * OIDC server {@code false} * @return updated builder instance */ public Builder cookieEncryptionEnabled(boolean cookieEncryptionEnabled) { @@ -1464,7 +1338,7 @@ public Builder cookieEncryptionEnabled(boolean cookieEncryptionEnabled) { * Defaults to {@code true}. * * @param cookieEncryptionEnabled whether cookie should be encrypted {@code true}, or as obtained from - * OIDC server {@code false} + * OIDC server {@code false} * @return updated builder instance */ public Builder cookieEncryptionEnabledIdToken(boolean cookieEncryptionEnabled) { @@ -1477,7 +1351,7 @@ public Builder cookieEncryptionEnabledIdToken(boolean cookieEncryptionEnabled) { * Defaults to {@code true}. * * @param cookieEncryptionEnabled whether cookie should be encrypted {@code true}, or as obtained from - * OIDC server {@code false} + * OIDC server {@code false} * @return updated builder instance */ public Builder cookieEncryptionEnabledTenantName(boolean cookieEncryptionEnabled) { @@ -1653,9 +1527,5 @@ public Builder addTenantConfig(TenantConfig tenantConfig) { tenantConfigurations.put(tenantConfig.name(), tenantConfig); return this; } - - private void clientTimeoutMillis(long millis) { - this.clientTimeout(Duration.ofMillis(millis)); - } } } diff --git a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcCookieHandler.java b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcCookieHandler.java index 87125cfc4f6..d54b212bd16 100644 --- a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcCookieHandler.java +++ b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcCookieHandler.java @@ -29,7 +29,6 @@ import java.util.function.Function; import io.helidon.common.http.SetCookie; -import io.helidon.common.reactive.Single; /** * Handler of cookies used in OIDC. @@ -42,8 +41,8 @@ public class OidcCookieHandler { private final List> createCookieUpdaters = new LinkedList<>(); private final String cookieName; private final String valuePrefix; - private final Function> encryptFunction; - private final Function> decryptFunction; + private final Function encryptFunction; + private final Function decryptFunction; private OidcCookieHandler(Builder builder) { this.cookieName = builder.cookieName; @@ -92,10 +91,10 @@ private OidcCookieHandler(Builder builder) { builder.encryptionName, builder.encryptionPassword); this.encryptFunction = it -> cookieEncryption.encrypt(it.getBytes(StandardCharsets.UTF_8)); - this.decryptFunction = it -> cookieEncryption.decrypt(it).map(String::new); + this.decryptFunction = it -> new String(cookieEncryption.decrypt(it), StandardCharsets.UTF_8); } else { - this.encryptFunction = Single::just; - this.decryptFunction = Single::just; + this.encryptFunction = Function.identity(); + this.decryptFunction = Function.identity(); } if (LOGGER.isLoggable(Level.TRACE)) { @@ -115,9 +114,8 @@ static Builder builder() { * @param value value of the cookie * @return a new builder to configure set cookie configured from OIDC Config */ - public Single createCookie(String value) { - return encryptFunction.apply(value) - .map(this::createCookieDirectValue); + public SetCookie.Builder createCookie(String value) { + return createCookieDirectValue(encryptFunction.apply(value)); } /** @@ -148,7 +146,7 @@ public SetCookie.Builder removeCookie() { * @param headers headers to process * @return cookie value, or empty if the cookie could not be found */ - public Optional> findCookie(Map> headers) { + public Optional findCookie(Map> headers) { Objects.requireNonNull(headers); List cookies = headers.get("Cookie"); @@ -176,7 +174,7 @@ public Optional> findCookie(Map> headers) { * @param cipherText cipher text to decrypt * @return secret */ - public Single decrypt(String cipherText) { + public String decrypt(String cipherText) { return decryptFunction.apply(cipherText); } diff --git a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcEncryption.java b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcEncryption.java index 7996d5aff5b..1be9e27578c 100644 --- a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcEncryption.java +++ b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcEncryption.java @@ -30,7 +30,6 @@ import io.helidon.common.Base64Value; import io.helidon.common.context.Contexts; import io.helidon.common.crypto.SymmetricCipher; -import io.helidon.common.reactive.Single; import io.helidon.security.Security; import io.helidon.security.spi.EncryptionProvider.EncryptionSupport; @@ -64,8 +63,8 @@ static EncryptionSupport create(String type, private static EncryptionSupport symmetricCipher(char[] masterPassword) { SymmetricCipher cipher = SymmetricCipher.create(masterPassword); return EncryptionSupport.create( - bytes -> Single.just(cipher.encrypt(Base64Value.create(bytes)).toBase64()), - cipherText -> Single.just(cipher.decrypt(Base64Value.createFromEncoded(cipherText)).toBytes()) + bytes -> cipher.encrypt(Base64Value.create(bytes)).toBase64(), + cipherText -> cipher.decrypt(Base64Value.createFromEncoded(cipherText)).toBytes() ); } diff --git a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcMetadata.java b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcMetadata.java index 1cb4e7866ba..8adc925cdc2 100644 --- a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcMetadata.java +++ b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcMetadata.java @@ -18,11 +18,10 @@ import java.lang.System.Logger.Level; import java.net.URI; -import java.time.Duration; import java.util.Optional; import io.helidon.common.Errors; -import io.helidon.reactive.webclient.WebClient; +import io.helidon.nima.webclient.http1.Http1Client; import jakarta.json.JsonObject; @@ -95,7 +94,7 @@ public Optional getString(String key) { static class Builder implements io.helidon.common.Builder { private boolean enableRemoteLoad; private JsonObject metadata; - private WebClient webClient; + private Http1Client webClient; private Errors.Collector collector = Errors.collector(); private URI identityUri; @@ -120,7 +119,7 @@ Builder json(JsonObject jsonObject) { return this; } - Builder webClient(WebClient webClient) { + Builder webClient(Http1Client webClient) { this.webClient = webClient; return this; } @@ -141,8 +140,7 @@ private void load() { try { this.metadata = webClient.get() .uri(wellKnown) - .request(JsonObject.class) - .await(Duration.ofSeconds(20)); + .request(JsonObject.class); LOGGER.log(Level.TRACE, () -> "OIDC Metadata loaded from well known URI: " + wellKnown); } catch (Exception e) { diff --git a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcUtil.java b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcUtil.java index 3dc602c179a..cc475eeb99e 100644 --- a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcUtil.java +++ b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/OidcUtil.java @@ -18,17 +18,15 @@ import java.lang.System.Logger.Level; import java.time.Duration; -import java.util.concurrent.TimeUnit; import io.helidon.common.Errors; -import io.helidon.reactive.media.jsonp.JsonpSupport; -import io.helidon.reactive.webclient.Proxy; -import io.helidon.reactive.webclient.WebClient; -import io.helidon.reactive.webclient.tracing.WebClientTracing; -import io.helidon.security.providers.common.OutboundConfig; - -import jakarta.ws.rs.client.ClientBuilder; -import org.glassfish.jersey.client.ClientProperties; +import io.helidon.common.config.Config; +import io.helidon.common.socket.SocketOptions; +import io.helidon.nima.http.media.MediaContext; +import io.helidon.nima.http.media.jsonp.JsonpSupport; +import io.helidon.nima.webclient.WebClient; +import io.helidon.nima.webclient.http1.Http1Client; +import io.helidon.nima.webclient.tracing.WebClientTracing; final class OidcUtil { private static final System.Logger LOGGER = System.getLogger(OidcUtil.class.getName()); @@ -50,40 +48,33 @@ static String fixServerType(String serverType) { return serverType; } - static ClientBuilder clientBaseBuilder(String proxyProtocol, String proxyHost, int proxyPort) { - ClientBuilder clientBuilder = ClientBuilder.newBuilder(); - - clientBuilder.property(OutboundConfig.PROPERTY_DISABLE_OUTBOUND, Boolean.TRUE); - - if (proxyHost != null) { - clientBuilder.property(ClientProperties.PROXY_URI, proxyProtocol - + "://" - + proxyHost - + ":" - + proxyPort); - } - - return clientBuilder; - } - - static WebClient.Builder webClientBaseBuilder(String proxyHost, - int proxyPort, - boolean relativeUris, - Duration clientTimeout) { - WebClient.Builder webClientBuilder = WebClient.builder() + static Http1Client.Http1ClientBuilder webClientBaseBuilder(String proxyProtocol, + String proxyHost, + int proxyPort, + boolean relativeUris, + Duration clientTimeout) { + Http1Client.Http1ClientBuilder webClientBuilder = WebClient.builder() .addService(WebClientTracing.create()) - .addMediaSupport(JsonpSupport.create()) - .connectTimeout(clientTimeout.toMillis(), TimeUnit.MILLISECONDS) - .readTimeout(clientTimeout.toMillis(), TimeUnit.MILLISECONDS) - .relativeUris(relativeUris); + .mediaContext(MediaContext.builder() + .discoverServices(false) + .addMediaSupport(JsonpSupport.create(Config.empty())) + .build()) + .channelOptions(SocketOptions.builder() + .connectTimeout(clientTimeout) + .readTimeout(clientTimeout) + .build()); - if (proxyHost != null) { - webClientBuilder.proxy(Proxy.builder() - .type(Proxy.ProxyType.HTTP) - .host(proxyHost) - .port(proxyPort) - .build()); - } + //TODO Níma client proxy +// if (proxyHost != null) { +// Proxy.ProxyType proxyType = Proxy.ProxyType.valueOf(proxyProtocol.toUpperCase()); +// webClientBuilder.proxy(Proxy.builder() +// .type(proxyType) +// .host(proxyHost) +// .port(proxyPort) +// .build()); + //relative uris should be set when proxy is used +// .relativeUris(relativeUris); +// } return webClientBuilder; } diff --git a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/Tenant.java b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/Tenant.java index 0cf5a3a048e..b07b4a6928b 100644 --- a/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/Tenant.java +++ b/security/providers/oidc-common/src/main/java/io/helidon/security/providers/oidc/common/Tenant.java @@ -17,10 +17,12 @@ package io.helidon.security.providers.oidc.common; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import io.helidon.common.Errors; -import io.helidon.reactive.webclient.WebClient; -import io.helidon.reactive.webclient.security.WebClientSecurity; +import io.helidon.common.http.Http; +import io.helidon.nima.webclient.http1.Http1Client; import io.helidon.security.Security; import io.helidon.security.SecurityException; import io.helidon.security.jwt.jwk.JwkKeys; @@ -29,10 +31,6 @@ import io.helidon.security.providers.httpauth.HttpBasicOutboundConfig; import jakarta.json.JsonObject; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.WebTarget; -import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; /** * Holder of the tenant configuration resolved at runtime. Used for OIDC lazy loading. @@ -44,9 +42,7 @@ public final class Tenant { private final String authorizationEndpointUri; private final URI logoutEndpointUri; private final String issuer; - private final Client appClient; - private final WebClient appWebClient; - private final WebTarget tokenEndpoint; + private final Http1Client appWebClient; private final JwkKeys signJwk; private final URI introspectUri; @@ -55,9 +51,7 @@ private Tenant(TenantConfig tenantConfig, URI authorizationEndpointUri, URI logoutEndpointUri, String issuer, - Client appClient, - WebClient appWebClient, - WebTarget tokenEndpoint, + Http1Client appWebClient, JwkKeys signJwk, URI introspectUri) { this.tenantConfig = tenantConfig; @@ -65,9 +59,7 @@ private Tenant(TenantConfig tenantConfig, this.authorizationEndpointUri = authorizationEndpointUri.toString(); this.logoutEndpointUri = logoutEndpointUri; this.issuer = issuer; - this.appClient = appClient; this.appWebClient = appWebClient; - this.tokenEndpoint = tokenEndpoint; this.signJwk = signJwk; this.introspectUri = introspectUri; } @@ -80,7 +72,7 @@ private Tenant(TenantConfig tenantConfig, * @return new instance with resolved OIDC metadata */ public static Tenant create(OidcConfig oidcConfig, TenantConfig tenantConfig) { - WebClient webClient = oidcConfig.generalWebClient(); + Http1Client webClient = oidcConfig.generalWebClient(); Errors.Collector collector = Errors.collector(); @@ -112,14 +104,9 @@ public static Tenant create(OidcConfig oidcConfig, TenantConfig tenantConfig) { .orElse(null); collector.collect().checkValid(); - WebClient.Builder webClientBuilder = oidcConfig.webClientBuilderSupplier().get(); - ClientBuilder clientBuilder = oidcConfig.jaxrsClientBuilderSupplier().get(); + Http1Client.Http1ClientBuilder webClientBuilder = oidcConfig.webClientBuilderSupplier().get(); if (tenantConfig.tokenEndpointAuthentication() == OidcConfig.ClientAuthentication.CLIENT_SECRET_BASIC) { - HttpAuthenticationFeature basicAuth = HttpAuthenticationFeature.basicBuilder() - .credentials(tenantConfig.clientId(), tenantConfig.clientSecret()) - .build(); - clientBuilder.register(basicAuth); HttpBasicAuthProvider httpBasicAuth = HttpBasicAuthProvider.builder() .addOutboundTarget(OutboundTarget.builder("oidc") @@ -133,12 +120,15 @@ public static Tenant create(OidcConfig oidcConfig, TenantConfig tenantConfig) { .addOutboundSecurityProvider(httpBasicAuth) .build(); - webClientBuilder.addService(WebClientSecurity.create(tokenOutboundSecurity)); + //TODO Níma client security? +// webClientBuilder.addService(WebClientSecurity.create(tokenOutboundSecurity)); + //This is workaround for missing Níma client security. This adds Authorization header to be used in every request. + byte[] byteArray = (tenantConfig.clientId() + ":" + tenantConfig.clientSecret()).getBytes(StandardCharsets.UTF_8); + String base64 = Base64.getEncoder().encodeToString(byteArray); + webClientBuilder.header(Http.Header.create(Http.Header.AUTHORIZATION, "Basic " + base64)); } - Client appClient = clientBuilder.build(); - WebClient appWebClient = webClientBuilder.build(); - WebTarget tokenEndpoint = appClient.target(tokenEndpointUri); + Http1Client appWebClient = webClientBuilder.build(); JwkKeys signJwk = tenantConfig.tenantSignJwk().orElseGet(() -> { if (tenantConfig.validateJwtWithJwk()) { @@ -158,8 +148,7 @@ public static Tenant create(OidcConfig oidcConfig, TenantConfig tenantConfig) { return JwkKeys.builder() .json(webClient.get() .uri(jwkUri) - .request(JsonObject.class) - .await()) + .request(JsonObject.class)) .build(); } } @@ -178,9 +167,7 @@ public static Tenant create(OidcConfig oidcConfig, TenantConfig tenantConfig) { authorizationEndpointUri, logoutEndpointUri, issuer, - appClient, appWebClient, - tokenEndpoint, signJwk, introspectUri); } @@ -235,7 +222,7 @@ public String issuer() { * * @return client for communicating with OIDC identity server */ - public WebClient appWebClient() { + public Http1Client appWebClient() { return appWebClient; } @@ -260,22 +247,4 @@ public URI introspectUri() { return introspectUri; } - /** - * Token endpoint of the OIDC server. - * - * @return target the endpoint is on - */ - WebTarget tokenEndpoint() { - return tokenEndpoint; - } - - /** - * Client with configured proxy and security of this OIDC client. - * - * @return client for communication with OIDC server - */ - Client appClient() { - return appClient; - } - } diff --git a/security/providers/oidc-common/src/main/java/module-info.java b/security/providers/oidc-common/src/main/java/module-info.java index a3a5a08760b..be109f85add 100644 --- a/security/providers/oidc-common/src/main/java/module-info.java +++ b/security/providers/oidc-common/src/main/java/module-info.java @@ -19,28 +19,22 @@ */ module io.helidon.security.providers.oidc.common { + requires transitive io.helidon.nima.webclient; // EncryptionProvider.EncryptionSupport is part of API requires transitive io.helidon.security; // TokenHandler is part of API requires transitive io.helidon.security.util; - // WebClient is part of API - requires transitive io.helidon.reactive.webclient; requires io.helidon.common.parameters; requires io.helidon.security.providers.common; requires io.helidon.security.jwt; requires io.helidon.security.providers.httpauth; - requires io.helidon.reactive.webclient.jaxrs; - requires io.helidon.reactive.webclient.security; - requires io.helidon.reactive.webclient.tracing; - requires io.helidon.reactive.media.jsonp; + requires io.helidon.common.context; requires io.helidon.common.crypto; requires static io.helidon.config.metadata; requires io.helidon.cors; - - // these are deprecated and will be removed in 3.x - requires jersey.client; - requires jakarta.ws.rs; + requires io.helidon.nima.http.media.jsonp; + requires io.helidon.nima.webclient.tracing; exports io.helidon.security.providers.oidc.common; exports io.helidon.security.providers.oidc.common.spi; diff --git a/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcConfigAbstractTest.java b/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcConfigAbstractTest.java index e1ae3c09108..2a113168815 100644 --- a/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcConfigAbstractTest.java +++ b/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcConfigAbstractTest.java @@ -41,13 +41,13 @@ void testExplicitValues() { () -> assertThat("Client ID", config.clientId(), is("client-id-value")), () -> assertThat("Validate JWT with JWK", config.validateJwtWithJwk(), is(false)), () -> assertThat("Token endpoint", - config.tokenEndpoint().getUri(), + config.tokenEndpointUri(), is(URI.create("http://identity.oracle.com/tokens"))), () -> assertThat("Authorization endpoint", config.authorizationEndpointUri(), is("http://identity.oracle.com/authorization")), () -> assertThat("Introspect endpoint", - config.introspectEndpoint().getUri(), + config.introspectUri(), is(URI.create("http://identity.oracle.com/introspect"))), () -> assertThat("Validate relativeUris flag", config.relativeUris(), @@ -71,8 +71,8 @@ void testDefaultValues() { () -> assertThat("Audience", config.audience(), is("https://identity.oracle.com")), () -> assertThat("Parameter name", config.paramName(), is("accessToken")), () -> assertThat("Issuer", config.issuer(), nullValue()), - () -> assertThat("Client without authentication", config.generalClient(), notNullValue()), - () -> assertThat("Client with authentication", config.appClient(), notNullValue()), + () -> assertThat("Client without authentication", config.generalWebClient(), notNullValue()), + () -> assertThat("Client with authentication", config.appWebClient(), notNullValue()), () -> assertThat("JWK Keys", config.signJwk(), notNullValue()) ); } diff --git a/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcConfigFromBuilderTest.java b/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcConfigFromBuilderTest.java index 73cf746d302..a667f8d299c 100644 --- a/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcConfigFromBuilderTest.java +++ b/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcConfigFromBuilderTest.java @@ -100,8 +100,8 @@ void testDefaultValues() { () -> assertThat("Audience", config.audience(), is("https://identity.oracle.com")), () -> assertThat("Parameter name", config.paramName(), is("accessToken")), () -> assertThat("Issuer", config.issuer(), nullValue()), - () -> assertThat("Client without authentication", config.generalClient(), notNullValue()), - () -> assertThat("Client with authentication", config.appClient(), notNullValue()), + () -> assertThat("Client without authentication", config.generalWebClient(), notNullValue()), + () -> assertThat("Client with authentication", config.appWebClient(), notNullValue()), () -> assertThat("JWK Keys", config.signJwk(), notNullValue()) ); } diff --git a/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcCookieHandlerTest.java b/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcCookieHandlerTest.java index 81adf192780..5807635740d 100644 --- a/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcCookieHandlerTest.java +++ b/security/providers/oidc-common/src/test/java/io/helidon/security/providers/oidc/common/OidcCookieHandlerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,7 +43,7 @@ static void initClass() { @Test void testFindCookieMissing() { Map> headers = Map.of(); - Optional> cookie = handler.findCookie(headers); + Optional cookie = handler.findCookie(headers); assertThat(cookie, is(Optional.empty())); } @@ -53,10 +53,10 @@ void testFindCookiePresent() { String expectedValue = "cookieValue"; Map> headers = Map.of("Accept", List.of("application/json"), "Cookie", List.of("COOKIE=" + expectedValue)); - Optional> cookie = handler.findCookie(headers); + Optional cookie = handler.findCookie(headers); assertThat(cookie, not(Optional.empty())); - String cookieValue = cookie.get().await(); + String cookieValue = cookie.get(); assertThat(cookieValue, is(expectedValue)); headers = Map.of("Accept", List.of("application/json"), @@ -64,7 +64,7 @@ void testFindCookiePresent() { cookie = handler.findCookie(headers); assertThat(cookie, not(Optional.empty())); - cookieValue = cookie.get().await(); + cookieValue = cookie.get(); assertThat(cookieValue, is(expectedValue)); headers = Map.of("Accept", List.of("application/json"), @@ -72,7 +72,7 @@ void testFindCookiePresent() { cookie = handler.findCookie(headers); assertThat(cookie, not(Optional.empty())); - cookieValue = cookie.get().await(); + cookieValue = cookie.get(); assertThat(cookieValue, is(expectedValue)); headers = Map.of("Accept", List.of("application/json"), @@ -80,7 +80,7 @@ void testFindCookiePresent() { cookie = handler.findCookie(headers); assertThat(cookie, not(Optional.empty())); - cookieValue = cookie.get().await(); + cookieValue = cookie.get(); assertThat(cookieValue, is(expectedValue)); } } diff --git a/security/providers/oidc-reactive/src/main/java/io/helidon/security/providers/oidc/reactive/OidcSupport.java b/security/providers/oidc-reactive/src/main/java/io/helidon/security/providers/oidc/reactive/OidcSupport.java index fff41ced92b..90fa44d41d8 100644 --- a/security/providers/oidc-reactive/src/main/java/io/helidon/security/providers/oidc/reactive/OidcSupport.java +++ b/security/providers/oidc-reactive/src/main/java/io/helidon/security/providers/oidc/reactive/OidcSupport.java @@ -26,24 +26,20 @@ import java.util.Map; import java.util.Optional; import java.util.ServiceLoader; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import io.helidon.common.HelidonServiceLoader; import io.helidon.common.configurable.LruCache; -import io.helidon.common.configurable.ThreadPoolSupplier; import io.helidon.common.http.Http; -import io.helidon.common.http.HttpMediaType; +import io.helidon.common.http.SetCookie; import io.helidon.common.parameters.Parameters; -import io.helidon.common.reactive.Single; import io.helidon.config.Config; import io.helidon.cors.CrossOriginConfig; -import io.helidon.reactive.webclient.WebClient; -import io.helidon.reactive.webclient.WebClientRequestBuilder; +import io.helidon.nima.webclient.http1.Http1Client; +import io.helidon.nima.webclient.http1.Http1ClientRequest; +import io.helidon.nima.webclient.http1.Http1ClientResponse; import io.helidon.reactive.webserver.RequestHeaders; import io.helidon.reactive.webserver.ResponseHeaders; import io.helidon.reactive.webserver.Routing; @@ -52,6 +48,7 @@ import io.helidon.reactive.webserver.Service; import io.helidon.reactive.webserver.cors.CorsSupport; import io.helidon.security.Security; +import io.helidon.security.SecurityException; import io.helidon.security.integration.webserver.WebSecurity; import io.helidon.security.providers.oidc.OidcProviderService; import io.helidon.security.providers.oidc.common.OidcConfig; @@ -140,7 +137,6 @@ */ public final class OidcSupport implements Service { private static final System.Logger LOGGER = System.getLogger(OidcSupport.class.getName()); - private static final Supplier OIDC_SUPPORT_SERVICE = ThreadPoolSupplier.create("oidc-support"); private static final String CODE_PARAM_NAME = "code"; private static final String STATE_PARAM_NAME = "state"; private static final String DEFAULT_REDIRECT = "/index.html"; @@ -231,12 +227,13 @@ public void update(Routing.Rules rules) { } private void processLogout(ServerRequest req, ServerResponse res) { - findTenantName(req) - .forSingle(tenantName -> processTenantLogout(req, res, tenantName)); + String tenantName = findTenantName(req); + processTenantLogout(req, res, tenantName); } private void processTenantLogout(ServerRequest req, ServerResponse res, String tenantName) { - obtainCurrentTenant(tenantName).forSingle(tenant -> logoutWithTenant(req, res, tenant)); + Tenant tenant = obtainCurrentTenant(tenantName); + logoutWithTenant(req, res, tenant); } private void logoutWithTenant(ServerRequest req, ServerResponse res, Tenant tenant) { @@ -256,30 +253,31 @@ private void logoutWithTenant(ServerRequest req, ServerResponse res, Tenant tena } String encryptedIdToken = idTokenCookie.get(); + try { + String idToken = idTokenCookieHandler.decrypt(encryptedIdToken); - idTokenCookieHandler.decrypt(encryptedIdToken) - .forSingle(idToken -> { - StringBuilder sb = new StringBuilder(tenant.logoutEndpointUri() - + "?id_token_hint=" - + idToken - + "&post_logout_redirect_uri=" + postLogoutUri(req)); - - req.queryParams().first("state") - .ifPresent(it -> sb.append("&state=").append(it)); - - ResponseHeaders headers = res.headers(); - headers.addCookie(tokenCookieHandler.removeCookie().build()); - headers.addCookie(idTokenCookieHandler.removeCookie().build()); - headers.addCookie(tenantCookieHandler.removeCookie().build()); - - res.status(Http.Status.TEMPORARY_REDIRECT_307) - .addHeader(Http.Header.LOCATION, sb.toString()) - .send(); - }) - .exceptionallyAccept(t -> sendError(res, t)); + StringBuilder sb = new StringBuilder(tenant.logoutEndpointUri() + + "?id_token_hint=" + + idToken + + "&post_logout_redirect_uri=" + postLogoutUri(req)); + + req.queryParams().first("state") + .ifPresent(it -> sb.append("&state=").append(it)); + + ResponseHeaders headers = res.headers(); + headers.addCookie(tokenCookieHandler.removeCookie().build()); + headers.addCookie(idTokenCookieHandler.removeCookie().build()); + headers.addCookie(tenantCookieHandler.removeCookie().build()); + + res.status(Http.Status.TEMPORARY_REDIRECT_307) + .addHeader(Http.Header.LOCATION, sb.toString()) + .send(); + } catch (Exception e) { + sendError(res, e); + } } - private Single findTenantName(ServerRequest request) { + private String findTenantName(ServerRequest request) { List missingLocations = new LinkedList<>(); Optional tenantId = Optional.empty(); if (oidcConfig.useParam()) { @@ -290,7 +288,7 @@ private Single findTenantName(ServerRequest request) { } } if (oidcConfig.useCookie() && tenantId.isEmpty()) { - Optional> cookie = oidcConfig.tenantCookieHandler() + Optional cookie = oidcConfig.tenantCookieHandler() .findCookie(request.headers().toMap()); if (cookie.isPresent()) { @@ -299,33 +297,28 @@ private Single findTenantName(ServerRequest request) { missingLocations.add("cookie"); } if (tenantId.isPresent()) { - return Single.just(tenantId.get()); + return tenantId.get(); } else { if (LOGGER.isLoggable(Level.TRACE)) { LOGGER.log(Level.TRACE, "Missing tenant id, could not find in either of: " + missingLocations + "Falling back to the default tenant id: " + DEFAULT_TENANT_ID); } - return Single.just(DEFAULT_TENANT_ID); + return DEFAULT_TENANT_ID; } } - private Single obtainCurrentTenant(String tenantName) { + private Tenant obtainCurrentTenant(String tenantName) { Optional maybeTenant = tenants.get(tenantName); if (maybeTenant.isPresent()) { - return Single.just(maybeTenant.get()); + return maybeTenant.get(); } else { - CompletableFuture tenantCompletableFuture = CompletableFuture.supplyAsync( - () -> { - Tenant tenant = oidcConfigFinders.stream() - .map(finder -> finder.config(tenantName)) - .flatMap(Optional::stream) - .map(tenantConfig -> Tenant.create(oidcConfig, tenantConfig)) - .findFirst() - .orElseGet(() -> Tenant.create(oidcConfig, oidcConfig.tenantConfig(tenantName))); - return tenants.computeValue(tenantName, () -> Optional.of(tenant)).get(); - }, - OIDC_SUPPORT_SERVICE.get()); - return Single.create(tenantCompletableFuture); + Tenant tenant = oidcConfigFinders.stream() + .map(finder -> finder.config(tenantName)) + .flatMap(Optional::stream) + .map(tenantConfig -> Tenant.create(oidcConfig, tenantConfig)) + .findFirst() + .orElseGet(() -> Tenant.create(oidcConfig, oidcConfig.tenantConfig(tenantName))); + return tenants.computeValue(tenantName, () -> Optional.of(tenant)).get(); } } @@ -363,34 +356,54 @@ private void processOidcRedirect(ServerRequest req, ServerResponse res) { private void processCode(String code, ServerRequest req, ServerResponse res) { String tenantName = req.queryParams().first(oidcConfig.tenantParamName()).orElse(TenantConfigFinder.DEFAULT_TENANT_ID); - obtainCurrentTenant(tenantName).forSingle(tenant -> processCodeWithTenant(code, req, res, tenantName, tenant)); + Tenant tenant = obtainCurrentTenant(tenantName); + processCodeWithTenant(code, req, res, tenantName, tenant); } private void processCodeWithTenant(String code, ServerRequest req, ServerResponse res, String tenantName, Tenant tenant) { TenantConfig tenantConfig = tenant.tenantConfig(); - WebClient webClient = tenant.appWebClient(); + Http1Client webClient = tenant.appWebClient(); Parameters.Builder form = Parameters.builder("oidc-form-params") .add("grant_type", "authorization_code") .add("code", code) .add("redirect_uri", redirectUri(req, tenantName)); - WebClientRequestBuilder post = webClient.post() + Http1ClientRequest post = webClient.post() .uri(tenant.tokenEndpointUri()) - .accept(HttpMediaType.APPLICATION_JSON); + .header(Http.HeaderValues.ACCEPT_JSON); if (tenantConfig.tokenEndpointAuthentication() == OidcConfig.ClientAuthentication.CLIENT_SECRET_POST) { form.add("client_id", tenantConfig.clientId()); form.add("client_secret", tenantConfig.clientSecret()); } - OidcConfig.postJsonResponse(post, - form.build(), - json -> processJsonResponse(req, res, json, tenantName), - (status, errorEntity) -> processError(res, status, errorEntity), - (t, message) -> processError(res, t, message)) - .ignoreElement(); + try (Http1ClientResponse response = post.submit(form.build())) { + if (response.status().family() == Http.Status.Family.SUCCESSFUL) { + try { + JsonObject jsonObject = response.as(JsonObject.class); + processJsonResponse(req, res, jsonObject, tenantName); + } catch (Exception e) { + processError(res, e, "Failed to read JSON from response"); + } + } else { + String message; + try { + message = response.as(String.class); + } catch (Exception e) { + processError(res, e, "Failed to process error entity"); + return; + } + try { + processError(res, response.status(), message); + } catch (Exception e) { + throw new SecurityException("Failed to process request: " + message); + } + } + } catch (Exception e) { + processError(res, e, "Failed to invoke request"); + } } private Object postLogoutUri(ServerRequest req) { @@ -449,27 +462,29 @@ private String processJsonResponse(ServerRequest req, if (oidcConfig.useCookie()) { ResponseHeaders headers = res.headers(); - OidcCookieHandler tenantCookieHandler = oidcConfig.tenantCookieHandler(); - tenantCookieHandler.createCookie(tenantName) - .forSingle(builder -> headers.addCookie(builder.build())) - .exceptionallyAccept(t -> sendError(res, t)); - - OidcCookieHandler tokenCookieHandler = oidcConfig.tokenCookieHandler(); - tokenCookieHandler.createCookie(tokenValue) - .forSingle(builder -> { - headers.addCookie(builder.build()); - if (idToken != null && oidcConfig.logoutEnabled()) { - oidcConfig.idTokenCookieHandler().createCookie(idToken) - .forSingle(it -> { - headers.addCookie(it.build()); - res.send(); - }) - .exceptionallyAccept(t -> sendError(res, t)); - } else { - res.send(); - } - }) - .exceptionallyAccept(t -> sendError(res, t)); + try { + OidcCookieHandler tenantCookieHandler = oidcConfig.tenantCookieHandler(); + SetCookie tenantCookie = tenantCookieHandler.createCookie(tenantName).build(); + headers.addCookie(tenantCookie); + + tenantCookieHandler.createCookie(tenantName); + + OidcCookieHandler tokenCookieHandler = oidcConfig.tokenCookieHandler(); + SetCookie tokenCookie = tokenCookieHandler.createCookie(tokenValue).build(); + headers.addCookie(tokenCookie); + + if (idToken != null && oidcConfig.logoutEnabled()) { + OidcCookieHandler idTokenCookieHandler = oidcConfig.idTokenCookieHandler(); + SetCookie idTokenCookie = idTokenCookieHandler.createCookie(tenantName).build(); + headers.addCookie(idTokenCookie); + res.send(); + } else { + res.send(); + } + + } catch (Exception e) { + sendError(res, e); + } } else { res.send(); } diff --git a/security/providers/oidc-reactive/src/test/java/io/helidon/security/providers/oidc/reactive/OidcSupportTest.java b/security/providers/oidc-reactive/src/test/java/io/helidon/security/providers/oidc/reactive/OidcSupportTest.java index 881ecf3813d..a435bdb71c0 100644 --- a/security/providers/oidc-reactive/src/test/java/io/helidon/security/providers/oidc/reactive/OidcSupportTest.java +++ b/security/providers/oidc-reactive/src/test/java/io/helidon/security/providers/oidc/reactive/OidcSupportTest.java @@ -181,9 +181,7 @@ void testOutbound() { .build(); EndpointConfig endpointConfig = EndpointConfig.builder().build(); - OutboundSecurityResponse response = provider.outboundSecurity(providerRequest, outboundEnv, endpointConfig) - .toCompletableFuture() - .join(); + OutboundSecurityResponse response = provider.outboundSecurity(providerRequest, outboundEnv, endpointConfig); List authorization = response.requestHeaders().get("Authorization"); assertThat("Authorization header", authorization, hasItem("Bearer " + tokenContent)); @@ -215,9 +213,7 @@ void testOutboundFull() { boolean outboundSupported = provider.isOutboundSupported(providerRequest, outboundEnv, endpointConfig); assertThat("Outbound should not be supported by default", outboundSupported, is(false)); - OutboundSecurityResponse response = provider.outboundSecurity(providerRequest, outboundEnv, endpointConfig) - .toCompletableFuture() - .join(); + OutboundSecurityResponse response = provider.outboundSecurity(providerRequest, outboundEnv, endpointConfig); assertThat("Disabled target should have empty headers", response.requestHeaders().size(), is(0)); } diff --git a/security/providers/oidc/pom.xml b/security/providers/oidc/pom.xml index 7c203728f2a..ec48da7e78b 100644 --- a/security/providers/oidc/pom.xml +++ b/security/providers/oidc/pom.xml @@ -62,10 +62,6 @@ io.helidon.config helidon-config - - io.helidon.reactive.webclient - helidon-reactive-webclient - io.helidon.security.integration helidon-security-integration-nima @@ -101,16 +97,6 @@ helidon-bundles-config test - - io.helidon.jersey - helidon-jersey-client - provided - - - io.helidon.jersey - helidon-jersey-media-jsonp - provided - org.junit.jupiter junit-jupiter-api diff --git a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcFeature.java b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcFeature.java index 0fd409822d0..bf57a439e96 100644 --- a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcFeature.java +++ b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcFeature.java @@ -36,21 +36,21 @@ import io.helidon.common.context.Context; import io.helidon.common.context.Contexts; import io.helidon.common.http.Http; -import io.helidon.common.http.HttpMediaType; import io.helidon.common.http.ServerRequestHeaders; import io.helidon.common.http.ServerResponseHeaders; import io.helidon.common.parameters.Parameters; -import io.helidon.common.reactive.Single; import io.helidon.config.Config; import io.helidon.cors.CrossOriginConfig; +import io.helidon.nima.webclient.http1.Http1Client; +import io.helidon.nima.webclient.http1.Http1ClientRequest; +import io.helidon.nima.webclient.http1.Http1ClientResponse; import io.helidon.nima.webserver.cors.CorsSupport; import io.helidon.nima.webserver.http.HttpFeature; import io.helidon.nima.webserver.http.HttpRouting; import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; -import io.helidon.reactive.webclient.WebClient; -import io.helidon.reactive.webclient.WebClientRequestBuilder; import io.helidon.security.Security; +import io.helidon.security.SecurityException; import io.helidon.security.integration.nima.SecurityFeature; import io.helidon.security.providers.oidc.common.OidcConfig; import io.helidon.security.providers.oidc.common.OidcCookieHandler; @@ -235,11 +235,11 @@ public void setup(HttpRouting.Builder routing) { } private void processLogout(ServerRequest req, ServerResponse res) { - findTenantName(req) - .forSingle(tenantName -> processTenantLogout(req, res, tenantName)); + String tenantName = findTenantName(req); + processTenantLogout(req, res, tenantName); } - private Single findTenantName(ServerRequest request) { + private String findTenantName(ServerRequest request) { List missingLocations = new LinkedList<>(); Optional tenantId = Optional.empty(); if (oidcConfig.useParam()) { @@ -250,7 +250,7 @@ private Single findTenantName(ServerRequest request) { } } if (oidcConfig.useCookie() && tenantId.isEmpty()) { - Optional> cookie = oidcConfig.tenantCookieHandler() + Optional cookie = oidcConfig.tenantCookieHandler() .findCookie(request.headers().toMap()); if (cookie.isPresent()) { @@ -259,13 +259,13 @@ private Single findTenantName(ServerRequest request) { missingLocations.add("cookie"); } if (tenantId.isPresent()) { - return Single.just(tenantId.get()); + return tenantId.get(); } else { if (LOGGER.isLoggable(Level.TRACE)) { LOGGER.log(Level.TRACE, "Missing tenant id, could not find in either of: " + missingLocations + "Falling back to the default tenant id: " + DEFAULT_TENANT_ID); } - return Single.just(DEFAULT_TENANT_ID); + return DEFAULT_TENANT_ID; } } @@ -304,26 +304,27 @@ private void logoutWithTenant(ServerRequest req, ServerResponse res, Tenant tena String encryptedIdToken = idTokenCookie.get(); - idTokenCookieHandler.decrypt(encryptedIdToken) - .forSingle(idToken -> { - StringBuilder sb = new StringBuilder(tenant.logoutEndpointUri() - + "?id_token_hint=" - + idToken - + "&post_logout_redirect_uri=" + postLogoutUri(req)); - - req.query().first("state") - .ifPresent(it -> sb.append("&state=").append(it)); - - ServerResponseHeaders headers = res.headers(); - headers.addCookie(tokenCookieHandler.removeCookie().build()); - headers.addCookie(idTokenCookieHandler.removeCookie().build()); - headers.addCookie(tenantCookieHandler.removeCookie().build()); - - res.status(Http.Status.TEMPORARY_REDIRECT_307) - .header(Http.Header.LOCATION, sb.toString()) - .send(); - }) - .exceptionallyAccept(t -> sendError(res, t)); + try { + String idToken = idTokenCookieHandler.decrypt(encryptedIdToken); + StringBuilder sb = new StringBuilder(tenant.logoutEndpointUri() + + "?id_token_hint=" + + idToken + + "&post_logout_redirect_uri=" + postLogoutUri(req)); + + req.query().first("state") + .ifPresent(it -> sb.append("&state=").append(it)); + + ServerResponseHeaders headers = res.headers(); + headers.addCookie(tokenCookieHandler.removeCookie().build()); + headers.addCookie(idTokenCookieHandler.removeCookie().build()); + headers.addCookie(tenantCookieHandler.removeCookie().build()); + + res.status(Http.Status.TEMPORARY_REDIRECT_307) + .header(Http.Header.LOCATION, sb.toString()) + .send(); + } catch (Exception e) { + sendError(res, e); + } } private void addRequestAsHeader(ServerRequest req, ServerResponse res) { @@ -369,26 +370,44 @@ private void processCode(String code, ServerRequest req, ServerResponse res) { private void processCodeWithTenant(String code, ServerRequest req, ServerResponse res, String tenantName, Tenant tenant) { TenantConfig tenantConfig = tenant.tenantConfig(); - WebClient webClient = tenant.appWebClient(); + Http1Client webClient = tenant.appWebClient(); Parameters.Builder form = Parameters.builder("oidc-form-params") .add("grant_type", "authorization_code") .add("code", code) .add("redirect_uri", redirectUri(req, tenantName)); - WebClientRequestBuilder post = webClient.post() + Http1ClientRequest post = webClient.post() .uri(tenant.tokenEndpointUri()) - .accept(HttpMediaType.APPLICATION_JSON); + .header(Http.HeaderValues.ACCEPT_JSON); OidcUtil.updateRequest(OidcConfig.RequestType.CODE_TO_TOKEN, tenantConfig, form); - OidcConfig.postJsonResponse(post, - form.build(), - json -> processJsonResponse(req, res, json, tenantName), - (status, errorEntity) -> processError(res, status, errorEntity), - (t, message) -> processError(res, t, message)) - .await(); - + try (Http1ClientResponse response = post.submit(form.build())) { + if (response.status().family() == Http.Status.Family.SUCCESSFUL) { + try { + JsonObject jsonObject = response.as(JsonObject.class); + processJsonResponse(req, res, jsonObject, tenantName); + } catch (Exception e) { + processError(res, e, "Failed to read JSON from response"); + } + } else { + String message; + try { + message = response.as(String.class); + } catch (Exception e) { + processError(res, e, "Failed to process error entity"); + return; + } + try { + processError(res, response.status(), message); + } catch (Exception e) { + throw new SecurityException("Failed to process request: " + message); + } + } + } catch (Exception e) { + processError(res, e, "Failed to invoke request"); + } } private Object postLogoutUri(ServerRequest req) { @@ -445,28 +464,22 @@ private String processJsonResponse(ServerRequest req, res.headers().add(Http.Header.LOCATION, state); if (oidcConfig.useCookie()) { - ServerResponseHeaders headers = res.headers(); + try { + ServerResponseHeaders headers = res.headers(); + + OidcCookieHandler tenantCookieHandler = oidcConfig.tenantCookieHandler(); - OidcCookieHandler tenantCookieHandler = oidcConfig.tenantCookieHandler(); - tenantCookieHandler.createCookie(tenantName) - .forSingle(builder -> headers.addCookie(builder.build())) - .exceptionallyAccept(t -> sendError(res, t)); - - tokenCookieHandler.createCookie(tokenValue) - .forSingle(builder -> { - headers.addCookie(builder.build()); - if (idToken != null && oidcConfig.logoutEnabled()) { - idTokenCookieHandler.createCookie(idToken) - .forSingle(it -> { - headers.addCookie(it.build()); - res.send(); - }) - .exceptionallyAccept(t -> sendError(res, t)); - } else { - res.send(); - } - }) - .exceptionallyAccept(t -> sendError(res, t)); + headers.addCookie(tenantCookieHandler.createCookie(tenantName).build()); //Add tenant name cookie + headers.addCookie(tokenCookieHandler.createCookie(tokenValue).build()); //Add token cookie + + if (idToken != null && oidcConfig.logoutEnabled()) { + headers.addCookie(idTokenCookieHandler.createCookie(idToken).build()); //Add token id cookie + } + res.send(); + + } catch (Exception e) { + sendError(res, e); + } } else { res.send(); } @@ -508,7 +521,7 @@ private Optional processError(ServerResponse res, Throwable t, String me // if they try to provide wrong data private void sendErrorResponse(ServerResponse serverResponse) { serverResponse.status(Http.Status.UNAUTHORIZED_401); - serverResponse.send("Not a valid authorization code"); + serverResponse.send("Not a valid authorization code2"); } String increaseRedirectCounter(String state) { diff --git a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java index cbbe3d64f3d..8e09a708d23 100644 --- a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java +++ b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/OidcProvider.java @@ -26,13 +26,10 @@ import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; import java.util.stream.Collectors; import io.helidon.common.HelidonServiceLoader; import io.helidon.common.configurable.LruCache; -import io.helidon.common.reactive.Single; import io.helidon.config.Config; import io.helidon.config.DeprecatedConfig; import io.helidon.config.metadata.Configured; @@ -134,41 +131,37 @@ public Collection> supportedAnnotations() { } @Override - public CompletionStage authenticate(ProviderRequest providerRequest) { - Single tenantIdSingle = tenantIdFinders.stream() + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { + String tenantId = tenantIdFinders.stream() .map(tenantIdFinder -> tenantIdFinder.tenantId(providerRequest)) .flatMap(Optional::stream) .findFirst() - .map(Single::just) .orElseGet(() -> findTenantIdFromRedirects(providerRequest)); - return tenantIdSingle.flatMapCompletionStage(tenantId -> authenticateWithTenant(tenantId, providerRequest)); + + return authenticateWithTenant(tenantId, providerRequest); } - private CompletionStage authenticateWithTenant(String tenantId, ProviderRequest providerRequest) { + private AuthenticationResponse authenticateWithTenant(String tenantId, ProviderRequest providerRequest) { Optional tenantHandler = tenantAuthHandlers.get(tenantId); if (tenantHandler.isPresent()) { return tenantHandler.get().authenticate(tenantId, providerRequest); } else { - return CompletableFuture.supplyAsync( - () -> { - TenantConfig possibleConfig = tenantConfigFinders.stream() - .map(tenantConfigFinder -> tenantConfigFinder.config(tenantId)) - .flatMap(Optional::stream) - .findFirst() - .orElse(oidcConfig.tenantConfig(tenantId)); - Tenant tenant = Tenant.create(oidcConfig, possibleConfig); - TenantAuthenticationHandler handler = new TenantAuthenticationHandler(oidcConfig, - tenant, - useJwtGroups, - optional); - return tenantAuthHandlers.computeValue(tenantId, () -> Optional.of(handler)).get(); - }, - providerRequest.securityContext().executorService()) - .thenCompose(handler -> handler.authenticate(tenantId, providerRequest)); + TenantConfig possibleConfig = tenantConfigFinders.stream() + .map(tenantConfigFinder -> tenantConfigFinder.config(tenantId)) + .flatMap(Optional::stream) + .findFirst() + .orElse(oidcConfig.tenantConfig(tenantId)); + Tenant tenant = Tenant.create(oidcConfig, possibleConfig); + TenantAuthenticationHandler handler = new TenantAuthenticationHandler(oidcConfig, + tenant, + useJwtGroups, + optional); + return tenantAuthHandlers.computeValue(tenantId, () -> Optional.of(handler)).get() + .authenticate(tenantId, providerRequest); } } - private Single findTenantIdFromRedirects(ProviderRequest providerRequest) { + private String findTenantIdFromRedirects(ProviderRequest providerRequest) { List missingLocations = new LinkedList<>(); Optional tenantId = Optional.empty(); missingLocations.add("tenant-id-finder"); @@ -180,7 +173,7 @@ private Single findTenantIdFromRedirects(ProviderRequest providerRequest } } if (oidcConfig.useCookie() && tenantId.isEmpty()) { - Optional> cookie = oidcConfig.tenantCookieHandler() + Optional cookie = oidcConfig.tenantCookieHandler() .findCookie(providerRequest.env().headers()); if (cookie.isPresent()) { @@ -189,14 +182,14 @@ private Single findTenantIdFromRedirects(ProviderRequest providerRequest missingLocations.add("cookie"); } if (tenantId.isPresent()) { - return Single.just(tenantId.get()); + return tenantId.get(); } else { if (LOGGER.isLoggable(Level.DEBUG)) { LOGGER.log(Level.DEBUG, "Missing tenant id, could not find in either of: " + missingLocations + "Falling back to the default tenant id: " + DEFAULT_TENANT_ID); } - return Single.just(DEFAULT_TENANT_ID); + return DEFAULT_TENANT_ID; } } @@ -213,9 +206,9 @@ public boolean isOutboundSupported(ProviderRequest providerRequest, } @Override - public CompletionStage outboundSecurity(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundEndpointConfig) { + public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv, + EndpointConfig outboundEndpointConfig) { Optional user = providerRequest.securityContext().user(); if (user.isPresent()) { @@ -232,12 +225,12 @@ public CompletionStage outboundSecurity(ProviderReques if (enabled) { Map> headers = new HashMap<>(outboundEnv.headers()); target.tokenHandler.header(headers, tokenContent); - return CompletableFuture.completedFuture(OutboundSecurityResponse.withHeaders(headers)); + return OutboundSecurityResponse.withHeaders(headers); } } } - return CompletableFuture.completedFuture(OutboundSecurityResponse.empty()); + return OutboundSecurityResponse.empty(); } /** diff --git a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/TenantAuthenticationHandler.java b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/TenantAuthenticationHandler.java index 20738cf86ba..d5afe1d4594 100644 --- a/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/TenantAuthenticationHandler.java +++ b/security/providers/oidc/src/main/java/io/helidon/security/providers/oidc/TenantAuthenticationHandler.java @@ -26,8 +26,6 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.regex.Matcher; @@ -36,10 +34,9 @@ import io.helidon.common.Errors; import io.helidon.common.http.Http; -import io.helidon.common.media.type.MediaTypes; import io.helidon.common.parameters.Parameters; -import io.helidon.common.reactive.Single; -import io.helidon.reactive.webclient.WebClientRequestBuilder; +import io.helidon.nima.webclient.http1.Http1ClientRequest; +import io.helidon.nima.webclient.http1.Http1ClientResponse; import io.helidon.security.AuthenticationResponse; import io.helidon.security.EndpointConfig; import io.helidon.security.Grant; @@ -48,6 +45,7 @@ import io.helidon.security.Role; import io.helidon.security.Security; import io.helidon.security.SecurityEnvironment; +import io.helidon.security.SecurityException; import io.helidon.security.SecurityLevel; import io.helidon.security.SecurityResponse; import io.helidon.security.Subject; @@ -63,7 +61,8 @@ import io.helidon.security.providers.oidc.common.TenantConfig; import io.helidon.security.util.TokenHandler; -import static io.helidon.security.providers.oidc.common.OidcConfig.postJsonResponse; +import jakarta.json.JsonObject; + import static io.helidon.security.providers.oidc.common.spi.TenantConfigFinder.DEFAULT_TENANT_ID; /** @@ -78,7 +77,7 @@ class TenantAuthenticationHandler { private final TenantConfig tenantConfig; private final Tenant tenant; private final boolean useJwtGroups; - private final BiFunction> jwtValidator; + private final BiFunction jwtValidator; private final BiConsumer scopeAppender; private final Pattern attemptPattern; @@ -108,17 +107,17 @@ class TenantAuthenticationHandler { break; } }); - return Single.just(collector); + return collector; }; } else { this.jwtValidator = (signedJwt, collector) -> { Parameters.Builder form = Parameters.builder("oidc-form-params") .add("token", signedJwt.tokenContent()); - WebClientRequestBuilder post = tenant.appWebClient() + Http1ClientRequest post = tenant.appWebClient() .post() .uri(tenant.introspectUri()) - .accept(MediaTypes.APPLICATION_JSON) + .header(Http.HeaderValues.ACCEPT_JSON) .headers(it -> { it.add(Http.Header.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); return it; @@ -126,24 +125,32 @@ class TenantAuthenticationHandler { OidcUtil.updateRequest(OidcConfig.RequestType.INTROSPECT_JWT, tenantConfig, form); - return postJsonResponse(post, - form.build(), - json -> { - if (!json.getBoolean("active")) { - collector.fatal(json, "Token is not active"); - } - return collector; - }, - (status, message) -> - Optional.of(collector.fatal(status, - "Failed to validate token, response " - + "status: " - + status - + ", entity: " + message)), - (t, message) -> - Optional.of(collector.fatal(t, - "Failed to validate token, request failed: " - + message))); + try (Http1ClientResponse response = post.submit(form.build())) { + if (response.status().family() == Http.Status.Family.SUCCESSFUL) { + try { + JsonObject jsonObject = response.as(JsonObject.class); + if (!jsonObject.getBoolean("active")) { + collector.fatal(jsonObject, "Token is not active"); + } + } catch (Exception e) { + collector.fatal(e, "Failed to validate token, request failed: " + + "Failed to read JSON from response"); + } + } else { + String message; + try { + message = response.as(String.class); + collector.fatal(response.status(), + "Failed to validate token, response " + "status: " + response.status() + ", " + + "entity: " + message); + } catch (Exception e) { + collector.fatal(e, "Failed to validate token, request failed: Failed to process error entity"); + } + } + } catch (Exception e) { + collector.fatal(e, "Failed to validate token, request failed: Failed to invoke request"); + } + return collector; }; } // clean the scope audience - must end with / if exists @@ -161,7 +168,7 @@ class TenantAuthenticationHandler { } } - CompletionStage authenticate(String tenantId, ProviderRequest providerRequest) { + AuthenticationResponse authenticate(String tenantId, ProviderRequest providerRequest) { /* 1. Get token from request - if available, validate it and continue 2. If not - Redirect to login page @@ -193,23 +200,24 @@ CompletionStage authenticate(String tenantId, ProviderRe if (oidcConfig.useCookie()) { if (token.isEmpty()) { // only do this for cookies - Optional> cookie = oidcConfig.tokenCookieHandler() + Optional cookie = oidcConfig.tokenCookieHandler() .findCookie(providerRequest.env().headers()); if (cookie.isEmpty()) { missingLocations.add("cookie"); } else { - return cookie.get() - .flatMapSingle(it -> validateToken(tenantId, providerRequest, it)) - .onErrorResumeWithSingle(throwable -> { - if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { - LOGGER.log(System.Logger.Level.DEBUG, "Invalid token in cookie", throwable); - } - return Single.just(errorResponse(providerRequest, - Http.Status.UNAUTHORIZED_401, - null, - "Invalid token", - tenantId)); - }); + try { + String tokenValue = cookie.get(); + return validateToken(tenantId, providerRequest, tokenValue); + } catch (Exception e) { + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "Invalid token in cookie", e); + } + return errorResponse(providerRequest, + Http.Status.UNAUTHORIZED_401, + null, + "Invalid token", + tenantId); + } } } } @@ -222,12 +230,12 @@ CompletionStage authenticate(String tenantId, ProviderRe return validateToken(tenantId, providerRequest, token.get()); } else { LOGGER.log(System.Logger.Level.DEBUG, () -> "Missing token, could not find in either of: " + missingLocations); - return CompletableFuture.completedFuture(errorResponse(providerRequest, - Http.Status.UNAUTHORIZED_401, - null, - "Missing token, could not find in either of: " - + missingLocations, - tenantId)); + return errorResponse(providerRequest, + Http.Status.UNAUTHORIZED_401, + null, + "Missing token, could not find in either of: " + + missingLocations, + tenantId); } } @@ -334,17 +342,17 @@ private String redirectUri(SecurityEnvironment env) { return oidcConfig.redirectUriWithHost(); } - private CompletionStage failOrAbstain(String message) { + private AuthenticationResponse failOrAbstain(String message) { if (optional) { - return CompletableFuture.completedFuture(AuthenticationResponse.builder() - .status(SecurityResponse.SecurityStatus.ABSTAIN) - .description(message) - .build()); + return AuthenticationResponse.builder() + .status(SecurityResponse.SecurityStatus.ABSTAIN) + .description(message) + .build(); } else { - return CompletableFuture.completedFuture(AuthenticationResponse.builder() - .status(AuthenticationResponse.SecurityStatus.FAILURE) - .description(message) - .build()); + return AuthenticationResponse.builder() + .status(AuthenticationResponse.SecurityStatus.FAILURE) + .description(message) + .build(); } } @@ -403,27 +411,27 @@ private String encode(String state) { return URLEncoder.encode(state, StandardCharsets.UTF_8); } - private Single validateToken(String tenantId, - ProviderRequest providerRequest, - String token) { + private AuthenticationResponse validateToken(String tenantId, ProviderRequest providerRequest, String token) { SignedJwt signedJwt; try { signedJwt = SignedJwt.parseToken(token); } catch (Exception e) { //invalid token - LOGGER.log(System.Logger.Level.DEBUG, "Could not parse inbound token", e); - return Single.just(AuthenticationResponse.failed("Invalid token", e)); + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "Could not parse inbound token", e); + } + return AuthenticationResponse.failed("Invalid token", e); } - return jwtValidator.apply(signedJwt, Errors.collector()) - .map(it -> processValidationResult(providerRequest, - signedJwt, - tenantId, - it)) - .onErrorResume(t -> { - LOGGER.log(System.Logger.Level.DEBUG, "Failed to validate request", t); - return AuthenticationResponse.failed("Failed to validate JWT", t); - }); + try { + Errors.Collector collector = jwtValidator.apply(signedJwt, Errors.collector()); + return processValidationResult(providerRequest, signedJwt, tenantId, collector); + } catch (Exception e) { + if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.DEBUG, "Failed to validate request", e); + } + return AuthenticationResponse.failed("Failed to validate JWT", e); + } } private AuthenticationResponse processValidationResult(ProviderRequest providerRequest, diff --git a/security/providers/oidc/src/main/java/module-info.java b/security/providers/oidc/src/main/java/module-info.java index 10542a1414c..0bc6cd9afee 100644 --- a/security/providers/oidc/src/main/java/module-info.java +++ b/security/providers/oidc/src/main/java/module-info.java @@ -31,6 +31,7 @@ requires io.helidon.config; requires io.helidon.common; requires io.helidon.common.crypto; + requires io.helidon.nima.webclient; requires io.helidon.security; requires io.helidon.security.providers.oidc.common; @@ -38,7 +39,6 @@ requires io.helidon.security.util; requires io.helidon.security.abac.scope; requires io.helidon.security.jwt; - requires io.helidon.reactive.webclient; requires io.helidon.cors; requires static io.helidon.nima.webserver; diff --git a/security/providers/oidc/src/test/java/io/helidon/security/providers/oidc/OidcFeatureTest.java b/security/providers/oidc/src/test/java/io/helidon/security/providers/oidc/OidcFeatureTest.java index e6418f4a9bf..ca85d2a04cd 100644 --- a/security/providers/oidc/src/test/java/io/helidon/security/providers/oidc/OidcFeatureTest.java +++ b/security/providers/oidc/src/test/java/io/helidon/security/providers/oidc/OidcFeatureTest.java @@ -180,9 +180,7 @@ void testOutbound() { .build(); EndpointConfig endpointConfig = EndpointConfig.builder().build(); - OutboundSecurityResponse response = provider.outboundSecurity(providerRequest, outboundEnv, endpointConfig) - .toCompletableFuture() - .join(); + OutboundSecurityResponse response = provider.outboundSecurity(providerRequest, outboundEnv, endpointConfig); List authorization = response.requestHeaders().get("Authorization"); assertThat("Authorization header", authorization, hasItem("Bearer " + tokenContent)); @@ -214,9 +212,7 @@ void testOutboundFull() { boolean outboundSupported = provider.isOutboundSupported(providerRequest, outboundEnv, endpointConfig); assertThat("Outbound should not be supported by default", outboundSupported, is(false)); - OutboundSecurityResponse response = provider.outboundSecurity(providerRequest, outboundEnv, endpointConfig) - .toCompletableFuture() - .join(); + OutboundSecurityResponse response = provider.outboundSecurity(providerRequest, outboundEnv, endpointConfig); assertThat("Disabled target should have empty headers", response.requestHeaders().size(), is(0)); } diff --git a/security/security/src/main/java/io/helidon/security/AuthenticationClientImpl.java b/security/security/src/main/java/io/helidon/security/AuthenticationClientImpl.java index 9a2c6685956..fb20fbe73f9 100644 --- a/security/security/src/main/java/io/helidon/security/AuthenticationClientImpl.java +++ b/security/security/src/main/java/io/helidon/security/AuthenticationClientImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package io.helidon.security; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - import io.helidon.security.internal.SecurityAuditEvent; import io.helidon.security.spi.AuthenticationProvider; @@ -42,49 +39,41 @@ final class AuthenticationClientImpl implements SecurityClient submit() { - return security.resolveAtnProvider(providerName) + public AuthenticationResponse submit() { + AuthenticationResponse authenticationResponse = security.resolveAtnProvider(providerName) .map(this::authenticate) .orElseThrow(() -> new SecurityException("Could not find any authentication provider. Security is not " - + "configured")) - .thenCompose(authenticationResponse -> { - CompletionStage response = mapSubject( - authenticationResponse); - return response; - }); - + + "configured")); + return mapSubject(authenticationResponse); } - private CompletionStage mapSubject(AuthenticationResponse prevResponse) { - ProviderRequest providerRequest = new ProviderRequest(context, - request.resources()); + private AuthenticationResponse mapSubject(AuthenticationResponse prevResponse) { + ProviderRequest providerRequest = new ProviderRequest(context, request.resources()); if (prevResponse.status() == SecurityResponse.SecurityStatus.SUCCESS) { - return security.subjectMapper() + AuthenticationResponse newResponse = security.subjectMapper() .map(mapper -> mapper.map(providerRequest, prevResponse)) - .orElseGet(() -> CompletableFuture.completedFuture(prevResponse)) - .thenApply(newResponse -> { - // intentionally checking for instance equality, as that means we are guaranteed no changes - if (newResponse == prevResponse) { - // no changes were done, response as is - return prevResponse; - } else { - newResponse.user().ifPresent(context::setUser); - newResponse.service().ifPresent(context::setService); - return newResponse; - } - }); + .orElseGet(() -> prevResponse); + + if (newResponse == prevResponse) { + // no changes were done, response as is + return prevResponse; + } else { + newResponse.user().ifPresent(context::setUser); + newResponse.service().ifPresent(context::setService); + return newResponse; + } } else { - return CompletableFuture.completedFuture(prevResponse); + return prevResponse; } } - private CompletionStage authenticate(AuthenticationProvider providerInstance) { + private AuthenticationResponse authenticate(AuthenticationProvider providerInstance) { // prepare request to provider - ProviderRequest providerRequest = new ProviderRequest(context, - request.resources()); + ProviderRequest providerRequest = new ProviderRequest(context, request.resources()); + AuthenticationResponse response = providerInstance.authenticate(providerRequest); - return providerInstance.authenticate(providerRequest).thenApply(response -> { + try { if (response.status().isSuccess()) { response.user() .ifPresent(context::setUser); @@ -114,17 +103,16 @@ private CompletionStage authenticate(AuthenticationProvi .map(e -> event.addParam(AuditEvent.AuditParam.plain("exception", response.throwable()))); context.audit(event); return response; - }).exceptionally(throwable -> { + } catch (Exception e) { //Audit failure context.audit(SecurityAuditEvent .error(AuditEvent.AUTHN_TYPE_PREFIX + ".authenticate", "Provider %s. Message: %s") .addParam(AuditEvent.AuditParam .plain("provider", providerInstance.getClass().getName())) - .addParam(AuditEvent.AuditParam.plain("message", throwable.getMessage())) - .addParam(AuditEvent.AuditParam.plain("exception", throwable))); - - throw new SecurityException(throwable); - }); + .addParam(AuditEvent.AuditParam.plain("message", e.getMessage())) + .addParam(AuditEvent.AuditParam.plain("exception", e))); + throw new SecurityException(e); + } } } diff --git a/security/security/src/main/java/io/helidon/security/AuthorizationClientImpl.java b/security/security/src/main/java/io/helidon/security/AuthorizationClientImpl.java index 78f12461f85..83d9b3dad68 100644 --- a/security/security/src/main/java/io/helidon/security/AuthorizationClientImpl.java +++ b/security/security/src/main/java/io/helidon/security/AuthorizationClientImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,8 @@ package io.helidon.security; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - import io.helidon.security.internal.SecurityAuditEvent; +import io.helidon.security.spi.AuthorizationProvider; /** * Authorizer. @@ -44,52 +42,57 @@ final class AuthorizationClientImpl implements SecurityClient submit() { + public AuthorizationResponse submit() { // TODO ABAC - if annotated with Attribute meta annot, make sure that all are processed return security.resolveAtzProvider(providerName) - .map(providerInstance -> providerInstance.authorize(providerRequest).thenApply(response -> { - if (response.status().isSuccess()) { - //Audit success - context.audit(SecurityAuditEvent.success( + .map(this::authorize) + .orElse(AuthorizationResponse.permit()); + } + + private AuthorizationResponse authorize(AuthorizationProvider providerInstance) { + AuthorizationResponse response = providerInstance.authorize(providerRequest); + try { + if (response.status().isSuccess()) { + //Audit success + context.audit(SecurityAuditEvent.success( AuditEvent.AUTHZ_TYPE_PREFIX + ".authorize", "Path %s. Provider %s. Subject %s") - .addParam(AuditEvent.AuditParam.plain("path", providerRequest.env().path())) - .addParam(AuditEvent.AuditParam - .plain("provider", providerInstance.getClass().getName())) - .addParam(AuditEvent.AuditParam.plain("subject", - context.user()))); - } else { - //Audit failure - context.audit(SecurityAuditEvent.failure( + .addParam(AuditEvent.AuditParam.plain("path", providerRequest.env().path())) + .addParam(AuditEvent.AuditParam + .plain("provider", providerInstance.getClass().getName())) + .addParam(AuditEvent.AuditParam.plain("subject", + context.user()))); + } else { + //Audit failure + context.audit(SecurityAuditEvent.failure( AuditEvent.AUTHZ_TYPE_PREFIX + ".authorize", "Path %s. Provider %s, Description %s, Request %s. Subject %s") - .addParam(AuditEvent.AuditParam.plain("path", providerRequest.env().path())) - .addParam(AuditEvent.AuditParam - .plain("provider", providerInstance.getClass().getName())) - .addParam(AuditEvent.AuditParam.plain("request", this)) - .addParam(AuditEvent.AuditParam.plain("subject", context.user())) - .addParam(AuditEvent.AuditParam - .plain("message", response.description().orElse(null))) - .addParam(AuditEvent.AuditParam - .plain("exception", response.throwable().orElse(null)))); - } + .addParam(AuditEvent.AuditParam.plain("path", providerRequest.env().path())) + .addParam(AuditEvent.AuditParam + .plain("provider", providerInstance.getClass().getName())) + .addParam(AuditEvent.AuditParam.plain("request", this)) + .addParam(AuditEvent.AuditParam.plain("subject", context.user())) + .addParam(AuditEvent.AuditParam + .plain("message", response.description().orElse(null))) + .addParam(AuditEvent.AuditParam + .plain("exception", response.throwable().orElse(null)))); + } - return response; - }).exceptionally(throwable -> { - //Audit failure - context.audit(SecurityAuditEvent.error( + return response; + } catch (Exception e) { + //Audit failure + context.audit(SecurityAuditEvent.error( AuditEvent.AUTHZ_TYPE_PREFIX + ".authorize", "Path %s. Provider %s, Description %s, Request %s. Subject %s. %s: %s") - .addParam(AuditEvent.AuditParam.plain("path", providerRequest.env().path())) - .addParam(AuditEvent.AuditParam - .plain("provider", providerInstance.getClass().getName())) - .addParam(AuditEvent.AuditParam.plain("description", "Audit failure")) - .addParam(AuditEvent.AuditParam.plain("request", this)) - .addParam(AuditEvent.AuditParam.plain("subject", context.user())) - .addParam(AuditEvent.AuditParam.plain("message", throwable.getMessage())) - .addParam(AuditEvent.AuditParam.plain("exception", throwable))); - throw new SecurityException(throwable); - })) - .orElse(CompletableFuture.completedFuture(AuthorizationResponse.permit())); + .addParam(AuditEvent.AuditParam.plain("path", providerRequest.env().path())) + .addParam(AuditEvent.AuditParam + .plain("provider", providerInstance.getClass().getName())) + .addParam(AuditEvent.AuditParam.plain("description", "Audit failure")) + .addParam(AuditEvent.AuditParam.plain("request", this)) + .addParam(AuditEvent.AuditParam.plain("subject", context.user())) + .addParam(AuditEvent.AuditParam.plain("message", e.getMessage())) + .addParam(AuditEvent.AuditParam.plain("exception", e))); + throw new SecurityException(e); + } } } diff --git a/security/security/src/main/java/io/helidon/security/CompositeAuthenticationProvider.java b/security/security/src/main/java/io/helidon/security/CompositeAuthenticationProvider.java index b139b81da26..6fb6955f427 100644 --- a/security/security/src/main/java/io/helidon/security/CompositeAuthenticationProvider.java +++ b/security/security/src/main/java/io/helidon/security/CompositeAuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,8 +23,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; import io.helidon.security.spi.AuthenticationProvider; import io.helidon.security.spi.ProviderConfig; @@ -74,17 +72,14 @@ public Collection supportedAttributes() { } @Override - public CompletionStage authenticate(ProviderRequest providerRequest) { - CompletionStage result = CompletableFuture.completedFuture(new AtnResponse(ABSTAIN_RESPONSE)); - - for (Atn providerConfig : providers) { - // go through all providers and validate each response, collecting successes - result = result.thenCompose(theResponse -> invokeProvider(theResponse, providerConfig, providerRequest)); - } - - return result.thenApply(atnResponse -> { - // when we get here, we should have all the successes and the response is the last one + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { + AtnResponse atnResponse = new AtnResponse(ABSTAIN_RESPONSE); + try { + for (Atn providerConfig : providers) { + // go through all providers and validate each response, collecting successes + atnResponse = invokeProvider(atnResponse, providerConfig, providerRequest); + } List successes = atnResponse.successResponses; if (successes.isEmpty()) { // no success - abstain @@ -98,50 +93,48 @@ public CompletionStage authenticate(ProviderRequest prov // build response return responseBuilder.build(); - }).exceptionally(throwable -> { - Throwable cause = throwable.getCause(); + } catch (Exception exception) { + Throwable cause = exception.getCause(); if (null == cause) { - cause = throwable; + cause = exception; } if (cause instanceof AsyncAtnException) { return ((AsyncAtnException) cause).response; } - return AuthenticationResponse.failed("Failed processing: " + throwable.getMessage(), throwable); - }); + return AuthenticationResponse.failed("Failed processing: " + exception.getMessage(), exception); + } } - private CompletionStage invokeProvider(AtnResponse previous, - Atn nextProviderConfig, - ProviderRequest providerRequest) { + private AtnResponse invokeProvider(AtnResponse previous, + Atn nextProviderConfig, + ProviderRequest providerRequest) { List successes = previous.successResponses; CompositeProviderFlag flag = nextProviderConfig.config.flag(); - return nextProviderConfig.provider - .authenticate(providerRequest) - .thenApply(atnResponse -> { - checkAtnResponseStatus(flag, atnResponse, atnResponse.status()); - if (atnResponse.status() == SUCCESS) { - successes.add(atnResponse); - } - if ((flag == SUFFICIENT) && (atnResponse.status() == SUCCESS)) { - // no need to go any further - AuthenticationResponse.Builder responseBuilder = AuthenticationResponse.builder(); - combineSubjects(successes, responseBuilder); - - // build response - AuthenticationResponse newResponse = responseBuilder - .status(SUCCESS) - .build(); - throw new AsyncAtnException(newResponse); - } - - if (atnResponse.status() == ABSTAIN) { - // if we abstain, we want to return the previous response - return new AtnResponse(previous.response, successes); - } - - return new AtnResponse(atnResponse, successes); - }); + AuthenticationResponse atnResponse = nextProviderConfig.provider.authenticate(providerRequest); + + checkAtnResponseStatus(flag, atnResponse, atnResponse.status()); + if (atnResponse.status() == SUCCESS) { + successes.add(atnResponse); + } + if ((flag == SUFFICIENT) && (atnResponse.status() == SUCCESS)) { + // no need to go any further + AuthenticationResponse.Builder responseBuilder = AuthenticationResponse.builder(); + combineSubjects(successes, responseBuilder); + + // build response + AuthenticationResponse newResponse = responseBuilder + .status(SUCCESS) + .build(); + throw new AsyncAtnException(newResponse); + } + + if (atnResponse.status() == ABSTAIN) { + // if we abstain, we want to return the previous response + return new AtnResponse(previous.response, successes); + } + + return new AtnResponse(atnResponse, successes); } private void combineSubjects(List successes, AuthenticationResponse.Builder responseBuilder) { diff --git a/security/security/src/main/java/io/helidon/security/CompositeAuthorizationProvider.java b/security/security/src/main/java/io/helidon/security/CompositeAuthorizationProvider.java index 638abe17d7f..1dde5e53d7c 100644 --- a/security/security/src/main/java/io/helidon/security/CompositeAuthorizationProvider.java +++ b/security/security/src/main/java/io/helidon/security/CompositeAuthorizationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; import io.helidon.security.spi.AuthorizationProvider; import io.helidon.security.spi.ProviderConfig; @@ -67,41 +65,33 @@ public Collection supportedAttributes() { } @Override - public CompletionStage authorize(ProviderRequest context) { - CompletionStage previous = CompletableFuture.completedFuture(AuthorizationResponse.abstain()); - - for (Atz providerConfig : providers) { - previous = previous.thenCombine(providerConfig.provider.authorize(context), - (prevResponse, thisResponse) -> processProvider(providerConfig, - prevResponse, - thisResponse)); - } + public AuthorizationResponse authorize(ProviderRequest context) { + AuthorizationResponse previous = AuthorizationResponse.abstain(); - return previous.exceptionally(throwable -> { - Throwable cause = throwable.getCause(); + try { + for (Atz providerConfig : providers) { + AuthorizationResponse thisResponse = providerConfig.provider.authorize(context); + previous = processProvider(providerConfig, previous, thisResponse); + } + } catch (Exception exception) { + Throwable cause = exception.getCause(); if (null == cause) { - cause = throwable; + cause = exception; } if (cause instanceof AsyncAtzException) { return ((AsyncAtzException) cause).response; } return AuthorizationResponse.builder() .status(SecurityResponse.SecurityStatus.FAILURE) - .description("Failed processing: " + throwable.getMessage()) - .throwable(throwable) + .description("Failed processing: " + exception.getMessage()) + .throwable(exception) .build(); - }).thenApply(atzResponse -> { - if (atzResponse.status() == SecurityResponse.SecurityStatus.ABSTAIN) { - // TODO how to resolve optional - too many places to configure it - // if (context.getSecurityContext().getEndpointConfig().isOptional()) { - // return AuthorizationResponse.permit(); - // } else { - // return AuthorizationResponse.abstain(); - // } - return AuthorizationResponse.abstain(); - } - return atzResponse; - }); + } + + if (previous.status() == SecurityResponse.SecurityStatus.ABSTAIN) { + return AuthorizationResponse.abstain(); + } + return previous; } private AuthorizationResponse processProvider(Atz providerConfig, diff --git a/security/security/src/main/java/io/helidon/security/CompositeOutboundProvider.java b/security/security/src/main/java/io/helidon/security/CompositeOutboundProvider.java index 9e6b34e1509..3b2ba7ad68e 100644 --- a/security/security/src/main/java/io/helidon/security/CompositeOutboundProvider.java +++ b/security/security/src/main/java/io/helidon/security/CompositeOutboundProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; import io.helidon.security.spi.OutboundSecurityProvider; @@ -54,57 +52,56 @@ public Collection> supportedAnnotations() { } @Override - public CompletionStage outboundSecurity(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundConfig) { + public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv, + EndpointConfig outboundConfig) { - CompletionStage previous = CompletableFuture - .completedFuture(new OutboundCall(OutboundSecurityResponse.abstain(), - providerRequest, - outboundEnv, - outboundConfig)); + OutboundCall previousCall = new OutboundCall(OutboundSecurityResponse.abstain(), + providerRequest, + outboundEnv, + outboundConfig); for (OutboundSecurityProvider provider : providers) { - previous = previous.thenCompose(call -> { - if (call.response.status() == SecurityResponse.SecurityStatus.ABSTAIN) { - // previous call(s) did not care, I don't have to update request - if (provider.isOutboundSupported(call.inboundContext, call.outboundEnv, call.outboundConfig)) { - return provider.outboundSecurity(call.inboundContext, call.outboundEnv, call.outboundConfig) - .thenApply(response -> { - SecurityEnvironment nextEnv = updateRequestHeaders(call.outboundEnv, response); - return new OutboundCall(response, call.inboundContext, nextEnv, call.outboundConfig); - }); - } else { - // just continue with existing result - return CompletableFuture.completedFuture(call); - } + if (previousCall.response.status() == SecurityResponse.SecurityStatus.ABSTAIN) { + // previous call(s) did not care, I don't have to update request + if (provider.isOutboundSupported(previousCall.inboundContext, + previousCall.outboundEnv, + previousCall.outboundConfig)) { + OutboundSecurityResponse outboundResponse = provider.outboundSecurity(previousCall.inboundContext, + previousCall.outboundEnv, + previousCall.outboundConfig); + SecurityEnvironment nextEnv = updateRequestHeaders(previousCall.outboundEnv, outboundResponse); + previousCall = new OutboundCall(outboundResponse, + previousCall.inboundContext, + nextEnv, + previousCall.outboundConfig); } - // construct a new request - if (call.response.status().isSuccess()) { - // invoke current - return provider.outboundSecurity(call.inboundContext, call.outboundEnv, call.outboundConfig) - .thenApply(thisResponse -> { - OutboundSecurityResponse prevResponse = call.response; - - // combine - OutboundSecurityResponse.Builder builder = OutboundSecurityResponse.builder(); - prevResponse.requestHeaders().forEach(builder::requestHeader); - prevResponse.responseHeaders().forEach(builder::responseHeader); - thisResponse.requestHeaders().forEach(builder::requestHeader); - thisResponse.responseHeaders().forEach(builder::responseHeader); - SecurityEnvironment nextEnv = updateRequestHeaders(call.outboundEnv, thisResponse); - - builder.status(thisResponse.status()); - return new OutboundCall(builder.build(), call.inboundContext, nextEnv, call.outboundConfig); - }); - } else { - // just fail (as previous outbound all failed) - return CompletableFuture.completedFuture(call); - } - }); + } + // construct a new request + if (previousCall.response.status().isSuccess()) { + // invoke current + OutboundSecurityResponse outboundResponse = provider.outboundSecurity(previousCall.inboundContext, + previousCall.outboundEnv, + previousCall.outboundConfig); + OutboundSecurityResponse prevResponse = previousCall.response; + + // combine + OutboundSecurityResponse.Builder builder = OutboundSecurityResponse.builder(); + prevResponse.requestHeaders().forEach(builder::requestHeader); + prevResponse.responseHeaders().forEach(builder::responseHeader); + outboundResponse.requestHeaders().forEach(builder::requestHeader); + outboundResponse.responseHeaders().forEach(builder::responseHeader); + SecurityEnvironment nextEnv = updateRequestHeaders(previousCall.outboundEnv, outboundResponse); + + builder.status(outboundResponse.status()); + previousCall = new OutboundCall(builder.build(), + previousCall.inboundContext, + nextEnv, + previousCall.outboundConfig); + } } - return previous.thenApply(outboundCall -> outboundCall.response); + return previousCall.response; } private SecurityEnvironment updateRequestHeaders(SecurityEnvironment env, OutboundSecurityResponse response) { diff --git a/security/security/src/main/java/io/helidon/security/OutboundSecurityClientBuilder.java b/security/security/src/main/java/io/helidon/security/OutboundSecurityClientBuilder.java index 9ff95761aa1..5ddd8fde7da 100644 --- a/security/security/src/main/java/io/helidon/security/OutboundSecurityClientBuilder.java +++ b/security/security/src/main/java/io/helidon/security/OutboundSecurityClientBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package io.helidon.security; -import java.util.concurrent.CompletionStage; import java.util.function.Supplier; import io.helidon.common.Builder; @@ -109,9 +108,9 @@ public OutboundSecurityResponse buildAndGet() { /** * A shortcut method to build the client and invoke {@link SecurityClient#submit()} on it. * - * @return {@link CompletionStage} with {@link SecurityResponse} of expected type + * @return {@link SecurityResponse} of expected type */ - public CompletionStage submit() { + public OutboundSecurityResponse submit() { return build().submit(); } } diff --git a/security/security/src/main/java/io/helidon/security/OutboundSecurityClientImpl.java b/security/security/src/main/java/io/helidon/security/OutboundSecurityClientImpl.java index defc5caae9b..29253af4fc4 100644 --- a/security/security/src/main/java/io/helidon/security/OutboundSecurityClientImpl.java +++ b/security/security/src/main/java/io/helidon/security/OutboundSecurityClientImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,6 @@ package io.helidon.security; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - import io.helidon.security.internal.SecurityAuditEvent; import io.helidon.security.spi.OutboundSecurityProvider; @@ -52,14 +49,15 @@ final class OutboundSecurityClientImpl implements SecurityClient submit() { + public OutboundSecurityResponse submit() { OutboundSecurityProvider providerInstance = findProvider(); if (null == providerInstance) { - return CompletableFuture.completedFuture(OutboundSecurityResponse.empty()); + return OutboundSecurityResponse.empty(); } - return providerInstance.outboundSecurity(providerRequest, outboundEnv, outboundEpConfig).thenApply(response -> { + try { + OutboundSecurityResponse response = providerInstance.outboundSecurity(providerRequest, outboundEnv, outboundEpConfig); if (response.status().isSuccess()) { //Audit success context.audit(SecurityAuditEvent.success(AuditEvent.OUTBOUND_TYPE_PREFIX + ".outbound", @@ -84,7 +82,7 @@ public CompletionStage submit() { } return response; - }).exceptionally(e -> { + } catch (Exception e) { context.audit(SecurityAuditEvent.error(AuditEvent.OUTBOUND_TYPE_PREFIX + ".outbound", "Provider %s, Description %s, Request %s. Subject %s") .addParam(AuditEvent.AuditParam.plain("provider", providerInstance.getClass().getName())) @@ -94,7 +92,7 @@ public CompletionStage submit() { .addParam(AuditEvent.AuditParam .plain("subject", context.user().orElse(SecurityContext.ANONYMOUS)))); throw new SecurityException("Failed to process security", e); - }); + } } private OutboundSecurityProvider findProvider() { diff --git a/security/security/src/main/java/io/helidon/security/Security.java b/security/security/src/main/java/io/helidon/security/Security.java index bcd3e74d46c..dd122709890 100644 --- a/security/security/src/main/java/io/helidon/security/Security.java +++ b/security/security/src/main/java/io/helidon/security/Security.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,17 +30,12 @@ import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import io.helidon.common.HelidonServiceLoader; -import io.helidon.common.configurable.ThreadPoolSupplier; -import io.helidon.common.reactive.Single; import io.helidon.config.Config; import io.helidon.config.ConfigValue; import io.helidon.config.metadata.Configured; @@ -203,7 +198,7 @@ static Set getRoles(Subject subject) { * @param bytesToEncrypt bytes to encrypt * @return future with cipher text */ - Single encrypt(String configurationName, byte[] bytesToEncrypt); + String encrypt(String configurationName, byte[] bytesToEncrypt); /** * Decrypt cipher text. @@ -214,7 +209,7 @@ static Set getRoles(Subject subject) { * @param cipherText cipher text to decrypt * @return future with decrypted bytes */ - Single decrypt(String configurationName, String cipherText); + byte[] decrypt(String configurationName, String cipherText); /** * Create a digest for the provided bytes. @@ -224,7 +219,7 @@ static Set getRoles(Subject subject) { * @param preHashed whether the data is already a hash * @return future with digest (such as signature or HMAC) */ - Single digest(String configurationName, byte[] bytesToDigest, boolean preHashed); + String digest(String configurationName, byte[] bytesToDigest, boolean preHashed); /** * Create a digest for the provided raw bytes. @@ -233,7 +228,7 @@ static Set getRoles(Subject subject) { * @param bytesToDigest data to digest * @return future with digest (such as signature or HMAC) */ - Single digest(String configurationName, byte[] bytesToDigest); + String digest(String configurationName, byte[] bytesToDigest); /** * Verify a digest. @@ -244,7 +239,7 @@ static Set getRoles(Subject subject) { * @param preHashed whether the data is already a hash * @return future with result of verification ({@code true} means the digest is valid) */ - Single verifyDigest(String configurationName, byte[] bytesToDigest, String digest, boolean preHashed); + boolean verifyDigest(String configurationName, byte[] bytesToDigest, String digest, boolean preHashed); /** * Verify a digest. @@ -254,7 +249,7 @@ static Set getRoles(Subject subject) { * @param digest digest as provided by a third party (or another component) * @return future with result of verification ({@code true} means the digest is valid) */ - Single verifyDigest(String configurationName, byte[] bytesToDigest, String digest); + boolean verifyDigest(String configurationName, byte[] bytesToDigest, String digest); /** * Get a secret. @@ -262,7 +257,7 @@ static Set getRoles(Subject subject) { * @param configurationName name of the secret configuration * @return future with the secret value, or error if the secret is not configured */ - Single> secret(String configurationName); + Optional secret(String configurationName); /** * Get a secret. @@ -271,7 +266,7 @@ static Set getRoles(Subject subject) { * @param defaultValue default value to use if secret not configured * @return future with the secret value */ - Single secret(String configurationName, String defaultValue); + String secret(String configurationName, String defaultValue); /** * Security environment builder, to be used to create @@ -313,13 +308,6 @@ static Set getRoles(Subject subject) { */ ProviderSelectionPolicy providerSelectionPolicy(); - /** - * Executor service to handle possible blocking tasks in security. - * - * @return executor service supplier (may be backed by a lazy implementation) - */ - Supplier executorService(); - /** * Find an authentication provider by name, or use the default if the name is not available. * @@ -361,7 +349,7 @@ final class Builder implements io.helidon.common.Builder { private final Map> digestProviders = new HashMap<>(); private final Map allProviders = new IdentityHashMap<>(); - private final Map>>> secrets = new HashMap<>(); + private final Map>> secrets = new HashMap<>(); private final Map encryptions = new HashMap<>(); private final Map digests = new HashMap<>(); private final Set providerNames = new HashSet<>(); @@ -374,7 +362,6 @@ final class Builder implements io.helidon.common.Builder { private Tracer tracer; private boolean tracingEnabled = true; private SecurityTime serverTime = SecurityTime.builder().build(); - private Supplier executorService = ThreadPoolSupplier.create("security-thread-pool"); private boolean enabled = true; private Builder() { @@ -885,8 +872,7 @@ public Security build() { } if (atnProviders.isEmpty()) { - addAuthenticationProvider(context -> CompletableFuture - .completedFuture(AuthenticationResponse.success(SecurityContext.ANONYMOUS)), "default"); + addAuthenticationProvider(context -> AuthenticationResponse.success(SecurityContext.ANONYMOUS), "default"); } if (atzProviders.isEmpty()) { @@ -978,7 +964,6 @@ private void fromConfig(Config config) { } config.get("environment.server-time").as(SecurityTime::create).ifPresent(this::serverTime); - executorService(ThreadPoolSupplier.create(config.get("environment.executor-service"), "security-thread-pool")); Map configKeyToService = new HashMap<>(); Map classNameToService = new HashMap<>(); @@ -1165,18 +1150,6 @@ private void providerFromConfig(Map configKeyTo } } - /** - * Configure executor service to be used for blocking operations within security. - * - * @param supplier supplier of an executor service, as as {@link io.helidon.common.configurable.ThreadPoolSupplier} - * @return updated builder - */ - @ConfiguredOption(key = "environment.executor-service", type = ThreadPoolSupplier.class) - public Builder executorService(Supplier supplier) { - this.executorService = supplier; - return this; - } - private String resolveProviderName(Config pConf, String className, Config providerSpecificConfig, @@ -1402,7 +1375,7 @@ Map allProviders() { return allProviders; } - Map>>> secrets() { + Map>> secrets() { return secrets; } @@ -1450,19 +1423,14 @@ SecurityTime serverTime() { return serverTime; } - Supplier executorService() { - return executorService; - } - boolean enabled() { return enabled; } private static class DefaultAtzProvider implements AuthorizationProvider { @Override - public CompletionStage authorize(ProviderRequest context) { - return CompletableFuture - .completedFuture(AuthorizationResponse.permit()); + public AuthorizationResponse authorize(ProviderRequest context) { + return AuthorizationResponse.permit(); } } } diff --git a/security/security/src/main/java/io/helidon/security/SecurityClient.java b/security/security/src/main/java/io/helidon/security/SecurityClient.java index 914f887d5d7..ae2ee7e0187 100644 --- a/security/security/src/main/java/io/helidon/security/SecurityClient.java +++ b/security/security/src/main/java/io/helidon/security/SecurityClient.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package io.helidon.security; -import java.util.concurrent.CompletionStage; - /** * Common methods for security clients. * @@ -33,7 +31,7 @@ public interface SecurityClient { * SecurityResponse.SecurityStatus#isSuccess()} * Otherwise security request failed or could not be processed. */ - CompletionStage submit(); + T submit(); /** * Synchronous complement to {@link #submit()}. @@ -43,7 +41,7 @@ public interface SecurityClient { * @throws SecurityException in case of timeout, interrupted call or exception during future processing */ default T get() { - return SecurityResponse.get(submit()); + return submit(); } } diff --git a/security/security/src/main/java/io/helidon/security/SecurityClientBuilder.java b/security/security/src/main/java/io/helidon/security/SecurityClientBuilder.java index 8740bdfa0d2..4902f10c0af 100644 --- a/security/security/src/main/java/io/helidon/security/SecurityClientBuilder.java +++ b/security/security/src/main/java/io/helidon/security/SecurityClientBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package io.helidon.security; -import java.util.concurrent.CompletionStage; - import io.helidon.common.Builder; /** @@ -64,9 +62,9 @@ public T buildAndGet() { /** * A shortcut method to build the client and invoke {@link SecurityClient#submit()} on it. * - * @return {@link CompletionStage} with {@link SecurityResponse} of expected type + * @return {@link SecurityResponse} of expected type */ - public CompletionStage submit() { + public T submit() { return build().submit(); } diff --git a/security/security/src/main/java/io/helidon/security/SecurityContext.java b/security/security/src/main/java/io/helidon/security/SecurityContext.java index 644dfa7fc1f..025512ea085 100644 --- a/security/security/src/main/java/io/helidon/security/SecurityContext.java +++ b/security/security/src/main/java/io/helidon/security/SecurityContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package io.helidon.security; import java.util.Optional; -import java.util.concurrent.ExecutorService; import java.util.function.Supplier; import io.helidon.tracing.SpanContext; @@ -126,13 +125,6 @@ public interface SecurityContext extends io.helidon.common.security.SecurityCont boolean isUserInRole(String role, String authorizerName); - /** - * Executor service of the security module. - * - * @return executor service to use to execute asynchronous tasks related to security - */ - ExecutorService executorService(); - /** * Check if user is in specified role if supported by global authorization provider. * @@ -327,7 +319,6 @@ default boolean atzChecked() { class Builder implements io.helidon.common.Builder { private final Security security; private String id; - private Supplier executorServiceSupplier; private SecurityTime serverTime; private Tracer tracingTracer; private SpanContext tracingSpan; @@ -336,7 +327,6 @@ class Builder implements io.helidon.common.Builder { Builder(Security security) { this.security = security; - this.executorServiceSupplier = security.executorService(); } @Override @@ -361,10 +351,6 @@ String id() { return id; } - Supplier executorServiceSupplier() { - return executorServiceSupplier; - } - SecurityTime serverTime() { return serverTime; } @@ -400,36 +386,6 @@ public Builder id(String id) { return this; } - /** - * Executor service to use for requests within this context. - * By default uses a custom executor service that is configured when building - * {@link Security} instance. - *

    - * Use this method only if you need to override default behavior! - * - * @param executorServiceSupplier supplier of an executor service - * @return updated builder instance - */ - public Builder executorService(Supplier executorServiceSupplier) { - this.executorServiceSupplier = executorServiceSupplier; - return this; - } - - /** - * Executor service to use for requests within this context. - * By default uses a custom executor service that is configured when building - * {@link Security} instance. - *

    - * Use this method only if you need to override default behavior! - * - * @param executorService executor service - * @return updated builder instance - */ - public Builder executorService(ExecutorService executorService) { - this.executorServiceSupplier = () -> executorService; - return this; - } - /** * SecurityTime to use when determining current time. Used e.g. when * creating a new {@link SecurityEnvironment}. diff --git a/security/security/src/main/java/io/helidon/security/SecurityContextImpl.java b/security/security/src/main/java/io/helidon/security/SecurityContextImpl.java index be869ddca2e..1ea290e00b9 100644 --- a/security/security/src/main/java/io/helidon/security/SecurityContextImpl.java +++ b/security/security/src/main/java/io/helidon/security/SecurityContextImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,10 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.function.Supplier; import io.helidon.common.context.Contexts; import io.helidon.security.internal.SecurityAuditEvent; @@ -39,7 +37,6 @@ final class SecurityContextImpl implements SecurityContext { private final Security security; private final String tracingId; private final SpanContext requestSpan; - private final Supplier executorService; private final Tracer securityTracer; private final SecurityTime serverTime; private final ReadWriteLock envLock = new ReentrantReadWriteLock(); @@ -55,7 +52,6 @@ final class SecurityContextImpl implements SecurityContext { this.security = builder.security(); this.tracingId = builder.id(); this.requestSpan = builder.tracingSpan(); - this.executorService = builder.executorServiceSupplier(); this.securityTracer = builder.tracingTracer(); this.serverTime = builder.serverTime(); this.environment = builder.env(); @@ -130,11 +126,6 @@ public void logout() { currentSubject = ANONYMOUS; } - @Override - public ExecutorService executorService() { - return executorService.get(); - } - @SuppressWarnings("unchecked") @Override public boolean isUserInRole(String role) { diff --git a/security/security/src/main/java/io/helidon/security/SecurityImpl.java b/security/security/src/main/java/io/helidon/security/SecurityImpl.java index 1ec3c109a20..e886bfd55d5 100644 --- a/security/security/src/main/java/io/helidon/security/SecurityImpl.java +++ b/security/security/src/main/java/io/helidon/security/SecurityImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,13 +24,11 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ExecutorService; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.logging.Logger; import io.helidon.common.LazyValue; -import io.helidon.common.reactive.Single; import io.helidon.config.Config; import io.helidon.security.internal.SecurityAuditEvent; import io.helidon.security.spi.AuditProvider; @@ -70,11 +68,10 @@ final class SecurityImpl implements Security { private final ProviderSelectionPolicy providerSelectionPolicy; private final LazyValue securityTracer; private final SecurityTime serverTime; - private final Supplier executorService; private final Config securityConfig; private final boolean enabled; - private final Map>>> secrets; + private final Map>> secrets; private final Map encryptions; private final Map digests; @@ -83,7 +80,6 @@ final class SecurityImpl implements Security { this.enabled = builder.enabled(); this.instanceUuid = UUID.randomUUID().toString(); this.serverTime = builder.serverTime(); - this.executorService = builder.executorService(); this.annotations.addAll(SecurityUtil.getAnnotations(builder.allProviders())); this.securityTracer = LazyValue.create(() -> SecurityUtil.getTracer(builder.tracingEnabled(), builder.tracer())); this.subjectMappingProvider = Optional.ofNullable(builder.subjectMappingProvider()); @@ -173,7 +169,6 @@ public SecurityContext.Builder contextBuilder(String id) { String newId = ((null == id) || id.isEmpty()) ? (instanceUuid + ":?") : (instanceUuid + ":" + id); return new SecurityContext.Builder(this) .id(newId) - .executorService(executorService) .tracingTracer(securityTracer.get()) .serverTime(serverTime); } @@ -209,73 +204,72 @@ public Config configFor(String child) { } @Override - public Single encrypt(String configurationName, byte[] bytesToEncrypt) { + public String encrypt(String configurationName, byte[] bytesToEncrypt) { EncryptionProvider.EncryptionSupport encryption = encryptions.get(configurationName); if (encryption == null) { - return Single.error(new SecurityException("There is no configured encryption named " + configurationName)); + throw new SecurityException("There is no configured encryption named " + configurationName); } return encryption.encrypt(bytesToEncrypt); } @Override - public Single decrypt(String configurationName, String cipherText) { + public byte[] decrypt(String configurationName, String cipherText) { EncryptionProvider.EncryptionSupport encryption = encryptions.get(configurationName); if (encryption == null) { - return Single.error(new SecurityException("There is no configured encryption named " + configurationName)); + throw new SecurityException("There is no configured encryption named " + configurationName); } return encryption.decrypt(cipherText); } @Override - public Single digest(String configurationName, byte[] bytesToDigest, boolean preHashed) { + public String digest(String configurationName, byte[] bytesToDigest, boolean preHashed) { DigestProvider.DigestSupport digest = digests.get(configurationName); if (digest == null) { - return Single.error(new SecurityException("There is no configured digest named " + configurationName)); + throw new SecurityException("There is no configured digest named " + configurationName); } return digest.digest(bytesToDigest, preHashed); } @Override - public Single digest(String configurationName, byte[] bytesToDigest) { + public String digest(String configurationName, byte[] bytesToDigest) { return digest(configurationName, bytesToDigest, false); } @Override - public Single verifyDigest(String configurationName, byte[] bytesToDigest, String digest, boolean preHashed) { + public boolean verifyDigest(String configurationName, byte[] bytesToDigest, String digest, boolean preHashed) { DigestProvider.DigestSupport digestSupport = digests.get(configurationName); if (digest == null) { - return Single.error(new SecurityException("There is no configured digest named " + configurationName)); + throw new SecurityException("There is no configured digest named " + configurationName); } return digestSupport.verify(bytesToDigest, preHashed, digest); } @Override - public Single verifyDigest(String configurationName, byte[] bytesToDigest, String digest) { + public boolean verifyDigest(String configurationName, byte[] bytesToDigest, String digest) { return verifyDigest(configurationName, bytesToDigest, digest, false); } @Override - public Single> secret(String configurationName) { - Supplier>> singleSupplier = secrets.get(configurationName); - if (singleSupplier == null) { - return Single.error(new SecurityException("Secret \"" + configurationName + "\" is not configured.")); + public Optional secret(String configurationName) { + Supplier> supplier = secrets.get(configurationName); + if (supplier == null) { + throw new SecurityException("Secret \"" + configurationName + "\" is not configured."); } - return singleSupplier.get(); + return supplier.get(); } @Override - public Single secret(String configurationName, String defaultValue) { - Supplier>> singleSupplier = secrets.get(configurationName); - if (singleSupplier == null) { + public String secret(String configurationName, String defaultValue) { + Supplier> supplier = secrets.get(configurationName); + if (supplier == null) { LOGGER.finest(() -> "There is no configured secret named " + configurationName + ", using default value"); - return Single.just(defaultValue); + return defaultValue; } - return singleSupplier.get() - .map(it -> it.orElse(defaultValue)); + return supplier.get().orElse(defaultValue); } @Override @@ -308,11 +302,6 @@ public ProviderSelectionPolicy providerSelectionPolicy() { return providerSelectionPolicy; } - @Override - public Supplier executorService() { - return executorService; - } - @Override public Optional resolveAtnProvider(String providerName) { return resolveProvider(AuthenticationProvider.class, providerName); diff --git a/security/security/src/main/java/io/helidon/security/SecurityResponse.java b/security/security/src/main/java/io/helidon/security/SecurityResponse.java index f5b91893202..f564f920c23 100644 --- a/security/security/src/main/java/io/helidon/security/SecurityResponse.java +++ b/security/security/src/main/java/io/helidon/security/SecurityResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,6 @@ import java.util.Map; import java.util.Optional; import java.util.OptionalInt; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import io.helidon.common.Builder; @@ -48,27 +44,6 @@ public abstract class SecurityResponse { this.statusCode = builder.statusCode; } - /** - * Synchronize a completion stage. - * - * @param stage future response - * @param type the future response will provide - * @return instance the future returns - * @throws SecurityException in case of timeout, interrupted call or exception during future processing - */ - static T get(CompletionStage stage) { - try { - // since java 9 this method is not optional, so we can safely call it - return stage.toCompletableFuture().get(60, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new SecurityException("Interrupted while waiting for completion stage to complete", e); - } catch (ExecutionException e) { - throw new SecurityException("Failure while executing asynchronous security", e); - } catch (TimeoutException e) { - throw new SecurityException("Timed out after waiting for completion stage to complete", e); - } - } - /** * Status of this response. * diff --git a/security/security/src/main/java/io/helidon/security/spi/AuthenticationProvider.java b/security/security/src/main/java/io/helidon/security/spi/AuthenticationProvider.java index a688f5c4dbe..a858405a1ae 100644 --- a/security/security/src/main/java/io/helidon/security/spi/AuthenticationProvider.java +++ b/security/security/src/main/java/io/helidon/security/spi/AuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package io.helidon.security.spi; -import java.util.concurrent.CompletionStage; - import io.helidon.security.AuthenticationResponse; import io.helidon.security.Principal; import io.helidon.security.ProviderRequest; @@ -50,5 +48,5 @@ public interface AuthenticationProvider extends SecurityProvider { * @return response that either authenticates the request, fails authentication or abstains from authentication * @see AuthenticationResponse#success(Subject) */ - CompletionStage authenticate(ProviderRequest providerRequest); + AuthenticationResponse authenticate(ProviderRequest providerRequest); } diff --git a/security/security/src/main/java/io/helidon/security/spi/AuthorizationProvider.java b/security/security/src/main/java/io/helidon/security/spi/AuthorizationProvider.java index a22ebf33d2a..f365539c58f 100644 --- a/security/security/src/main/java/io/helidon/security/spi/AuthorizationProvider.java +++ b/security/security/src/main/java/io/helidon/security/spi/AuthorizationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package io.helidon.security.spi; -import java.util.concurrent.CompletionStage; - import io.helidon.security.AuthorizationResponse; import io.helidon.security.ProviderRequest; import io.helidon.security.Role; @@ -44,7 +42,7 @@ public interface AuthorizationProvider extends SecurityProvider { * @return response that either permits, denies or abstains from decision * @see AuthorizationResponse#permit() */ - CompletionStage authorize(ProviderRequest context); + AuthorizationResponse authorize(ProviderRequest context); /** * Return true if current user is in the specified role. diff --git a/security/security/src/main/java/io/helidon/security/spi/DigestProvider.java b/security/security/src/main/java/io/helidon/security/spi/DigestProvider.java index 7f8aa8f6e40..2304c18ed13 100644 --- a/security/security/src/main/java/io/helidon/security/spi/DigestProvider.java +++ b/security/security/src/main/java/io/helidon/security/spi/DigestProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package io.helidon.security.spi; -import io.helidon.common.reactive.Single; import io.helidon.config.Config; /** @@ -56,7 +55,7 @@ interface DigestFunction { * @param preHashed whether the data is already a hash ({@code true}), or the raw data ({@code false}) * @return future with the digest string (signature, HMAC) */ - Single apply(byte[] data, Boolean preHashed); + String apply(byte[] data, Boolean preHashed); } /** @@ -72,7 +71,7 @@ interface VerifyFunction { * @param digest original digest of the data (signature, HMAC) * @return future with the result of verification */ - Single apply(byte[] data, Boolean preHashed, String digest); + boolean apply(byte[] data, Boolean preHashed, String digest); } /** @@ -114,7 +113,7 @@ public static DigestSupport create(DigestFunction digestFunction, * @param preHashed whether the bytes are pre-hashed * @return future with the digest (signature or HMAC) */ - public Single digest(byte[] bytes, boolean preHashed) { + public String digest(byte[] bytes, boolean preHashed) { return digestFunction.apply(bytes, preHashed); } @@ -127,7 +126,7 @@ public Single digest(byte[] bytes, boolean preHashed) { * @return future with {@code true} if the digest is valid, {@code false} if not valid, and an error if not * a supported digest */ - public Single verify(byte[] bytes, boolean preHashed, String digest) { + public boolean verify(byte[] bytes, boolean preHashed, String digest) { return verifyFunction.apply(bytes, preHashed, digest); } } diff --git a/security/security/src/main/java/io/helidon/security/spi/EncryptionProvider.java b/security/security/src/main/java/io/helidon/security/spi/EncryptionProvider.java index 17d007abc96..31e0a848d12 100644 --- a/security/security/src/main/java/io/helidon/security/spi/EncryptionProvider.java +++ b/security/security/src/main/java/io/helidon/security/spi/EncryptionProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.util.function.Function; -import io.helidon.common.reactive.Single; import io.helidon.config.Config; /** @@ -49,8 +48,8 @@ public interface EncryptionProvider extends SecurityPr * Encryption support created for each named encryption configuration. */ class EncryptionSupport { - private final Function> encryptionFunction; - private final Function> decryptionFunction; + private final Function encryptionFunction; + private final Function decryptionFunction; /** * Encryption support based on the two functions. @@ -58,8 +57,8 @@ class EncryptionSupport { * @param encryptionFunction encrypts the provided bytes into cipher text * @param decryptionFunction decrypts cipher text into bytes */ - protected EncryptionSupport(Function> encryptionFunction, - Function> decryptionFunction) { + protected EncryptionSupport(Function encryptionFunction, + Function decryptionFunction) { this.encryptionFunction = encryptionFunction; this.decryptionFunction = decryptionFunction; } @@ -71,8 +70,8 @@ protected EncryptionSupport(Function> encryptionFunction, * @param decryptionFunction decrypts cipher text into bytes * @return new encryption support */ - public static EncryptionSupport create(Function> encryptionFunction, - Function> decryptionFunction) { + public static EncryptionSupport create(Function encryptionFunction, + Function decryptionFunction) { return new EncryptionSupport(encryptionFunction, decryptionFunction); } @@ -82,7 +81,7 @@ public static EncryptionSupport create(Function> encrypti * @param bytes bytes to encrypt * @return future with the encrypted cipher text */ - public Single encrypt(byte[] bytes) { + public String encrypt(byte[] bytes) { return encryptionFunction.apply(bytes); } @@ -92,7 +91,7 @@ public Single encrypt(byte[] bytes) { * @param encrypted cipher text * @return future with the decrypted bytes */ - public Single decrypt(String encrypted) { + public byte[] decrypt(String encrypted) { return decryptionFunction.apply(encrypted); } } diff --git a/security/security/src/main/java/io/helidon/security/spi/OutboundSecurityProvider.java b/security/security/src/main/java/io/helidon/security/spi/OutboundSecurityProvider.java index d68bd409019..a6520d4d8ef 100644 --- a/security/security/src/main/java/io/helidon/security/spi/OutboundSecurityProvider.java +++ b/security/security/src/main/java/io/helidon/security/spi/OutboundSecurityProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,6 @@ package io.helidon.security.spi; -import java.util.concurrent.CompletionStage; - import io.helidon.security.EndpointConfig; import io.helidon.security.OutboundSecurityResponse; import io.helidon.security.ProviderRequest; @@ -63,7 +61,7 @@ default boolean isOutboundSupported(ProviderRequest providerRequest, * @return response with generated headers and other possible configuration * @see OutboundSecurityResponse#builder() */ - CompletionStage outboundSecurity(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundConfig); + OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv, + EndpointConfig outboundConfig); } diff --git a/security/security/src/main/java/io/helidon/security/spi/SecretsProvider.java b/security/security/src/main/java/io/helidon/security/spi/SecretsProvider.java index d3928592f9c..2dbcfb749ed 100644 --- a/security/security/src/main/java/io/helidon/security/spi/SecretsProvider.java +++ b/security/security/src/main/java/io/helidon/security/spi/SecretsProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import java.util.Optional; import java.util.function.Supplier; -import io.helidon.common.reactive.Single; import io.helidon.config.Config; /** @@ -34,7 +33,7 @@ public interface SecretsProvider extends SecurityProvi * @param config config located on the node of the specific secret {@code config} node * @return supplier to retrieve the secret */ - Supplier>> secret(Config config); + Supplier> secret(Config config); /** * Create secret supplier from configuration object. @@ -42,5 +41,5 @@ public interface SecretsProvider extends SecurityProvi * @param providerConfig configuration of a specific secret * @return supplier to retrieve the secret */ - Supplier>> secret(T providerConfig); + Supplier> secret(T providerConfig); } diff --git a/security/security/src/main/java/io/helidon/security/spi/SubjectMappingProvider.java b/security/security/src/main/java/io/helidon/security/spi/SubjectMappingProvider.java index e6828c911a6..41311fc0012 100644 --- a/security/security/src/main/java/io/helidon/security/spi/SubjectMappingProvider.java +++ b/security/security/src/main/java/io/helidon/security/spi/SubjectMappingProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,6 @@ */ package io.helidon.security.spi; -import java.util.concurrent.CompletionStage; - import io.helidon.security.AuthenticationResponse; import io.helidon.security.ProviderRequest; @@ -42,5 +40,5 @@ public interface SubjectMappingProvider extends SecurityProvider { * @param previousResponse response from previous authentication or subject mapping provider * @return a new authentication response with updated user and/or service subjects */ - CompletionStage map(ProviderRequest providerRequest, AuthenticationResponse previousResponse); + AuthenticationResponse map(ProviderRequest providerRequest, AuthenticationResponse previousResponse); } diff --git a/security/security/src/main/java/io/helidon/security/spi/SynchronousProvider.java b/security/security/src/main/java/io/helidon/security/spi/SynchronousProvider.java deleted file mode 100644 index 901570cbebc..00000000000 --- a/security/security/src/main/java/io/helidon/security/spi/SynchronousProvider.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.security.spi; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - -import io.helidon.security.AuthenticationResponse; -import io.helidon.security.AuthorizationResponse; -import io.helidon.security.EndpointConfig; -import io.helidon.security.OutboundSecurityResponse; -import io.helidon.security.ProviderRequest; -import io.helidon.security.SecurityEnvironment; - -/** - * A provider base for synchronous providers. - * This class doesn't (intentionally) implement any of the interfaces, as we leave it up to you, provider developer - * to choose which of them suits your needs. - * Just override the method for your provider and let the magic begin. - * As java does not allow for multiple inheritance of classes, this is an easy way to implement methods - * for all SPI interfaces without forcing each provider to handle all types of security. - */ -public abstract class SynchronousProvider implements SecurityProvider { - /** - * Authenticate a request. - * This may be just resolving headers (tokens) or full authentication (basic auth). - * Do not throw exception for normal processing (e.g. invalid credentials; you may throw an exception in case of - * misconfiguration). - * - * This method will be invoked for inbound requests ONLY. - * - * @param providerRequest context of this security enforcement/validation - * @return AuthenticationResponse, including the subject for successful authentications - * @see AuthenticationResponse#success(io.helidon.security.Subject) - */ - public final CompletionStage authenticate(ProviderRequest providerRequest) { - return CompletableFuture - .supplyAsync(() -> syncAuthenticate(providerRequest), - providerRequest.securityContext().executorService()); - } - - /** - * Authorize a request based on configuration. - * - * Authorization cannot be optional. If this method is called, it should always attempt to authorize the current request. - * This method will be invoked for inbound requests ONLY. - * - * @param providerRequest context of this security enforcement/validation - * @return response that either permits, denies or abstains from decision - * @see AuthorizationResponse#permit() - */ - public final CompletionStage authorize(ProviderRequest providerRequest) { - return CompletableFuture.supplyAsync(() -> syncAuthorize(providerRequest), - providerRequest.securityContext().executorService()); - } - - /** - * Creates necessary updates to headers and entity needed for outbound - * security (e.g. identity propagation, s2s security etc.). - * This method will be invoked for outbound requests ONLY. - * - * @param providerRequest context with environment, subject(s) etc. that was received - * @param outboundEnv environment for outbound call - * @param outboundConfig outbound endpoint configuration - * @return response with generated headers and other possible configuration - * @see OutboundSecurityResponse#builder() - */ - public final CompletionStage outboundSecurity(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundConfig) { - return CompletableFuture.supplyAsync(() -> syncOutbound(providerRequest, outboundEnv, outboundConfig), - providerRequest.securityContext().executorService()); - } - - /** - * Synchronous authentication. - * - * @param providerRequest context with environment, subject(s) etc. - * @return authentication response - * @see AuthenticationProvider#authenticate(ProviderRequest) - */ - protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { - throw new UnsupportedOperationException("You must override syncAuthenticate method in your provider implementation " - + "to act as an AuthenticationProvider"); - } - - /** - * Synchronous authorization. - * - * @param providerRequest context with environment, subject(s) etc. - * @return authorization response - * @see AuthorizationProvider#authorize(ProviderRequest) - */ - protected AuthorizationResponse syncAuthorize(ProviderRequest providerRequest) { - throw new UnsupportedOperationException("You must override syncAuthorize method in your provider implementation " - + "to act as an AuthorizationProvider"); - } - - /** - * Synchronous outbound security. - * - * @param providerRequest context with environment, subject(s) etc. - * @param outboundEnv environment of this outbound call - * @param outboundEndpointConfig endpoint config for outbound call - * @return outbound response - * @see OutboundSecurityProvider#outboundSecurity(ProviderRequest, SecurityEnvironment, EndpointConfig) - * @see OutboundSecurityProvider#isOutboundSupported(ProviderRequest, SecurityEnvironment, EndpointConfig) - */ - protected OutboundSecurityResponse syncOutbound(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundEndpointConfig) { - throw new UnsupportedOperationException("You must override syncOutbound method in your provider implementation " - + "to act as an OutboundProvider"); - } - -} diff --git a/security/security/src/test/java/io/helidon/security/CompositePolicyFlagsTest.java b/security/security/src/test/java/io/helidon/security/CompositePolicyFlagsTest.java index 551e1618511..3cce6c8e246 100644 --- a/security/security/src/test/java/io/helidon/security/CompositePolicyFlagsTest.java +++ b/security/security/src/test/java/io/helidon/security/CompositePolicyFlagsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,49 +50,49 @@ private void testIt(TestConfig conf) { AuthenticationProvider atn = psp.selectProvider(AuthenticationProvider.class).get(); AuthorizationProvider atz = psp.selectProvider(AuthorizationProvider.class).get(); - SecurityResponse res = SecurityResponse.get(atn.authenticate(request("/jack", "jack"))); + SecurityResponse res = atn.authenticate(request("/jack", "jack")); assertThat(res.status(), is(conf.okOk)); - res = SecurityResponse.get(atz.authorize(request("/atz/permit", "atz/permit"))); + res = atz.authorize(request("/atz/permit", "atz/permit")); assertThat(res.status(), is(conf.okOk)); - res = SecurityResponse.get(atn.authenticate(request("/jack", "abstain"))); + res = atn.authenticate(request("/jack", "abstain")); assertThat(res.status(), is(conf.okAbstain)); - res = SecurityResponse.get(atz.authorize(request("/atz/permit", "atz/abstain"))); + res = atz.authorize(request("/atz/permit", "atz/abstain")); assertThat(res.status(), is(conf.okAbstain)); - res = SecurityResponse.get(atn.authenticate(request("/jack", "fail"))); + res = atn.authenticate(request("/jack", "fail")); assertThat(res.status(), is(conf.okFail)); - res = SecurityResponse.get(atz.authorize(request("/atz/permit", "atz/fail"))); + res = atz.authorize(request("/atz/permit", "atz/fail")); assertThat(res.status(), is(conf.okFail)); - res = SecurityResponse.get(atn.authenticate(request("/abstain", "jack"))); + res = atn.authenticate(request("/abstain", "jack")); assertThat(res.status(), is(conf.abstainOk)); - res = SecurityResponse.get(atz.authorize(request("/atz/abstain", "atz/permit"))); + res = atz.authorize(request("/atz/abstain", "atz/permit")); assertThat(res.status(), is(conf.abstainOk)); - res = SecurityResponse.get(atn.authenticate(request("/abstain", "abstain"))); + res = atn.authenticate(request("/abstain", "abstain")); assertThat(res.status(), is(conf.abstainAbstain)); - res = SecurityResponse.get(atz.authorize(request("/atz/abstain", "atz/abstain"))); + res = atz.authorize(request("/atz/abstain", "atz/abstain")); assertThat(res.status(), is(conf.abstainAbstain)); - res = SecurityResponse.get(atn.authenticate(request("/abstain", "fail"))); + res = atn.authenticate(request("/abstain", "fail")); assertThat(res.status(), is(conf.abstainFail)); - res = SecurityResponse.get(atz.authorize(request("/atz/abstain", "atz/fail"))); + res = atz.authorize(request("/atz/abstain", "atz/fail")); assertThat(res.status(), is(conf.abstainFail)); - res = SecurityResponse.get(atn.authenticate(request("/fail", "jack"))); + res = atn.authenticate(request("/fail", "jack")); assertThat(res.status(), is(conf.failOk)); - res = SecurityResponse.get(atz.authorize(request("/atz/fail", "atz/permit"))); + res = atz.authorize(request("/atz/fail", "atz/permit")); assertThat(res.status(), is(conf.failOk)); - res = SecurityResponse.get(atn.authenticate(request("/fail", "abstain"))); + res = atn.authenticate(request("/fail", "abstain")); assertThat(res.status(), is(conf.failAbstain)); - res = SecurityResponse.get(atz.authorize(request("/atz/fail", "atz/abstain"))); + res = atz.authorize(request("/atz/fail", "atz/abstain")); assertThat(res.status(), is(conf.failAbstain)); - res = SecurityResponse.get(atn.authenticate(request("/fail", "fail"))); + res = atn.authenticate(request("/fail", "fail")); assertThat(res.status(), is(conf.failFail)); - res = SecurityResponse.get(atz.authorize(request("/atz/fail", "atz/fail"))); + res = atz.authorize(request("/atz/fail", "atz/fail")); assertThat(res.status(), is(conf.failFail)); } diff --git a/security/security/src/test/java/io/helidon/security/CompositePolicyTest.java b/security/security/src/test/java/io/helidon/security/CompositePolicyTest.java index e47f15e4854..a81a0b1168f 100644 --- a/security/security/src/test/java/io/helidon/security/CompositePolicyTest.java +++ b/security/security/src/test/java/io/helidon/security/CompositePolicyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -97,20 +97,17 @@ public void testSuccessSecurity() { @Test public void testAtz() { - AuthorizationResponse response = SecurityResponse - .get(getAuthorization().authorize((context("/atz/permit", "atz/permit")))); + AuthorizationResponse response = getAuthorization().authorize((context("/atz/permit", "atz/permit"))); assertThat(response, notNullValue()); assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); - response = SecurityResponse - .get(getAuthorization().authorize((context("/atz/abstain", "atz/permit")))); + response = getAuthorization().authorize((context("/atz/abstain", "atz/permit"))); assertThat(response, notNullValue()); assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); - response = SecurityResponse - .get(getAuthorization().authorize((context("/atz/abstain", "atz/abstain")))); + response = getAuthorization().authorize((context("/atz/abstain", "atz/abstain"))); assertThat(response, notNullValue()); assertThat(response.status(), is(SecurityResponse.SecurityStatus.FAILURE)); @@ -118,9 +115,7 @@ public void testAtz() { @Test public void testAtnAllSuccess() throws ExecutionException, InterruptedException { - AuthenticationResponse response = getAuthentication().authenticate(context("/jack", "service")) - .toCompletableFuture() - .get(); + AuthenticationResponse response = getAuthentication().authenticate(context("/jack", "service")); assertThat(response, notNullValue()); assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); @@ -132,9 +127,7 @@ public void testAtnAllSuccess() throws ExecutionException, InterruptedException @Test public void testAtnAllSuccessServiceFirst() throws ExecutionException, InterruptedException { - AuthenticationResponse response = getAuthentication().authenticate(context("/service", "jack")) - .toCompletableFuture() - .get(); + AuthenticationResponse response = getAuthentication().authenticate(context("/service", "jack")); assertThat(response, notNullValue()); assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); Subject user = response.user().get(); @@ -151,8 +144,7 @@ public void testOutboundSuccess() throws ExecutionException, InterruptedExceptio OutboundSecurityResponse response = getOutbound().outboundSecurity(context, context.env(), - context.endpointConfig()).toCompletableFuture() - .get(); + context.endpointConfig()); assertThat(response.status(), is(SecurityResponse.SecurityStatus.SUCCESS)); diff --git a/security/security/src/test/java/io/helidon/security/SubjectMappingTest.java b/security/security/src/test/java/io/helidon/security/SubjectMappingTest.java index 016fd91b504..8762f1a43b8 100644 --- a/security/security/src/test/java/io/helidon/security/SubjectMappingTest.java +++ b/security/security/src/test/java/io/helidon/security/SubjectMappingTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -118,9 +118,8 @@ void testFailure() { private static class Mapper implements SubjectMappingProvider { @Override - public CompletionStage map(ProviderRequest providerRequest, - AuthenticationResponse previousResponse) { - return CompletableFuture.completedFuture(buildResponse(providerRequest, previousResponse)); + public AuthenticationResponse map(ProviderRequest providerRequest, AuthenticationResponse previousResponse) { + return buildResponse(providerRequest, previousResponse); } private AuthenticationResponse buildResponse(ProviderRequest providerRequest, @@ -158,8 +157,8 @@ private AuthenticationResponse mapSubject(Subject subject) { private static class Atn implements AuthenticationProvider { @Override - public CompletionStage authenticate(ProviderRequest providerRequest) { - return CompletableFuture.completedFuture(buildResponse(providerRequest)); + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { + return buildResponse(providerRequest); } private AuthenticationResponse buildResponse(ProviderRequest providerRequest) { diff --git a/security/security/src/test/java/io/helidon/security/SynchronousProviderTest.java b/security/security/src/test/java/io/helidon/security/SynchronousProviderTest.java deleted file mode 100644 index 97aa0b9800f..00000000000 --- a/security/security/src/test/java/io/helidon/security/SynchronousProviderTest.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.security; - -import io.helidon.security.spi.AuthenticationProvider; -import io.helidon.security.spi.AuthorizationProvider; -import io.helidon.security.spi.OutboundSecurityProvider; -import io.helidon.security.spi.SynchronousProvider; - -import org.junit.jupiter.api.Test; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -/** - * Unit test for {@link SynchronousProvider}. - * This is (mostly) a compilation based test - as the class does not directly implement the interfaces, I validate in this - * test that correct method signatures are present in it. - */ -public class SynchronousProviderTest { - @Test - public void testSecurity() { - Security security = Security.builder() - .addAuthenticationProvider(new Atn()) - .addAuthorizationProvider(new Atz()) - .addOutboundSecurityProvider(new Outbound()) - .build(); - - SecurityContext context = security.contextBuilder("unit_test").build(); - - AuthenticationResponse authenticationResponse = context.atnClientBuilder().buildAndGet(); - checkResponse(authenticationResponse); - AuthorizationResponse authorizationResponse = context.atzClientBuilder().buildAndGet(); - checkResponse(authorizationResponse); - OutboundSecurityResponse outboundSecurityResponse = context.outboundClientBuilder().buildAndGet(); - checkResponse(outboundSecurityResponse); - } - - private void checkResponse(SecurityResponse response) { - assertThat(response.status(), is(SecurityResponse.SecurityStatus.ABSTAIN)); - assertThat(response.description().isPresent(), is(true)); - assertThat(response.description().get(), is("unit.test")); - } - - private class Atn extends SynchronousProvider implements AuthenticationProvider { - @Override - protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { - return AuthenticationResponse.builder() - .description("unit.test") - .status(SecurityResponse.SecurityStatus.ABSTAIN) - .build(); - } - } - - private class Atz extends SynchronousProvider implements AuthorizationProvider { - @Override - protected AuthorizationResponse syncAuthorize(ProviderRequest providerRequest) { - return AuthorizationResponse.builder() - .description("unit.test") - .status(SecurityResponse.SecurityStatus.ABSTAIN) - .build(); - } - } - - private class Outbound extends SynchronousProvider implements OutboundSecurityProvider { - @Override - protected OutboundSecurityResponse syncOutbound(ProviderRequest providerRequest, - SecurityEnvironment outEnv, - EndpointConfig epc) { - return OutboundSecurityResponse.builder() - .description("unit.test") - .status(SecurityResponse.SecurityStatus.ABSTAIN) - .build(); - } - } -} diff --git a/security/security/src/test/java/io/helidon/security/providers/PathBasedProvider.java b/security/security/src/test/java/io/helidon/security/providers/PathBasedProvider.java index ed133438e72..067adb792bb 100644 --- a/security/security/src/test/java/io/helidon/security/providers/PathBasedProvider.java +++ b/security/security/src/test/java/io/helidon/security/providers/PathBasedProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,8 +38,8 @@ */ public class PathBasedProvider implements AuthenticationProvider, OutboundSecurityProvider, AuthorizationProvider { @Override - public CompletionStage authenticate(ProviderRequest providerRequest) { - return CompletableFuture.completedFuture(providerRequest.env().path().map(path -> { + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { + return providerRequest.env().path().map(path -> { switch (path) { case "/jack": return ResourceBasedProvider.success("path-jack"); @@ -62,12 +62,12 @@ public CompletionStage authenticate(ProviderRequest prov } return AuthenticationResponse.failed("path-Invalid request"); } - }).orElse(AuthenticationResponse.abstain())); + }).orElse(AuthenticationResponse.abstain()); } @Override - public CompletionStage authorize(ProviderRequest context) { - return CompletableFuture.completedFuture(context.env().path().map(path -> { + public AuthorizationResponse authorize(ProviderRequest context) { + return context.env().path().map(path -> { switch (path) { case "/atz/permit": return AuthorizationResponse.permit(); @@ -83,7 +83,7 @@ public CompletionStage authorize(ProviderRequest context) default: return AuthorizationResponse.permit(); } - }).orElse(AuthorizationResponse.abstain())); + }).orElse(AuthorizationResponse.abstain()); } @Override @@ -94,11 +94,11 @@ public boolean isOutboundSupported(ProviderRequest providerRequest, } @Override - public CompletionStage outboundSecurity(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundConfig) { + public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv, + EndpointConfig outboundConfig) { - return CompletableFuture.completedFuture(providerRequest.env().path().map(path -> { + return providerRequest.env().path().map(path -> { switch (path) { case "/jack": return OutboundSecurityResponse.withHeaders(Map.of("path", List.of("path-jack"))); @@ -119,6 +119,6 @@ public CompletionStage outboundSecurity(ProviderReques return OutboundSecurityResponse.builder().status(SecurityResponse.SecurityStatus.FAILURE) .description("path-Invalid request").build(); } - }).orElse(OutboundSecurityResponse.abstain())); + }).orElse(OutboundSecurityResponse.abstain()); } } diff --git a/security/security/src/test/java/io/helidon/security/providers/ProviderForTesting.java b/security/security/src/test/java/io/helidon/security/providers/ProviderForTesting.java index 403a86dbc57..87c770a2705 100644 --- a/security/security/src/test/java/io/helidon/security/providers/ProviderForTesting.java +++ b/security/security/src/test/java/io/helidon/security/providers/ProviderForTesting.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,13 +27,13 @@ import io.helidon.security.spi.AuthenticationProvider; import io.helidon.security.spi.AuthorizationProvider; import io.helidon.security.spi.OutboundSecurityProvider; -import io.helidon.security.spi.SynchronousProvider; +import io.helidon.security.spi.SecurityProvider; /** * Just a simple testing provider. */ -public class ProviderForTesting extends SynchronousProvider - implements AuthenticationProvider, AuthorizationProvider, OutboundSecurityProvider { +public class ProviderForTesting implements AuthenticationProvider, AuthorizationProvider, OutboundSecurityProvider, + SecurityProvider { private final String denyResource; public ProviderForTesting(String denyResource) { @@ -45,20 +45,20 @@ public static ProviderForTesting create(Config config) { } @Override - protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { return AuthenticationResponse .success(SecurityTest.SYSTEM); } @Override - protected OutboundSecurityResponse syncOutbound(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundEndpointConfig) { + public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv, + EndpointConfig outboundEndpointConfig) { return OutboundSecurityResponse.empty(); } @Override - protected AuthorizationResponse syncAuthorize(ProviderRequest providerRequest) { + public AuthorizationResponse authorize(ProviderRequest providerRequest) { String resource = providerRequest.env().abacAttribute("resourceType") .map(String::valueOf) .orElseThrow(() -> new IllegalArgumentException("Resource type is required")); diff --git a/security/security/src/test/java/io/helidon/security/providers/ResourceBasedProvider.java b/security/security/src/test/java/io/helidon/security/providers/ResourceBasedProvider.java index 6c843e09c8b..edf3a5f84d1 100644 --- a/security/security/src/test/java/io/helidon/security/providers/ResourceBasedProvider.java +++ b/security/security/src/test/java/io/helidon/security/providers/ResourceBasedProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,6 @@ import java.util.List; import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; import io.helidon.security.AuthenticationResponse; import io.helidon.security.AuthorizationResponse; @@ -47,61 +45,61 @@ static AuthenticationResponse success(String name) { } @Override - public CompletionStage authenticate(ProviderRequest providerRequest) { + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { SecurityEnvironment env = providerRequest.env(); - return CompletableFuture.completedFuture(env.abacAttribute("resourceType") - .map(String::valueOf) - .map(resource -> { - switch (resource) { - case "jack": - return success("resource-jack"); - case "jill": - return success("resource-jill"); - case "service": - return service("resource-aService"); - case "fail": - return AuthenticationResponse - .failed("resource-Intentional fail"); - case "successFinish": - return AuthenticationResponse.builder() - .status(SecurityResponse.SecurityStatus.SUCCESS_FINISH) - .user(SecurityTest.SYSTEM) - .build(); - case "abstain": - return AuthenticationResponse.abstain(); - default: - if (resource.startsWith("atz")) { - return success("atz"); - } - return AuthenticationResponse.failed("resource-Invalid request"); - } - }).orElse(AuthenticationResponse.abstain())); + return env.abacAttribute("resourceType") + .map(String::valueOf) + .map(resource -> { + switch (resource) { + case "jack": + return success("resource-jack"); + case "jill": + return success("resource-jill"); + case "service": + return service("resource-aService"); + case "fail": + return AuthenticationResponse + .failed("resource-Intentional fail"); + case "successFinish": + return AuthenticationResponse.builder() + .status(SecurityResponse.SecurityStatus.SUCCESS_FINISH) + .user(SecurityTest.SYSTEM) + .build(); + case "abstain": + return AuthenticationResponse.abstain(); + default: + if (resource.startsWith("atz")) { + return success("atz"); + } + return AuthenticationResponse.failed("resource-Invalid request"); + } + }).orElse(AuthenticationResponse.abstain()); } @Override - public CompletionStage authorize(ProviderRequest context) { + public AuthorizationResponse authorize(ProviderRequest context) { SecurityEnvironment env = context.env(); - return CompletableFuture.completedFuture(env.abacAttribute("resourceType") - .map(String::valueOf) - .map(resource -> { - switch (resource) { - case "atz/permit": - return AuthorizationResponse.permit(); - case "atz/deny": - return AuthorizationResponse.deny(); - case "atz/abstain": - return AuthorizationResponse.abstain(); - case "atz/fail": - return AuthorizationResponse.builder() - .status(SecurityResponse.SecurityStatus.FAILURE) - .description("Intentional failure") - .build(); - default: - return AuthorizationResponse.permit(); - } - }).orElse(AuthorizationResponse.abstain())); + return env.abacAttribute("resourceType") + .map(String::valueOf) + .map(resource -> { + switch (resource) { + case "atz/permit": + return AuthorizationResponse.permit(); + case "atz/deny": + return AuthorizationResponse.deny(); + case "atz/abstain": + return AuthorizationResponse.abstain(); + case "atz/fail": + return AuthorizationResponse.builder() + .status(SecurityResponse.SecurityStatus.FAILURE) + .description("Intentional failure") + .build(); + default: + return AuthorizationResponse.permit(); + } + }).orElse(AuthorizationResponse.abstain()); } @Override @@ -116,43 +114,43 @@ public boolean isOutboundSupported(ProviderRequest providerRequest, } @Override - public CompletionStage outboundSecurity(ProviderRequest providerRequest, - SecurityEnvironment outboundEnv, - EndpointConfig outboundConfig) { + public OutboundSecurityResponse outboundSecurity(ProviderRequest providerRequest, + SecurityEnvironment outboundEnv, + EndpointConfig outboundConfig) { - return CompletableFuture.completedFuture(providerRequest - .env() - .abacAttribute("resourceType") - .map(String::valueOf) - .map(resource -> { - switch (resource) { - case "jack": - return OutboundSecurityResponse - .withHeaders(Map.of("resource", - List.of("resource-jack"))); - case "jill": - return OutboundSecurityResponse - .withHeaders(Map.of("resource", - List.of("resource-jill"))); - case "service": - return OutboundSecurityResponse - .withHeaders(Map.of("resource", - List.of("resource-aService"))); - case "fail": - return OutboundSecurityResponse.builder() - .status(SecurityResponse.SecurityStatus.FAILURE) - .description("resource-Intentional fail").build(); - case "successFinish": - return OutboundSecurityResponse.builder() - .status(SecurityResponse.SecurityStatus.SUCCESS_FINISH) - .build(); - case "abstain": - return OutboundSecurityResponse.abstain(); - default: - return OutboundSecurityResponse.builder() - .status(SecurityResponse.SecurityStatus.FAILURE) - .description("resource-Invalid request").build(); - } - }).orElse(OutboundSecurityResponse.abstain())); + return providerRequest + .env() + .abacAttribute("resourceType") + .map(String::valueOf) + .map(resource -> { + switch (resource) { + case "jack": + return OutboundSecurityResponse + .withHeaders(Map.of("resource", + List.of("resource-jack"))); + case "jill": + return OutboundSecurityResponse + .withHeaders(Map.of("resource", + List.of("resource-jill"))); + case "service": + return OutboundSecurityResponse + .withHeaders(Map.of("resource", + List.of("resource-aService"))); + case "fail": + return OutboundSecurityResponse.builder() + .status(SecurityResponse.SecurityStatus.FAILURE) + .description("resource-Intentional fail").build(); + case "successFinish": + return OutboundSecurityResponse.builder() + .status(SecurityResponse.SecurityStatus.SUCCESS_FINISH) + .build(); + case "abstain": + return OutboundSecurityResponse.abstain(); + default: + return OutboundSecurityResponse.builder() + .status(SecurityResponse.SecurityStatus.FAILURE) + .description("resource-Invalid request").build(); + } + }).orElse(OutboundSecurityResponse.abstain()); } } diff --git a/tests/apps/bookstore/bookstore-mp/pom.xml b/tests/apps/bookstore/bookstore-mp/pom.xml index 54f8702f7e7..6bd07e2cd4e 100644 --- a/tests/apps/bookstore/bookstore-mp/pom.xml +++ b/tests/apps/bookstore/bookstore-mp/pom.xml @@ -60,7 +60,7 @@ ${project.version} - org.jboss + io.smallrye jandex runtime true @@ -94,7 +94,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/tests/functional/config-profiles/pom.xml b/tests/functional/config-profiles/pom.xml index 6fcb55e073e..1a0a2cdce40 100644 --- a/tests/functional/config-profiles/pom.xml +++ b/tests/functional/config-profiles/pom.xml @@ -34,12 +34,12 @@ - io.helidon.reactive.webserver - helidon-reactive-webserver + io.helidon.nima.webserver + helidon-nima-webserver - io.helidon.reactive.media - helidon-reactive-media-jsonp + io.helidon.nima.http.media + helidon-nima-http-media-jsonp io.helidon.config @@ -55,13 +55,13 @@ test - org.hamcrest - hamcrest-all + io.helidon.nima.testing.junit5 + helidon-nima-testing-junit5-webserver test - io.helidon.reactive.webclient - helidon-reactive-webclient + io.helidon.nima.webclient + helidon-nima-webclient test diff --git a/tests/functional/config-profiles/src/main/java/io/helidon/tests/configprofile/GreetService.java b/tests/functional/config-profiles/src/main/java/io/helidon/tests/configprofile/GreetService.java index ece9acb493b..e58ca2d2645 100644 --- a/tests/functional/config-profiles/src/main/java/io/helidon/tests/configprofile/GreetService.java +++ b/tests/functional/config-profiles/src/main/java/io/helidon/tests/configprofile/GreetService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,16 +20,16 @@ import java.util.concurrent.atomic.AtomicReference; import io.helidon.config.Config; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.ServerRequest; -import io.helidon.reactive.webserver.ServerResponse; -import io.helidon.reactive.webserver.Service; +import io.helidon.nima.webserver.http.HttpRules; +import io.helidon.nima.webserver.http.HttpService; +import io.helidon.nima.webserver.http.ServerRequest; +import io.helidon.nima.webserver.http.ServerResponse; import jakarta.json.Json; import jakarta.json.JsonBuilderFactory; import jakarta.json.JsonObject; -public class GreetService implements Service { +public class GreetService implements HttpService { private final AtomicReference greeting = new AtomicReference<>(); @@ -40,7 +40,7 @@ public class GreetService implements Service { } @Override - public void update(Routing.Rules rules) { + public void routing(HttpRules rules) { rules.get("/", this::getDefaultMessageHandler); } diff --git a/tests/functional/config-profiles/src/main/java/io/helidon/tests/configprofile/Main.java b/tests/functional/config-profiles/src/main/java/io/helidon/tests/configprofile/Main.java index 6f2a917dd4d..dad5a85af21 100644 --- a/tests/functional/config-profiles/src/main/java/io/helidon/tests/configprofile/Main.java +++ b/tests/functional/config-profiles/src/main/java/io/helidon/tests/configprofile/Main.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,10 @@ package io.helidon.tests.configprofile; -import io.helidon.common.reactive.Single; import io.helidon.config.Config; import io.helidon.logging.common.LogConfig; -import io.helidon.reactive.media.jsonp.JsonpSupport; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.WebServer; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http.HttpRouting; public final class Main { @@ -29,38 +27,19 @@ private Main() { } public static void main(final String[] args) { - startServer(); - } - - static Single startServer() { - LogConfig.configureRuntime(); - Config config = Config.create(); - - WebServer server = WebServer.builder(createRouting(config)) - .config(config.get("server")) - .addMediaSupport(JsonpSupport.create()) - .build(); - - Single webserver = server.start(); + WebServer server = WebServer.builder() + .routing(Main::routing) + .start(); - webserver.thenAccept(ws -> { - System.out.println("WEB server is up! http://localhost:" + ws.port() + "/greet"); - ws.whenShutdown().thenRun(() -> System.out.println("WEB server is DOWN. Good bye!")); - }) - .exceptionallyAccept(t -> { - System.err.println("Startup failed: " + t.getMessage()); - t.printStackTrace(System.err); - }); - - return webserver; + System.out.println("WEB server is up! http://localhost:" + server.port() + "/greet"); } - private static Routing createRouting(Config config) { + static void routing(HttpRouting.Builder routing) { + Config config = Config.create(); GreetService greetService = new GreetService(config); - return Routing.builder() - .register("/greet", greetService) - .build(); + routing.register("/greet", greetService) + .build(); } } diff --git a/tests/functional/config-profiles/src/test/java/io/helidon/tests/configprofile/BaseTest.java b/tests/functional/config-profiles/src/test/java/io/helidon/tests/configprofile/BaseTest.java deleted file mode 100644 index f5b6e7514e4..00000000000 --- a/tests/functional/config-profiles/src/test/java/io/helidon/tests/configprofile/BaseTest.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.tests.configprofile; - -import java.util.concurrent.TimeUnit; - -import io.helidon.reactive.media.jsonp.JsonpSupport; -import io.helidon.reactive.webclient.WebClient; -import io.helidon.reactive.webserver.WebServer; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; - -public class BaseTest { - - private static WebServer webServer; - private static WebClient webClient; - - @BeforeAll - public static void startTheServer() { - webServer = Main.startServer().await(); - - webClient = WebClient.builder() - .baseUri("http://localhost:" + webServer.port()) - .addMediaSupport(JsonpSupport.create()) - .build(); - } - - @AfterAll - public static void stopServer() throws Exception { - if (webServer != null) { - webServer.shutdown() - .toCompletableFuture() - .get(10, TimeUnit.SECONDS); - } - } - - public static WebServer webServer() { - return webServer; - } - - public static WebClient webClient() { - return webClient; - } -} diff --git a/tests/functional/config-profiles/src/test/java/io/helidon/tests/configprofile/DevTest.java b/tests/functional/config-profiles/src/test/java/io/helidon/tests/configprofile/DevTest.java index 60478f76ae8..178278baf2b 100644 --- a/tests/functional/config-profiles/src/test/java/io/helidon/tests/configprofile/DevTest.java +++ b/tests/functional/config-profiles/src/test/java/io/helidon/tests/configprofile/DevTest.java @@ -16,15 +16,31 @@ package io.helidon.tests.configprofile; -import jakarta.json.JsonObject; +import io.helidon.nima.testing.junit5.webserver.ServerTest; +import io.helidon.nima.testing.junit5.webserver.SetUpRoute; +import io.helidon.nima.webclient.http1.Http1Client; +import io.helidon.nima.webserver.http.HttpRouting; +import jakarta.json.JsonObject; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfSystemProperty; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -public class DevTest extends BaseTest { +@ServerTest +class DevTest { + + private final Http1Client client; + + protected DevTest(Http1Client client) { + this.client = client; + } + + @SetUpRoute + static void routing(HttpRouting.Builder builder) { + Main.routing(builder); + } /** * This test will only succeed if the 'dev' profile is enabled and the @@ -33,10 +49,9 @@ public class DevTest extends BaseTest { @Test @EnabledIfSystemProperty(named = "config.profile", matches = "dev") public void testHelloDevWorld() { - JsonObject jsonObject = webClient().get() + JsonObject jsonObject = client.get() .path("/greet") - .request(JsonObject.class) - .await(); + .request(JsonObject.class); assertThat(jsonObject.getString("message"), is("Hello Dev World!")); } } diff --git a/tests/functional/config-profiles/src/test/java/io/helidon/tests/configprofile/ProdTest.java b/tests/functional/config-profiles/src/test/java/io/helidon/tests/configprofile/ProdTest.java index 1925c21a04c..c6b97fd757c 100644 --- a/tests/functional/config-profiles/src/test/java/io/helidon/tests/configprofile/ProdTest.java +++ b/tests/functional/config-profiles/src/test/java/io/helidon/tests/configprofile/ProdTest.java @@ -16,15 +16,31 @@ package io.helidon.tests.configprofile; -import jakarta.json.JsonObject; +import io.helidon.nima.testing.junit5.webserver.ServerTest; +import io.helidon.nima.testing.junit5.webserver.SetUpRoute; +import io.helidon.nima.webclient.http1.Http1Client; +import io.helidon.nima.webserver.http.HttpRouting; +import jakarta.json.JsonObject; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfSystemProperty; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -public class ProdTest extends BaseTest { +@ServerTest +class ProdTest { + + private final Http1Client client; + + protected ProdTest(Http1Client client) { + this.client = client; + } + + @SetUpRoute + static void routing(HttpRouting.Builder builder) { + Main.routing(builder); + } /** * This test will only succeed if the 'prod' profile is enabled and the @@ -33,10 +49,9 @@ public class ProdTest extends BaseTest { @Test @EnabledIfSystemProperty(named = "config.profile", matches = "prod") public void testHelloDevWorld() { - JsonObject jsonObject = webClient().get() + JsonObject jsonObject = client.get() .path("/greet") - .request(JsonObject.class) - .await(); + .request(JsonObject.class); assertThat(jsonObject.getString("message"), is("Hello Prod World!")); } } diff --git a/tests/functional/jax-rs-multiple-apps/pom.xml b/tests/functional/jax-rs-multiple-apps/pom.xml index bc24bc916ed..96fcdd33226 100644 --- a/tests/functional/jax-rs-multiple-apps/pom.xml +++ b/tests/functional/jax-rs-multiple-apps/pom.xml @@ -34,7 +34,7 @@ helidon-microprofile - org.jboss + io.smallrye jandex runtime true diff --git a/tests/functional/param-converter-provider/pom.xml b/tests/functional/param-converter-provider/pom.xml index 3e009c97c4b..a2bc41d16d8 100644 --- a/tests/functional/param-converter-provider/pom.xml +++ b/tests/functional/param-converter-provider/pom.xml @@ -34,7 +34,7 @@ helidon-microprofile - org.jboss + io.smallrye jandex runtime true diff --git a/tests/functional/request-scope-cdi/pom.xml b/tests/functional/request-scope-cdi/pom.xml index 035854135ab..43536e7c0bd 100644 --- a/tests/functional/request-scope-cdi/pom.xml +++ b/tests/functional/request-scope-cdi/pom.xml @@ -34,7 +34,7 @@ helidon-microprofile - org.jboss + io.smallrye jandex runtime true diff --git a/tests/functional/request-scope/pom.xml b/tests/functional/request-scope/pom.xml index 771e42a21ec..65432086d49 100644 --- a/tests/functional/request-scope/pom.xml +++ b/tests/functional/request-scope/pom.xml @@ -34,7 +34,7 @@ helidon-microprofile - org.jboss + io.smallrye jandex runtime true diff --git a/tests/integration/dbclient/appl/pom.xml b/tests/integration/dbclient/appl/pom.xml index f10c1539e89..7abc068a349 100644 --- a/tests/integration/dbclient/appl/pom.xml +++ b/tests/integration/dbclient/appl/pom.xml @@ -123,7 +123,7 @@ slf4j-jdk14 - org.jboss + io.smallrye jandex runtime true @@ -152,7 +152,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin ${version.lib.jandex-maven-plugin} @@ -182,7 +182,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/tests/integration/jpa/appl/pom.xml b/tests/integration/jpa/appl/pom.xml index 0b73564150e..c475089c754 100644 --- a/tests/integration/jpa/appl/pom.xml +++ b/tests/integration/jpa/appl/pom.xml @@ -101,7 +101,7 @@ compile - org.jboss + io.smallrye jandex runtime @@ -157,7 +157,7 @@ maven-dependency-plugin - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/tests/integration/jpa/model/pom.xml b/tests/integration/jpa/model/pom.xml index 908f0fd6539..9e875fb3b04 100644 --- a/tests/integration/jpa/model/pom.xml +++ b/tests/integration/jpa/model/pom.xml @@ -42,7 +42,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/tests/integration/jpa/simple/pom.xml b/tests/integration/jpa/simple/pom.xml index dbe13f582e1..32029773233 100644 --- a/tests/integration/jpa/simple/pom.xml +++ b/tests/integration/jpa/simple/pom.xml @@ -109,7 +109,7 @@ test - org.jboss + io.smallrye jandex runtime @@ -128,7 +128,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/tests/integration/mp-bean-validation/pom.xml b/tests/integration/mp-bean-validation/pom.xml index 6c51bb1c4b5..5f85950222c 100644 --- a/tests/integration/mp-bean-validation/pom.xml +++ b/tests/integration/mp-bean-validation/pom.xml @@ -41,7 +41,7 @@ helidon-microprofile-bean-validation - org.jboss + io.smallrye jandex runtime true @@ -75,7 +75,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/tests/integration/mp-graphql/pom.xml b/tests/integration/mp-graphql/pom.xml index df7266eb40e..2dae07d6804 100644 --- a/tests/integration/mp-graphql/pom.xml +++ b/tests/integration/mp-graphql/pom.xml @@ -118,7 +118,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin @@ -127,7 +127,7 @@ jandex - integration-test + process-test-resources diff --git a/tests/integration/native-image/mp-1/pom.xml b/tests/integration/native-image/mp-1/pom.xml index 4b62d49fb8a..54e0fc67f42 100644 --- a/tests/integration/native-image/mp-1/pom.xml +++ b/tests/integration/native-image/mp-1/pom.xml @@ -67,7 +67,7 @@ runtime - org.jboss + io.smallrye jandex runtime @@ -90,7 +90,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/tests/integration/native-image/mp-2/pom.xml b/tests/integration/native-image/mp-2/pom.xml index f04838e7508..a2ad1dc569d 100644 --- a/tests/integration/native-image/mp-2/pom.xml +++ b/tests/integration/native-image/mp-2/pom.xml @@ -52,7 +52,7 @@ helidon-microprofile-config - org.jboss + io.smallrye jandex - - - 4.0.0 - - io.helidon.applications - helidon-se - 4.0.0-SNAPSHOT - ../../../../applications/se/pom.xml - - io.helidon.tests.integration - helidon-tests-native-image-se-1 - Helidon Integration Tests GraalVM Native image SE1 - - - This test makes sure the following helidon modules can be compiled into native image: WebServer JSON-P Classpath static - content File static content Tyrus(web sockets) Config (with change support) File watch Classpath YAML Tracing Zipkin - Health check Health checks Custom Metrics Vendor Base Custom Security Webserver integration ABAC (verified through roles) - HTTP Basic authentication HTTP Signatures - - - - io.helidon.tests.integration.nativeimage.se1.Se1Main - - - - - io.helidon.reactive.webserver - helidon-reactive-webserver - - - io.helidon.reactive.webserver - helidon-reactive-webserver-static-content - - - io.helidon.reactive.webserver - helidon-reactive-webserver-websocket - - - io.helidon.reactive.media - helidon-reactive-media-jsonp - - - io.helidon.reactive.media - helidon-reactive-media-jsonb - - - io.helidon.config - helidon-config-yaml - - - io.helidon.tracing - helidon-tracing - - - io.helidon.tracing - helidon-tracing-zipkin - - - io.helidon.reactive.health - helidon-reactive-health - - - io.helidon.health - helidon-health-checks - - - io.helidon.reactive.metrics - helidon-reactive-metrics - - - io.helidon.reactive.webclient - helidon-reactive-webclient - - - io.helidon.reactive.webclient - helidon-reactive-webclient-metrics - - - io.helidon.reactive.webclient - helidon-reactive-webclient-tracing - - - io.helidon.security.integration - helidon-security-integration-webserver - - - io.helidon.security.providers - helidon-security-providers-abac - - - io.helidon.security.providers - helidon-security-providers-http-auth - - - io.helidon.security.providers - helidon-security-providers-http-sign - - - io.helidon.tests.integration - helidon-tests-native-image-static-content - ${project.version} - - - org.junit.jupiter - junit-jupiter-api - test - - - org.hamcrest - hamcrest-all - test - - - - - - - org.graalvm.buildtools - native-maven-plugin - - true - - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-libs - - - - - - diff --git a/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/Animal.java b/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/Animal.java deleted file mode 100644 index 3099444816d..00000000000 --- a/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/Animal.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.tests.integration.nativeimage.se1; - -import io.helidon.common.Reflected; - -@Reflected -public class Animal { - private TYPE type; - private String name; - - public Animal() { - } - - public Animal(final TYPE type, final String name) { - this.type = type; - this.name = name; - } - - public TYPE getType() { - return type; - } - - public void setType(final TYPE type) { - this.type = type; - } - - public String getName() { - return name; - } - - public void setName(final String name) { - this.name = name; - } - - public enum TYPE { - BIRD, DOG, CAT - } -} diff --git a/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/ColorService.java b/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/ColorService.java deleted file mode 100644 index 7d42c13490c..00000000000 --- a/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/ColorService.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.tests.integration.nativeimage.se1; - -import io.helidon.config.Config; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.ServerRequest; -import io.helidon.reactive.webserver.ServerResponse; -import io.helidon.reactive.webserver.Service; - -/** - * Test service to exercise config enum mapping. - */ -public class ColorService implements Service { - - /** - * Enum used in config mapping. - */ - public enum Color {RED, YELLOW, BLUE} - - private final Color configuredColor; - - /** - * Creates a new instance of the service. - * - * @param config config tree - */ - public ColorService(Config config) { - // Attempt the mapping now to force a failure (if any) during server start-up when it's easily and quickly visible. - configuredColor = config.get("color.tint").as(Color.class).get(); - } - - @Override - public void update(Routing.Rules rules) { - rules.get("/", this::reportColor); - } - - private void reportColor(ServerRequest request, ServerResponse response) { - response.send(configuredColor.name()); - } -} diff --git a/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/GreetService.java b/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/GreetService.java deleted file mode 100644 index 57abd9f7d61..00000000000 --- a/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/GreetService.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.tests.integration.nativeimage.se1; - -import java.security.Principal; -import java.util.Collections; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; - -import io.helidon.common.http.Http; -import io.helidon.config.Config; -import io.helidon.metrics.RegistryFactory; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.ServerRequest; -import io.helidon.reactive.webserver.ServerResponse; -import io.helidon.reactive.webserver.Service; -import io.helidon.security.SecurityContext; - -import jakarta.json.Json; -import jakarta.json.JsonBuilderFactory; -import jakarta.json.JsonObject; -import org.eclipse.microprofile.metrics.MetricRegistry; -import org.eclipse.microprofile.metrics.Timer; - -/** - * A simple service to greet you. Examples: - * - * Get default greeting message: - * curl -X GET http://localhost:8080/greet - * - * Get greeting message for Joe: - * curl -X GET http://localhost:8080/greet/Joe - * - * Change greeting - * curl -X PUT -H "Content-Type: application/json" -d '{"greeting" : "Howdy"}' http://localhost:8080/greet/greeting - * - * The message is returned as a JSON object - */ - -public class GreetService implements Service { - - /** - * The config value for the key {@code greeting}. - */ - private final AtomicReference greeting = new AtomicReference<>(); - - private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); - private final Timer defaultMessageTimer; - - GreetService(Config config) { - Config greetingConfig = config.get("app.greeting"); - - // initial value - greeting.set(greetingConfig.asString().orElse("Ciao")); - - greetingConfig.onChange((Consumer) cfg -> greeting.set(cfg.asString().orElse("Ciao"))); - - RegistryFactory metricsRegistry = RegistryFactory.getInstance(); - MetricRegistry appRegistry = metricsRegistry.getRegistry(MetricRegistry.Type.APPLICATION); - this.defaultMessageTimer = appRegistry.timer("greet.default.timer"); - } - - /** - * A service registers itself by updating the routine rules. - * @param rules the routing rules. - */ - @Override - public void update(Routing.Rules rules) { - rules - .get("/", this::getDefaultMessageHandler) - // Outbound is commented out, as we want native image to work - //.get("/outbound", this::outbound) - .get("/{name}", this::getMessageHandler) - .put("/greeting", this::updateGreetingHandler); - - } - - /** - * Return a worldly greeting message. - * @param request the server request - * @param response the server response - */ - private void getDefaultMessageHandler(ServerRequest request, - ServerResponse response) { - Timer.Context timerContext = defaultMessageTimer.time(); - sendResponse(response, "World"); - response.whenSent() - .thenAccept(res -> timerContext.stop()); - } - - /** - * Return a greeting message using the name that was provided. - * @param request the server request - * @param response the server response - */ - private void getMessageHandler(ServerRequest request, - ServerResponse response) { - String name = request.path().param("name"); - - // if we run with security enabled, we want to return the user from security - name = request.context() - .get(SecurityContext.class) - .flatMap(SecurityContext::userPrincipal) - .map(Principal::getName) - .orElse(name); - - sendResponse(response, name); - } - - private void sendResponse(ServerResponse response, String name) { - String msg = String.format("%s %s!", greeting.get(), name); - - JsonObject returnObject = JSON.createObjectBuilder() - .add("message", msg) - .build(); - response.send(returnObject); - } - - private void updateGreetingFromJson(JsonObject jo, ServerResponse response) { - - if (!jo.containsKey("greeting")) { - JsonObject jsonErrorObject = JSON.createObjectBuilder() - .add("error", "No greeting provided") - .build(); - response.status(Http.Status.BAD_REQUEST_400) - .send(jsonErrorObject); - return; - } - - greeting.set(jo.getString("greeting")); - response.status(Http.Status.NO_CONTENT_204).send(); - } - - /** - * Set the greeting to use in future messages. - * @param request the server request - * @param response the server response - */ - private void updateGreetingHandler(ServerRequest request, - ServerResponse response) { - request.content().as(JsonObject.class).thenAccept(jo -> updateGreetingFromJson(jo, response)); - } - -} diff --git a/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/MockZipkinService.java b/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/MockZipkinService.java deleted file mode 100644 index 43c3aed753c..00000000000 --- a/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/MockZipkinService.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.tests.integration.nativeimage.se1; - -import java.io.ByteArrayInputStream; -import java.io.EOFException; -import java.io.IOException; -import java.io.StringReader; -import java.lang.System.Logger.Level; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.Flow; -import java.util.concurrent.atomic.AtomicReference; -import java.util.zip.GZIPInputStream; - -import io.helidon.common.GenericType; -import io.helidon.common.http.DataChunk; -import io.helidon.common.reactive.Multi; -import io.helidon.common.reactive.Single; -import io.helidon.reactive.media.common.MessageBodyReaderContext; -import io.helidon.reactive.media.common.MessageBodyStreamReader; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.ServerRequest; -import io.helidon.reactive.webserver.ServerResponse; -import io.helidon.reactive.webserver.Service; - -import jakarta.json.Json; -import jakarta.json.JsonPointer; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; - -public class MockZipkinService implements Service { - - private static final System.Logger LOGGER = System.getLogger(MockZipkinService.class.getName()); - - final static JsonPointer TAGS_POINTER = Json.createPointer("/tags"); - final static JsonPointer COMPONENT_POINTER = Json.createPointer("/tags/component"); - - private final Set filteredComponents; - private final AtomicReference> next = new AtomicReference<>(new CompletableFuture<>()); - - /** - * Create mock of the Zipkin listening on /api/v2/spans. - * - * @param filteredComponents listen only for traces with component tag having one of specified values - */ - MockZipkinService(Set filteredComponents) { - this.filteredComponents = filteredComponents; - } - - @Override - public void update(final Routing.Rules rules) { - rules.post("/api/v2/spans", this::mockZipkin); - } - - /** - * Return completion being completed when next trace call arrives. - * - * @return completion being completed when next trace call arrives - */ - CompletionStage next() { - return next.get(); - } - - private void mockZipkin(final ServerRequest request, final ServerResponse response) { - request.queryParams().all("serviceName", List::of).forEach(s -> System.out.println(">>>" + s)); - request.content() - .registerReader(new MessageBodyStreamReader() { - @Override - public PredicateResult accept(final GenericType type, final MessageBodyReaderContext context) { - return PredicateResult.COMPATIBLE; - } - - @Override - @SuppressWarnings("unchecked") - public Flow.Publisher read(final Flow.Publisher publisher, final GenericType type, final MessageBodyReaderContext context) { - return (Flow.Publisher) Multi.create(publisher) - .map(d -> ByteBuffer.wrap(d.bytes())) - .reduce((buf, buf2) -> - ByteBuffer.allocate(buf.capacity() + buf2.capacity()) - .put(buf.array()) - .put(buf2.array())) - .flatMap(b -> { - try (ByteArrayInputStream bais = new ByteArrayInputStream(b.array()); - GZIPInputStream gzipInputStream = new GZIPInputStream(bais)) { - return Single.just(Json.createReader(new StringReader(new String(gzipInputStream.readAllBytes()))) - .readArray()); - } catch (EOFException e) { - //ignore - return Multi.empty(); - } catch (IOException e) { - throw new RuntimeException(e); - } - }) - .flatMap(a -> Multi.create(a.stream())); - } - }) - .asStream(JsonValue.class) - .map(JsonValue::asJsonObject) - .filter(json -> - TAGS_POINTER.containsValue(json) - && COMPONENT_POINTER.containsValue(json) - && filteredComponents.stream() - .anyMatch(s -> s.equals(((JsonString) COMPONENT_POINTER.getValue(json)).getString())) - ) - .onError(Throwable::printStackTrace) - .onError(t -> response.status(500).send(t)) - .onComplete(response::send) - .peek(json -> LOGGER.log(Level.INFO, json.toString())) - .forEach(e -> next.getAndSet(new CompletableFuture<>()).complete(e)); - } -} diff --git a/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/Se1Main.java b/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/Se1Main.java deleted file mode 100644 index 9060ebb17c7..00000000000 --- a/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/Se1Main.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.helidon.tests.integration.nativeimage.se1; - -import java.nio.file.Paths; -import java.util.Set; - -import io.helidon.config.Config; -import io.helidon.config.FileSystemWatcher; -import io.helidon.health.checks.HealthChecks; -import io.helidon.logging.common.LogConfig; -import io.helidon.reactive.health.HealthSupport; -import io.helidon.reactive.media.jsonb.JsonbSupport; -import io.helidon.reactive.media.jsonp.JsonpSupport; -import io.helidon.reactive.metrics.MetricsSupport; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.WebServer; -import io.helidon.reactive.webserver.staticcontent.StaticContentSupport; -import io.helidon.reactive.webserver.websocket.WebSocketRouting; -import io.helidon.security.integration.webserver.WebSecurity; -import io.helidon.tracing.TracerBuilder; - -import jakarta.websocket.server.ServerEndpointConfig; -import org.eclipse.microprofile.health.HealthCheckResponse; - -import static io.helidon.config.ConfigSources.classpath; -import static io.helidon.config.ConfigSources.file; - -/** - * Main class of this integration test. - */ -public final class Se1Main { - /** - * Cannot be instantiated. - */ - private Se1Main() { - } - - /** - * Application main entry point. - * @param args command line arguments. - */ - public static void main(final String[] args) { - startServer(); - } - - /** - * Start the server. - * @return the created {@link io.helidon.reactive.webserver.WebServer} instance - */ - static WebServer startServer() { - // load logging configuration - LogConfig.configureRuntime(); - - // By default this will pick up application.yaml from the classpath - Config config = buildConfig(); - - // Get webserver config from the "server" section of application.yaml - WebServer server = WebServer.builder() - .routing(createRouting(config)) - .routing(WebSocketRouting.builder() - .endpoint("/ws", ServerEndpointConfig.Builder.create( - WebSocketEndpoint.class, "/messages") - .build()) - .build()) - .config(config.get("server")) - .tracer(TracerBuilder.create(config.get("tracing")).build()) - .addMediaSupport(JsonpSupport.create()) - .addMediaSupport(JsonbSupport.create()) - .printFeatureDetails(true) - .build(); - - // Try to start the server. If successful, print some info and arrange to - // print a message at shutdown. If unsuccessful, print the exception. - server.start() - .thenAccept(ws -> { - System.out.println( - "WEB server is up! http://localhost:" + ws.port() + "/greet"); - ws.whenShutdown().thenRun(() - -> System.out.println("WEB server is DOWN. Good bye!")); - }) - .exceptionally(t -> { - System.err.println("Startup failed: " + t.getMessage()); - t.printStackTrace(System.err); - return null; - }); - - // Server threads are not daemon. No need to block. Just react. - - return server; - } - - private static Config buildConfig() { - return Config.builder() - .sources( - classpath("se-test.yaml").optional(), - file("conf/se.yaml") - .changeWatcher(FileSystemWatcher.create()) - .optional(), - classpath("application.yaml")) - .build(); - } - - /** - * Creates new {@link io.helidon.reactive.webserver.Routing}. - * - * @return routing configured with JSON support, a health check, and a service - * @param config configuration of this server - */ - private static Routing createRouting(Config config) { - - MetricsSupport metrics = MetricsSupport.create(); - GreetService greetService = new GreetService(config); - ColorService colorService = new ColorService(config); - MockZipkinService zipkinService = new MockZipkinService(Set.of("helidon-reactive-webclient")); - WebClientService webClientService = new WebClientService(config, zipkinService); - HealthSupport health = HealthSupport.builder() - .add(HealthChecks.healthChecks()) // Adds a convenient set of checks - .addLiveness(() -> HealthCheckResponse.named("custom") // a custom health check - .up() - .withData("timestamp", System.currentTimeMillis()) - .build()) - .build(); - - return Routing.builder() - .register("/static/path", StaticContentSupport.create(Paths.get("web"))) - .register("/static/classpath", StaticContentSupport.create("web")) - .register("/static/jar", StaticContentSupport.create("web-jar")) - .register(WebSecurity.create(config.get("security"))) - .register(health) // Health at "/health" - .register(metrics) // Metrics at "/metrics" - .register("/greet", greetService) - .register("/color", colorService) - .register("/wc", webClientService) - .register("/zipkin", zipkinService) - .build(); - } - -} diff --git a/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/WebClientService.java b/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/WebClientService.java deleted file mode 100644 index ab5e10de086..00000000000 --- a/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/WebClientService.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.tests.integration.nativeimage.se1; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.lang.System.Logger.Level; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.TimeUnit; -import java.util.function.Predicate; - -import io.helidon.common.context.Context; -import io.helidon.common.http.Http; -import io.helidon.common.media.type.MediaTypes; -import io.helidon.common.reactive.Single; -import io.helidon.config.Config; -import io.helidon.reactive.media.jsonb.JsonbSupport; -import io.helidon.reactive.webclient.WebClient; -import io.helidon.reactive.webclient.WebClientException; -import io.helidon.reactive.webclient.WebClientResponse; -import io.helidon.reactive.webserver.Routing; -import io.helidon.reactive.webserver.ServerRequest; -import io.helidon.reactive.webserver.ServerResponse; -import io.helidon.reactive.webserver.Service; - -import jakarta.json.JsonValue; - -public class WebClientService implements Service { - - private static final System.Logger LOGGER = System.getLogger(WebClientService.class.getName()); - private final WebClient client; - private final MockZipkinService zipkinService; - private final String context; - - public WebClientService(Config config, MockZipkinService zipkinService) { - this.zipkinService = zipkinService; - this.context = "http://localhost:" + config.get("port").asInt().orElse(7076); - client = WebClient.builder() - .baseUri(context) - .addReader(JsonbSupport.reader()) - .addHeader(Http.HeaderValues.ACCEPT_JSON) - .config(config.get("client")) - .build(); - } - - @Override - public void update(final Routing.Rules rules) { - rules.get("/test", this::getTest) - .get("/redirect", this::redirect) - .get("/redirect/infinite", this::redirectInfinite) - .get("/endpoint", this::getEndpoint); - } - - private void redirect(ServerRequest request, - ServerResponse response) { - response.headers().add(Http.Header.LOCATION, context + "/wc/endpoint"); - response.status(Http.Status.MOVED_PERMANENTLY_301).send(); - } - - private void redirectInfinite(ServerRequest serverRequest, ServerResponse response) { - response.headers().add(Http.Header.LOCATION, context + "/wc/redirect/infinite"); - response.status(Http.Status.MOVED_PERMANENTLY_301).send(); - } - - private void getEndpoint(final ServerRequest request, final ServerResponse response) { - response.send(new Animal(Animal.TYPE.BIRD, "Frank")); - } - - private void getTest(final ServerRequest request, final ServerResponse response) { - CompletableFuture.runAsync(() -> { - - testTracedGet(request.context()); - testFollowRedirect(request.context()); - testFollowRedirectInfinite(request.context()); - - }).whenComplete((u, t) -> { - if (t == null) { - response.send("ALL TESTS PASSED!\n"); - } else { - response.status(Http.Status.INTERNAL_SERVER_ERROR_500); - StringWriter writer = new StringWriter(); - t.printStackTrace(new PrintWriter(writer)); - response.send("Failed to process request: " + writer); - } - }); - } - - public void testTracedGet(Context ctx) { - final CompletionStage nextTrace = zipkinService.next(); - client.get() - .path("/wc/endpoint") - .context(ctx) - .request(Animal.class) - .thenAccept(animal -> assertTrue(animal, a -> "Frank".equals(a.getName()))) - .await(15, TimeUnit.SECONDS); - //Wait for trace arrival to MockZipkin - Single.create(nextTrace).await(15, TimeUnit.SECONDS); - } - - public void testFollowRedirect(Context ctx) { - client.get() - .path("/wc/redirect") - .followRedirects(true) - .context(ctx) - .request(Animal.class) - .thenAccept(animal -> assertTrue(animal, a -> "Frank".equals(a.getName()))) - .await(15, TimeUnit.SECONDS); - - WebClientResponse response = client.get() - .path("/wc/redirect") - .followRedirects(false) - .context(ctx) - .request() - .await(15, TimeUnit.SECONDS); - assertEquals(response.status(), Http.Status.MOVED_PERMANENTLY_301); - } - - public void testFollowRedirectInfinite(Context ctx) { - try { - client.get() - .path("/wc/redirect/infinite") - .context(ctx) - .request(Animal.class) - .thenAccept(a -> fail("This should have failed!")) - .await(15, TimeUnit.SECONDS); - fail("This should have failed!"); - } catch (Exception e) { - if (e.getCause() instanceof WebClientException) { - WebClientException clientException = (WebClientException) e.getCause(); - assertTrue(clientException.getMessage(), m -> m.startsWith("Max number of redirects extended! (5)")); - } else { - fail(e); - } - } - } - - private void assertTrue(T value, Predicate predicate) { - if (!predicate.test(value)) { - fail("for value: " + value); - } - } - - private void assertEquals(Object a, Object b) { - if (!Objects.equals(a, b)) { - fail("Expected " + a + " equals " + b); - } - } - - private void fail(String msg) { - fail(new RuntimeException("Assertion error " + msg)); - } - - private void fail(Exception e) { - LOGGER.log(Level.ERROR, e.getMessage()); - throw new RuntimeException("Assertion error!", e); - } -} diff --git a/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/WebSocketEndpoint.java b/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/WebSocketEndpoint.java deleted file mode 100644 index 82a6376099c..00000000000 --- a/tests/integration/native-image/se-1/src/main/java/io/helidon/tests/integration/nativeimage/se1/WebSocketEndpoint.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.tests.integration.nativeimage.se1; - -import java.io.IOException; -import java.lang.System.Logger.Level; - -import jakarta.websocket.Endpoint; -import jakarta.websocket.EndpointConfig; -import jakarta.websocket.MessageHandler; -import jakarta.websocket.Session; - - -public class WebSocketEndpoint extends Endpoint { - - private static final System.Logger LOGGER = System.getLogger(WebSocketEndpoint.class.getName()); - - @Override - public void onOpen(Session session, EndpointConfig endpointConfig) { - - StringBuilder sb = new StringBuilder(); - - LOGGER.log(Level.INFO, "Session " + session.getId()); - session.addMessageHandler(new MessageHandler.Whole() { - @Override - public void onMessage(String message) { - LOGGER.log(Level.INFO, "WS Receiving " + message); - if (message.contains("SEND")) { - sendTextMessage(session, sb.toString()); - sb.setLength(0); - } else { - sb.append(message); - } - } - }); - } - - private void sendTextMessage(Session session, String msg) { - try { - session.getBasicRemote().sendText(msg); - } catch (IOException e) { - LOGGER.log(Level.ERROR, "Message sending failed", e); - } - } -} diff --git a/tests/integration/native-image/se-1/src/main/resources/logging.properties b/tests/integration/native-image/se-1/src/main/resources/logging.properties deleted file mode 100644 index 36c82ef445e..00000000000 --- a/tests/integration/native-image/se-1/src/main/resources/logging.properties +++ /dev/null @@ -1,37 +0,0 @@ -# -# Copyright (c) 2018, 2022 Oracle and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# Example Logging Configuration File -# For more information see $JAVA_HOME/jre/lib/logging.properties - -# Send messages to the console -handlers=io.helidon.logging.jul.HelidonConsoleHandler - -# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread -java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n - -# Global logging level. Can be overridden by specific loggers -.level=INFO - -# Component specific log levels -#io.helidon.reactive.webserver.level=INFO -#io.helidon.config.level=INFO -#io.helidon.security.level=INFO -#io.helidon.common.level=INFO -#io.netty.level=INFO - -io.helidon.reactive.webserver.ClassPathContentHandler.level=FINEST -io.helidon.reactive.webserver.FileSystemContentHandler.level=FINEST diff --git a/tests/integration/native-image/se-1/src/main/resources/web/resource.txt b/tests/integration/native-image/se-1/src/main/resources/web/resource.txt deleted file mode 100644 index bf0db6fd62a..00000000000 --- a/tests/integration/native-image/se-1/src/main/resources/web/resource.txt +++ /dev/null @@ -1 +0,0 @@ -classpath-resource-text \ No newline at end of file diff --git a/tests/integration/native-image/se-1/src/test/java/io/helidon/tests/integration/nativeimage/se1/Se1MainTest.java b/tests/integration/native-image/se-1/src/test/java/io/helidon/tests/integration/nativeimage/se1/Se1MainTest.java deleted file mode 100644 index 2aee8ae1092..00000000000 --- a/tests/integration/native-image/se-1/src/test/java/io/helidon/tests/integration/nativeimage/se1/Se1MainTest.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.tests.integration.nativeimage.se1; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.Collections; -import java.util.concurrent.TimeUnit; - -import io.helidon.reactive.webserver.WebServer; - -import jakarta.json.Json; -import jakarta.json.JsonObject; -import jakarta.json.JsonReader; -import jakarta.json.JsonReaderFactory; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - - -/** - * Unit test for {@link Se1Main}. - */ -class Se1MainTest { - private static WebServer webServer; - private static final JsonReaderFactory JSON = Json.createReaderFactory(Collections.emptyMap()); - - @BeforeAll - public static void startTheServer() throws Exception { - webServer = Se1Main.startServer(); - - long timeout = 2000; // 2 seconds should be enough to start the server - long now = System.currentTimeMillis(); - - while (!webServer.isRunning()) { - Thread.sleep(100); - if ((System.currentTimeMillis() - now) > timeout) { - Assertions.fail("Failed to start webserver"); - } - } - } - - @AfterAll - public static void stopServer() throws Exception { - if (webServer != null) { - webServer.shutdown() - .toCompletableFuture() - .get(10, TimeUnit.SECONDS); - } - } - - @Test - public void testHelloWorld() throws Exception { - HttpURLConnection conn; - - conn = getURLConnection("GET","/greet"); - assertThat("HTTP response1", conn.getResponseCode(), is(200)); - JsonReader jsonReader = JSON.createReader(conn.getInputStream()); - JsonObject jsonObject = jsonReader.readObject(); - assertThat("default message", jsonObject.getString("message"), - is("Hello World!")); - - conn = getURLConnection("GET", "/greet/Joe"); - assertThat("HTTP response2 - not authenticated", conn.getResponseCode(), is(200)); - jsonReader = JSON.createReader(conn.getInputStream()); - jsonObject = jsonReader.readObject(); - assertThat("hello Joe message", jsonObject.getString("message"), - is("Hello Joe!")); - - conn = getURLConnection("PUT", "/greet/greeting"); - conn.setRequestProperty("Content-Type", "application/json"); - conn.setDoOutput(true); - OutputStream os = conn.getOutputStream(); - os.write("{\"greeting\" : \"Hola\"}".getBytes()); - os.close(); - assertThat("HTTP response3", conn.getResponseCode(), is(204)); - - conn = getURLConnection("GET", "/greet/Jose"); - assertThat("HTTP response4", conn.getResponseCode(), is(200)); - jsonReader = JSON.createReader(conn.getInputStream()); - jsonObject = jsonReader.readObject(); - assertThat("hola Jose message", jsonObject.getString("message"), - is("Hola Jose!")); - - conn = getURLConnection("GET", "/health"); - assertThat("HTTP response2", conn.getResponseCode(), is(200)); - - conn = getURLConnection("GET", "/metrics"); - assertThat("HTTP response2", conn.getResponseCode(), is(200)); - } - - @Test - void testEnumMapping() throws Exception { - HttpURLConnection conn = getURLConnection("GET", "/color"); - int status = conn.getResponseCode(); - ColorService.Color tint; - try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { - String colorName = reader.readLine(); // Makes sure the color name was sent. - tint = ColorService.Color.valueOf(colorName); // Makes sure the color name maps to a Color. - } - assertThat("/color GET status", status, is(200)); - assertThat("reported tint", tint, is(ColorService.Color.RED)); // Makes sure the mapped color is RED. - } - - private HttpURLConnection getURLConnection(String method, String path) throws Exception { - URL url = new URL("http://localhost:" + webServer.port() + path); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod(method); - conn.setRequestProperty("Accept", "application/json"); - System.out.println("Connecting: " + method + " " + url); - return conn; - } -} diff --git a/tests/integration/native-image/se-1/src/test/resources/se-test.yaml b/tests/integration/native-image/se-1/src/test/resources/se-test.yaml deleted file mode 100644 index 22ce20c035f..00000000000 --- a/tests/integration/native-image/se-1/src/test/resources/se-test.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# -# Copyright (c) 2018, 2021 Oracle and/or its affiliates. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -app.greeting: "Hello" - -server: - port: -1 - host: localhost - -tracing: - service: "se-unit-test" - -security: - providers: - web-server: - paths: - - path: "/greet/{*}" - authenticate: false - roles-allowed: [] - - path: "/outbound" - authenticate: false diff --git a/tests/integration/native-image/se-1/web/resource.txt b/tests/integration/native-image/se-1/web/resource.txt deleted file mode 100644 index 776303ad9af..00000000000 --- a/tests/integration/native-image/se-1/web/resource.txt +++ /dev/null @@ -1 +0,0 @@ -file-resource-text \ No newline at end of file diff --git a/tests/integration/oidc/pom.xml b/tests/integration/oidc/pom.xml index e51bb930627..9a11f991353 100644 --- a/tests/integration/oidc/pom.xml +++ b/tests/integration/oidc/pom.xml @@ -42,7 +42,7 @@ jersey-client - org.jboss + io.smallrye jandex runtime true diff --git a/tests/integration/pom.xml b/tests/integration/pom.xml index 2fba593105b..6eefa5ffacd 100644 --- a/tests/integration/pom.xml +++ b/tests/integration/pom.xml @@ -71,8 +71,9 @@ native-image vault oidc - gh-5792 - gh-5792-nima + + + diff --git a/tests/integration/restclient/pom.xml b/tests/integration/restclient/pom.xml index 6d66ab35a10..a8a7b6d5c26 100644 --- a/tests/integration/restclient/pom.xml +++ b/tests/integration/restclient/pom.xml @@ -38,7 +38,7 @@ helidon-microprofile-rest-client - org.jboss + io.smallrye jandex runtime true diff --git a/tests/integration/security/gh1487/pom.xml b/tests/integration/security/gh1487/pom.xml index 90fa730c3d4..c25bf8fd75f 100644 --- a/tests/integration/security/gh1487/pom.xml +++ b/tests/integration/security/gh1487/pom.xml @@ -47,7 +47,7 @@ runtime - org.jboss + io.smallrye jandex runtime true diff --git a/tests/integration/security/security-response-mapper/pom.xml b/tests/integration/security/security-response-mapper/pom.xml index 10b967ad82c..cf7e19d1d48 100644 --- a/tests/integration/security/security-response-mapper/pom.xml +++ b/tests/integration/security/security-response-mapper/pom.xml @@ -38,7 +38,7 @@ helidon-microprofile - org.jboss + io.smallrye jandex runtime true diff --git a/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/RestrictedProvider.java b/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/RestrictedProvider.java index 1508c442ea2..641205b9aca 100644 --- a/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/RestrictedProvider.java +++ b/tests/integration/security/security-response-mapper/src/main/java/io/helidon/tests/integration/security/mapper/RestrictedProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,9 +24,9 @@ import io.helidon.security.AuthenticationResponse; import io.helidon.security.ProviderRequest; import io.helidon.security.spi.AuthenticationProvider; -import io.helidon.security.spi.SynchronousProvider; +import io.helidon.security.spi.SecurityProvider; -public class RestrictedProvider extends SynchronousProvider implements AuthenticationProvider { +public class RestrictedProvider implements AuthenticationProvider { /** * Register an entry in {@link io.helidon.common.context.Context} and fail authentication. @@ -35,7 +35,7 @@ public class RestrictedProvider extends SynchronousProvider implements Authentic * @return authentication response */ @Override - protected AuthenticationResponse syncAuthenticate(ProviderRequest providerRequest) { + public AuthenticationResponse authenticate(ProviderRequest providerRequest) { // Use context to communicate with MySecurityResponseMapper Contexts.context() .ifPresent(c -> c.register(RestrictedProvider.class, getClass().getSimpleName())); diff --git a/tests/integration/tools/example/pom.xml b/tests/integration/tools/example/pom.xml index dfda0096fc1..ae1b965e799 100644 --- a/tests/integration/tools/example/pom.xml +++ b/tests/integration/tools/example/pom.xml @@ -38,7 +38,7 @@ Helidon Integration Tests Tools: Test Example - 1.0.6 + 3.1.2 io.helidon.tests.integration.tools.example.ServerMain @@ -115,7 +115,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin ${version.lib.jandex-maven-plugin} @@ -145,7 +145,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/tests/integration/zipkin-mp-2.2/pom.xml b/tests/integration/zipkin-mp-2.2/pom.xml index ce157061965..0811691a9f9 100644 --- a/tests/integration/zipkin-mp-2.2/pom.xml +++ b/tests/integration/zipkin-mp-2.2/pom.xml @@ -43,7 +43,7 @@ helidon-microprofile-tracing - org.jboss + io.smallrye jandex runtime true @@ -72,7 +72,7 @@ - org.jboss.jandex + io.smallrye jandex-maven-plugin diff --git a/tracing/jersey-client/pom.xml b/tracing/jersey-client/pom.xml index 87dc275660e..46154c9cdae 100644 --- a/tracing/jersey-client/pom.xml +++ b/tracing/jersey-client/pom.xml @@ -53,10 +53,6 @@ io.helidon.common helidon-common-context - - io.helidon.reactive.webclient - helidon-reactive-webclient-jaxrs - io.helidon.common.features helidon-common-features-api diff --git a/tracing/jersey-client/src/main/java/module-info.java b/tracing/jersey-client/src/main/java/module-info.java index 0cddd1ac598..03752447f2a 100644 --- a/tracing/jersey-client/src/main/java/module-info.java +++ b/tracing/jersey-client/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2022 Oracle and/or its affiliates. + * Copyright (c) 2017, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,6 @@ requires io.helidon.tracing.config; requires io.helidon.common; requires io.helidon.common.context; - requires io.helidon.reactive.webclient.jaxrs; exports io.helidon.tracing.jersey.client; diff --git a/tracing/opentelemetry/pom.xml b/tracing/opentelemetry/pom.xml index 09eebf6894f..e3f8a84bc27 100644 --- a/tracing/opentelemetry/pom.xml +++ b/tracing/opentelemetry/pom.xml @@ -58,6 +58,7 @@ provided true + org.junit.jupiter junit-jupiter-api @@ -68,15 +69,5 @@ hamcrest-all test - - io.helidon.microprofile.tests - helidon-microprofile-tests-junit5 - test - - - io.helidon.microprofile.server - helidon-microprofile-server - test - diff --git a/tracing/opentelemetry/src/main/java/io/helidon/tracing/opentelemetry/HelidonOpenTelemetry.java b/tracing/opentelemetry/src/main/java/io/helidon/tracing/opentelemetry/HelidonOpenTelemetry.java index 3faff6c0e8d..490445f86ed 100644 --- a/tracing/opentelemetry/src/main/java/io/helidon/tracing/opentelemetry/HelidonOpenTelemetry.java +++ b/tracing/opentelemetry/src/main/java/io/helidon/tracing/opentelemetry/HelidonOpenTelemetry.java @@ -33,8 +33,8 @@ public final class HelidonOpenTelemetry { private static final System.Logger LOGGER = System.getLogger(HelidonOpenTelemetry.class.getName()); - static final String OTEL_AGENT_PRESENT_PROPERTY = "otel.agent.present"; - static final String IO_OPENTELEMETRY_JAVAAGENT = "io.opentelemetry.javaagent"; + private static final String OTEL_AGENT_PRESENT_PROPERTY = "otel.agent.present"; + private static final String IO_OPENTELEMETRY_JAVAAGENT = "io.opentelemetry.javaagent"; private HelidonOpenTelemetry() { } /** diff --git a/tracing/opentelemetry/src/test/java/io/helidon/tracing/opentelemetry/AgentDetectorTest.java b/tracing/opentelemetry/src/test/java/io/helidon/tracing/opentelemetry/AgentDetectorTest.java index bf673ef91b6..7769947fc91 100644 --- a/tracing/opentelemetry/src/test/java/io/helidon/tracing/opentelemetry/AgentDetectorTest.java +++ b/tracing/opentelemetry/src/test/java/io/helidon/tracing/opentelemetry/AgentDetectorTest.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.helidon.tracing.opentelemetry; +import java.util.Map; + import io.helidon.config.Config; -import io.helidon.microprofile.server.ServerCdiExtension; -import io.helidon.microprofile.tests.junit5.AddConfig; -import io.helidon.microprofile.tests.junit5.AddExtension; -import io.helidon.microprofile.tests.junit5.HelidonTest; -import jakarta.enterprise.inject.spi.CDI; +import io.helidon.config.ConfigSources; + import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -30,30 +30,29 @@ /** * Check Agent Detector working correctly. */ -@HelidonTest(resetPerTest = true) -@AddExtension(ServerCdiExtension.class) class AgentDetectorTest { + public static final String OTEL_AGENT_PRESENT = "otel.agent.present"; + public static final String IO_OPENTELEMETRY_JAVAAGENT = "io.opentelemetry.javaagent"; + @Test - @AddConfig(key = HelidonOpenTelemetry.OTEL_AGENT_PRESENT_PROPERTY, value = "true") void shouldBeNoOpTelemetry(){ - Config config = CDI.current().select(Config.class).get(); + Config config = Config.create(ConfigSources.create(Map.of(OTEL_AGENT_PRESENT, "true"))); boolean present = HelidonOpenTelemetry.AgentDetector.isAgentPresent(config); assertThat(present, is(true)); } @Test - @AddConfig(key = HelidonOpenTelemetry.OTEL_AGENT_PRESENT_PROPERTY, value = "false") void shouldNotBeNoOpTelemetry(){ - Config config = CDI.current().select(Config.class).get(); + Config config = Config.create(ConfigSources.create(Map.of(OTEL_AGENT_PRESENT, "false"))); boolean present = HelidonOpenTelemetry.AgentDetector.isAgentPresent(config); assertThat(present, is(false)); } @Test void checkEnvVariable(){ - System.setProperty(HelidonOpenTelemetry.IO_OPENTELEMETRY_JAVAAGENT, "true"); - Config config = CDI.current().select(Config.class).get(); + System.setProperty(IO_OPENTELEMETRY_JAVAAGENT, "true"); + Config config = Config.create(); boolean present = HelidonOpenTelemetry.AgentDetector.isAgentPresent(config); assertThat(present, is(true)); }