diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 565307fd9b7..85d47032d56 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -115,3 +115,47 @@ jobs: with: name: io-helidon-artifacts-${{ needs.create-tag.outputs.version }} path: staging + resolve-all: + needs: [ create-tag, deploy ] + timeout-minutes: 30 + runs-on: ubuntu-20.04 + name: resolve-all + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: '0' + ref: ${{ needs.create-tag.outputs.tag }} + - uses: ./.github/actions/common + with: + run: | + mvn ${MAVEN_ARGS} -N \ + -Possrh-staging \ + -Dartifact=io.helidon:helidon-all:${{ needs.create-tag.outputs.version }}:pom \ + dependency:get + smoketest: + needs: [ create-tag, deploy ] + timeout-minutes: 30 + strategy: + matrix: + archetype: + - bare-se + - bare-mp + - quickstart-se + - quickstart-mp + - database-se + - database-mp + runs-on: ubuntu-20.04 + name: smoketest/${{ matrix.archetype }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: '0' + ref: ${{ needs.create-tag.outputs.tag }} + - uses: ./.github/actions/common + with: + run: | + ./etc/scripts/smoketest.sh \ + --clean \ + --staged \ + --version=${{ needs.create-tag.outputs.version }} \ + --archetype=${{ matrix.archetype }} diff --git a/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java b/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java index a7ad18404ff..2336ba4426e 100644 --- a/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java +++ b/builder/codegen/src/main/java/io/helidon/builder/codegen/BuilderCodegen.java @@ -255,7 +255,7 @@ private void process(RoundContext roundContext, TypeInfo blueprint) { roundContext.addGeneratedType(prototype, classModel, blueprint.typeName(), - blueprint.originatingElement().orElse(blueprint.typeName())); + blueprint.originatingElementValue()); if (typeContext.typeInfo().supportsServiceRegistry() && typeContext.propertyData().hasProvider()) { for (PrototypeProperty property : typeContext.propertyData().properties()) { diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/Annotated.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/Annotated.java index 07f13ac5c29..3aab5620bdf 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/Annotated.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/Annotated.java @@ -43,6 +43,8 @@ public interface Annotated { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @return list of all meta annotations of this element */ @@ -85,11 +87,10 @@ default Optional findAnnotation(TypeName annotationType) { * @see #findAnnotation(TypeName) */ default Annotation annotation(TypeName annotationType) { - return findAnnotation(annotationType).orElseThrow(() -> new NoSuchElementException("Annotation " + annotationType + " " - + "is not present. Guard " - + "with hasAnnotation(), or " - + "use findAnnotation() " - + "instead")); + return findAnnotation(annotationType) + .orElseThrow(() -> new NoSuchElementException("Annotation " + annotationType + " is not present. " + + "Guard with hasAnnotation(), " + + "or use findAnnotation() instead")); } /** diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java index 3017fdd6526..b51fb659b6d 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java @@ -79,6 +79,15 @@ interface AnnotationBlueprint { @Option.Singular Map values(); + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @return list of all annotations declared on the annotation type, or inherited from them + */ + @Option.Redundant + @Option.Singular + List metaAnnotations(); + /** * The value property. * @@ -653,4 +662,19 @@ default > Optional> enumValues(String property, Class< return AnnotationSupport.asEnums(typeName(), values(), property, type); } + /** + * Check if {@link io.helidon.common.types.Annotation#metaAnnotations()} contains an annotation of the provided type. + *

+ * Note: we ignore {@link java.lang.annotation.Target}, {@link java.lang.annotation.Inherited}, + * {@link java.lang.annotation.Documented}, and {@link java.lang.annotation.Retention}. + * + * @param annotationType type of annotation + * @return {@code true} if the annotation is declared on this annotation, or is inherited from a declared annotation + */ + default boolean hasMetaAnnotation(TypeName annotationType) { + return metaAnnotations() + .stream() + .map(Annotation::typeName) + .anyMatch(annotationType::equals); + } } diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationSupport.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationSupport.java index 52501d9d95e..b21a8a1a917 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationSupport.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/AnnotationSupport.java @@ -532,6 +532,14 @@ private static String asString(TypeName typeName, String property, Object value) return str; } + if (value instanceof TypeName tn) { + return tn.fqName(); + } + + if (value instanceof EnumValue ev) { + return ev.name(); + } + if (value instanceof List) { throw new IllegalArgumentException(typeName.fqName() + " property " + property + " is a list, cannot be converted to String"); @@ -619,22 +627,30 @@ private static Class asClass(TypeName typeName, String property, Object value if (value instanceof Class theClass) { return theClass; } - if (value instanceof String str) { - try { - return Class.forName(str); - } catch (ClassNotFoundException e) { + + String className = switch (value) { + case TypeName tn -> tn.name(); + case String str -> str; + default -> { throw new IllegalArgumentException(typeName.fqName() + " property " + property - + " of type String and value \"" + str + "\"" + + " of type " + value.getClass().getName() + " cannot be converted to Class"); } - } + }; - throw new IllegalArgumentException(typeName.fqName() + " property " + property - + " of type " + value.getClass().getName() - + " cannot be converted to Class"); + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException(typeName.fqName() + " property " + property + + " of type String and value \"" + className + "\"" + + " cannot be converted to Class"); + } } private static TypeName asTypeName(TypeName typeName, String property, Object value) { + if (value instanceof TypeName tn) { + return tn; + } if (value instanceof Class theClass) { return TypeName.create(theClass); } diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignature.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignature.java new file mode 100644 index 00000000000..a167313516d --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignature.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 Oracle and/or 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.types; + +import java.util.List; + +/** + * Signature of a {@link io.helidon.common.types.TypedElementInfo}. + *

+ * The {@link io.helidon.common.types.TypedElementInfo#signature()} is intended to compare + * fields, methods, and constructors across type hierarchy - for example when looking for a method + * that we override. + *

+ * The following information is used for equals and hash-code: + *

+ * + * The signature has well-defined {@code hashCode} and {@code equals} methods, + * so it can be safely used as a key in a {@link java.util.Map}. + *

+ * This interface is sealed, an instance can only be obtained + * from {@link io.helidon.common.types.TypedElementInfo#signature()}. + * + * @see #text() + */ +public sealed interface ElementSignature permits ElementSignatures.FieldSignature, + ElementSignatures.MethodSignature, + ElementSignatures.ParameterSignature, + ElementSignatures.NoSignature { + /** + * Type of the element. Resolves as follows: + *

+ * + * @return type of this element, never used for equals or hashCode + */ + TypeName type(); + + /** + * Name of the element. For constructor, this always returns {@code }, + * for parameters, this method may return the real parameter name or an index + * parameter name depending on the source of the information (during annotation processing, + * this would be the actual parameter name, when classpath scanning, this would be something like + * {@code param0}. + * + * @return name of this element + */ + String name(); + + /** + * Types of parameters if this represents a method or a constructor, + * empty {@link java.util.List} otherwise. + * + * @return parameter types + */ + List parameterTypes(); + + /** + * A text representation of this signature. + * + * + * + * @return text representation + */ + String text(); +} diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignatures.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignatures.java new file mode 100644 index 00000000000..4f5be563f29 --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/ElementSignatures.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2024 Oracle and/or 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.types; + +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +final class ElementSignatures { + private ElementSignatures() { + } + + static ElementSignature createNone() { + return new NoSignature(); + } + + static ElementSignature createField(TypeName type, + String name) { + Objects.requireNonNull(type); + Objects.requireNonNull(name); + return new FieldSignature(type, name); + } + + static ElementSignature createConstructor(List parameters) { + Objects.requireNonNull(parameters); + return new MethodSignature(TypeNames.PRIMITIVE_VOID, + "", + parameters); + } + + static ElementSignature createMethod(TypeName returnType, String name, List parameters) { + Objects.requireNonNull(returnType); + Objects.requireNonNull(name); + Objects.requireNonNull(parameters); + return new MethodSignature(returnType, + name, + parameters); + } + + static ElementSignature createParameter(TypeName type, String name) { + Objects.requireNonNull(type); + Objects.requireNonNull(name); + return new ParameterSignature(type, name); + } + + static final class FieldSignature implements ElementSignature { + private final TypeName type; + private final String name; + + private FieldSignature(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return List.of(); + } + + @Override + public String text() { + return name; + } + + @Override + public String toString() { + return type.resolvedName() + " " + name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FieldSignature that)) { + return false; + } + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + } + + static final class MethodSignature implements ElementSignature { + private final TypeName type; + private final String name; + private final List parameters; + private final String text; + private final boolean constructor; + + private MethodSignature(TypeName type, + String name, + List parameters) { + this.type = type; + this.name = name; + this.parameters = parameters; + if (name.equals("")) { + this.constructor = true; + this.text = parameterTypesSection(parameters, ",", TypeName::fqName); + } else { + this.constructor = false; + this.text = name + parameterTypesSection(parameters, ",", TypeName::fqName); + } + + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return parameters; + } + + @Override + public String text() { + return text; + } + + @Override + public String toString() { + if (constructor) { + return text; + } else { + return type.resolvedName() + " " + name + parameterTypesSection(parameters, + ", ", + TypeName::resolvedName); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MethodSignature that)) { + return false; + } + return Objects.equals(name, that.name) && Objects.equals(parameters, that.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(name, parameters); + } + } + + static final class ParameterSignature implements ElementSignature { + private final TypeName type; + private final String name; + + private ParameterSignature(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return List.of(); + } + + @Override + public String text() { + return name; + } + + @Override + public String toString() { + return type.resolvedName() + " " + name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ParameterSignature that)) { + return false; + } + return name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + } + + static final class NoSignature implements ElementSignature { + @Override + public TypeName type() { + return TypeNames.PRIMITIVE_VOID; + } + + @Override + public String name() { + return ""; + } + + @Override + public String text() { + return ""; + } + + @Override + public String toString() { + return text(); + } + + @Override + public List parameterTypes() { + return List.of(); + } + } + + private static String parameterTypesSection(List parameters, + String delimiter, + Function typeMapper) { + return parameters.stream() + .map(typeMapper) + .collect(Collectors.joining(delimiter, "(", ")")); + } +} diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValue.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValue.java index 34fa57b6990..3a405d354f2 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValue.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValue.java @@ -16,6 +16,8 @@ package io.helidon.common.types; +import java.util.Objects; + /** * When creating an {@link io.helidon.common.types.Annotation}, we may need to create an enum value * without access to the enumeration. @@ -31,17 +33,24 @@ public interface EnumValue { * @return enum value */ static EnumValue create(TypeName enumType, String enumName) { - return new EnumValue() { - @Override - public TypeName type() { - return enumType; - } + Objects.requireNonNull(enumType); + Objects.requireNonNull(enumName); + return new EnumValueImpl(enumType, enumName); + } + + /** + * Create a new enum value. + * + * @param type enum type + * @param value enum value constant + * @return new enum value + * @param type of the enum + */ + static > EnumValue create(Class type, T value) { + Objects.requireNonNull(type); + Objects.requireNonNull(value); - @Override - public String name() { - return enumName; - } - }; + return new EnumValueImpl(TypeName.create(type), value.name()); } /** diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValueImpl.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValueImpl.java new file mode 100644 index 00000000000..f2205b2452e --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/EnumValueImpl.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 Oracle and/or 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.types; + +import java.util.Objects; + +final class EnumValueImpl implements EnumValue { + private final TypeName type; + private final String name; + + EnumValueImpl(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof EnumValue enumValue)) { + return false; + } + return Objects.equals(type, enumValue.type()) && Objects.equals(name, enumValue.name()); + } + + @Override + public int hashCode() { + return Objects.hash(type, name); + } + + @Override + public String toString() { + return type.fqName() + "." + name; + } +} diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/Modifier.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/Modifier.java index 87a1e75e4db..7d36ffc7ece 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/Modifier.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/Modifier.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,23 @@ public enum Modifier { /** * The {@code final} modifier. */ - FINAL("final"); + FINAL("final"), + /** + * The {@code transient} modifier. + */ + TRANSIENT("transient"), + /** + * The {@code volatile} modifier. + */ + VOLATILE("volatile"), + /** + * The {@code synchronized} modifier. + */ + SYNCHRONIZED("synchronized"), + /** + * The {@code native} modifier. + */ + NATIVE("native"); private final String modifierName; diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java index 4bb3da96300..704c34d139c 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java @@ -16,10 +16,13 @@ package io.helidon.common.types; +import java.util.ArrayDeque; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Queue; import java.util.Set; import io.helidon.builder.api.Option; @@ -228,6 +231,54 @@ default Optional metaAnnotation(TypeName annotation, TypeName metaAn @Option.Redundant Optional originatingElement(); + /** + * The element used to create this instance, or {@link io.helidon.common.types.TypeInfo#typeName()} if none provided. + * The type of the object depends on the environment we are in - it may be an {@code TypeElement} in annotation processing, + * or a {@code ClassInfo} when using classpath scanning. + * + * @return originating element, or the type of this type info + */ + default Object originatingElementValue() { + return originatingElement().orElseGet(this::typeName); + } + + /** + * Checks if the current type implements, or extends the provided type. + * This method analyzes the whole dependency tree of the current type. + * + * @param typeName type of interface to check + * @return the super type info, or interface type info matching the provided type, with appropriate generic declarations + */ + default Optional findInHierarchy(TypeName typeName) { + if (typeName.equals(typeName())) { + return Optional.of((TypeInfo) this); + } + // scan super types + Optional superClass = superTypeInfo(); + if (superClass.isPresent() && !superClass.get().typeName().equals(TypeNames.OBJECT)) { + var superType = superClass.get(); + var foundInSuper = superType.findInHierarchy(typeName); + if (foundInSuper.isPresent()) { + return foundInSuper; + } + } + // nope, let's try interfaces + Queue interfaces = new ArrayDeque<>(interfaceTypeInfo()); + Set processed = new HashSet<>(); + + while (!interfaces.isEmpty()) { + TypeInfo type = interfaces.remove(); + // make sure we process each type only once + if (processed.add(type.typeName())) { + if (typeName.equals(type.typeName())) { + return Optional.of(type); + } + interfaces.addAll(type.interfaceTypeInfo()); + } + } + return Optional.empty(); + } + /** * Uses {@link io.helidon.common.types.TypeInfo#referencedModuleNames()} to determine if the module name is known for the * given type. diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java index 62fe59da5a3..f539e5849af 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypeNames.java @@ -16,8 +16,10 @@ package io.helidon.common.types; +import java.lang.annotation.Documented; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; +import java.lang.annotation.Target; import java.time.Duration; import java.util.Collection; import java.util.List; @@ -73,10 +75,18 @@ public final class TypeNames { * Type name for {@link java.lang.annotation.Retention}. */ public static final TypeName RETENTION = TypeName.create(Retention.class); + /** + * Type name for {@link java.lang.annotation.Documented}. + */ + public static final TypeName DOCUMENTED = TypeName.create(Documented.class); /** * Type name for {@link java.lang.annotation.Inherited}. */ public static final TypeName INHERITED = TypeName.create(Inherited.class); + /** + * Type name for {@link java.lang.annotation.Target}. + */ + public static final TypeName TARGET = TypeName.create(Target.class); /* Primitive types and their boxed counterparts diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java index b52ba88a4bd..3de312eb13c 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java @@ -167,4 +167,25 @@ interface TypedElementInfoBlueprint extends Annotated { */ @Option.Redundant Optional originatingElement(); + + /** + * The element used to create this instance, or {@link io.helidon.common.types.TypedElementInfo#signature()} + * if none provided. + * The type of the object depends on the environment we are in - it may be an {@code TypeElement} in annotation processing, + * or a {@code MethodInfo} (and such) when using classpath scanning. + * + * @return originating element, or the signature of this element + */ + default Object originatingElementValue() { + return originatingElement().orElseGet(this::signature); + } + + /** + * Signature of this element. + * + * @return signature of this element + * @see io.helidon.common.types.ElementSignature + */ + @Option.Access("") + ElementSignature signature(); } diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java index 9edf23abd11..0cba7edaff8 100644 --- a/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or 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.types; +import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.stream.Collectors; @@ -60,10 +61,67 @@ static class BuilderDecorator implements Prototype.BuilderDecorator target) { -/* + backwardCompatibility(target); + constructorName(target); + signature(target); + } + + private void signature(TypedElementInfo.BuilderBase target) { + if (target.kind().isEmpty()) { + // this will fail when validating + target.signature(ElementSignatures.createNone()); + return; + } else { + target.signature(signature(target, target.kind().get())); + } + } + + private ElementSignature signature(TypedElementInfo.BuilderBase target, ElementKind elementKind) { + if (elementKind == ElementKind.CONSTRUCTOR) { + return ElementSignatures.createConstructor(toTypes(target.parameterArguments())); + } + // for everything else we need the type (it is required) + if (target.typeName().isEmpty() || target.elementName().isEmpty()) { + return ElementSignatures.createNone(); + } + + TypeName typeName = target.typeName().get(); + String name = target.elementName().get(); + + if (elementKind == ElementKind.FIELD + || elementKind == ElementKind.RECORD_COMPONENT + || elementKind == ElementKind.ENUM_CONSTANT) { + return ElementSignatures.createField(typeName, name); + } + if (elementKind == ElementKind.METHOD) { + return ElementSignatures.createMethod(typeName, name, toTypes(target.parameterArguments())); + } + if (elementKind == ElementKind.PARAMETER) { + return ElementSignatures.createParameter(typeName, name); + } + return ElementSignatures.createNone(); + } + + private List toTypes(List typedElementInfos) { + return typedElementInfos.stream() + .map(TypedElementInfo::typeName) + .collect(Collectors.toUnmodifiableList()); + } + + private void constructorName(TypedElementInfo.BuilderBase target) { + Optional elementKind = target.kind(); + if (elementKind.isPresent()) { + if (elementKind.get() == ElementKind.CONSTRUCTOR) { + target.elementName(""); + } + } + } + + @SuppressWarnings("removal") + private void backwardCompatibility(TypedElementInfo.BuilderBase target) { + /* Backward compatibility for deprecated methods. */ if (target.kind().isEmpty() && target.elementTypeKind().isPresent()) { @@ -103,14 +161,6 @@ public void decorate(TypedElementInfo.BuilderBase target) { target.addModifier(typeModifier.modifierName()); } target.addModifier(target.accessModifier().get().modifierName()); - - - Optional elementKind = target.kind(); - if (elementKind.isPresent()) { - if (elementKind.get() == ElementKind.CONSTRUCTOR) { - target.elementName(""); - } - } } } } diff --git a/builder/tests/common-types/src/main/java/io/helidon/common/types/package-info.java b/builder/tests/common-types/src/main/java/io/helidon/common/types/package-info.java new file mode 100644 index 00000000000..7851acd401b --- /dev/null +++ b/builder/tests/common-types/src/main/java/io/helidon/common/types/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2022, 2024 Oracle and/or 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. + */ + +/** + * Subset of Builder's SPI types that are useful for runtime. Used in the ConfigBean builder, etc., that require a minimal set of + * types present at runtime. + */ +package io.helidon.common.types; diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java index 45d998f2bc6..5f080864e79 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptAnnotationFactory.java @@ -16,8 +16,11 @@ package io.helidon.codegen.apt; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Optional; +import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; @@ -26,6 +29,7 @@ import io.helidon.common.types.Annotation; import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; /** * Factory for annotations. @@ -48,7 +52,43 @@ public static Annotation createAnnotation(AnnotationMirror am, .orElseThrow(() -> new IllegalArgumentException("Cannot create annotation for non-existent type: " + am.getAnnotationType())); - return Annotation.create(val, extractAnnotationValues(am, elements)); + // ignore these annotations, unless one of them was explicitly requested + var set = new HashSet(); + set.add(TypeNames.INHERITED); + set.add(TypeNames.TARGET); + set.add(TypeNames.RETENTION); + set.add(TypeNames.DOCUMENTED); + set.remove(val); + + return createAnnotation(elements, am, set) + .orElseThrow(); + } + + private static Optional createAnnotation(Elements elements, AnnotationMirror am, Set processedTypes) { + TypeName val = AptTypeFactory.createTypeName(am.getAnnotationType()) + .orElseThrow(() -> new IllegalArgumentException("Cannot create annotation for non-existent type: " + + am.getAnnotationType())); + + if (processedTypes.contains(val)) { + return Optional.empty(); + } + + Annotation.Builder builder = Annotation.builder(); + + elements.getAllAnnotationMirrors(am.getAnnotationType().asElement()) + .stream() + .map(it -> { + var newProcessed = new HashSet<>(processedTypes); + newProcessed.add(val); + return createAnnotation(elements, it, newProcessed); + }) + .flatMap(Optional::stream) + .forEach(builder::addMetaAnnotation); + + return Optional.of(builder + .typeName(val) + .values(extractAnnotationValues(am, elements)) + .build()); } /** diff --git a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptFiler.java b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptFiler.java index df4cc10db60..9efa87e8e72 100644 --- a/codegen/apt/src/main/java/io/helidon/codegen/apt/AptFiler.java +++ b/codegen/apt/src/main/java/io/helidon/codegen/apt/AptFiler.java @@ -40,6 +40,7 @@ import io.helidon.codegen.FilerTextResource; import io.helidon.codegen.IndentType; import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.common.types.TypeName; import static java.nio.charset.StandardCharsets.UTF_8; @@ -73,6 +74,23 @@ public Path writeSourceFile(ClassModel classModel, Object... originatingElements } } + @Override + public Path writeSourceFile(TypeName type, String content, Object... originatingElements) { + Element[] elements = toElements(originatingElements); + + try { + JavaFileObject sourceFile = filer.createSourceFile(type.fqName(), elements); + try (Writer os = sourceFile.openWriter()) { + os.write(content); + } + return Path.of(sourceFile.toUri()); + } catch (IOException e) { + throw new CodegenException("Failed to write source file for type: " + type, + e, + originatingElement(elements, type)); + } + } + @Override public Path writeResource(byte[] resource, String location, Object... originatingElements) { Element[] elements = toElements(originatingElements); diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java index d5055caae68..26dc36a78fa 100644 --- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java +++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/ContentSupport.java @@ -106,7 +106,7 @@ static void addCreateElement(ContentBuilder contentBuilder, TypedElementInfo static void addCreateAnnotation(ContentBuilder contentBuilder, Annotation annotation) { Map values = annotation.values(); - if (values.isEmpty()) { + if (values.isEmpty() && annotation.metaAnnotations().isEmpty()) { // Annotation.create(TypeName.create("my.type.AnnotationType")) contentBuilder.addContent(ANNOTATION) .addContent(".create(") @@ -136,6 +136,16 @@ static void addCreateAnnotation(ContentBuilder contentBuilder, Annotation ann contentBuilder.addContentLine(")"); }); + // .addMetaAnnotation(...) + annotation.metaAnnotations() + .forEach(it -> contentBuilder.addContent(".addMetaAnnotation(") + .increaseContentPadding() + .increaseContentPadding() + .addContentCreate(it) + .addContentLine(")") + .decreaseContentPadding() + .decreaseContentPadding()); + // .build() contentBuilder.addContentLine(".build()") .decreaseContentPadding() diff --git a/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/AnnotationTest.java b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/AnnotationTest.java index 85aea878fc1..0e95c163447 100644 --- a/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/AnnotationTest.java +++ b/codegen/class-model/src/test/java/io/helidon/codegen/classmodel/AnnotationTest.java @@ -57,6 +57,37 @@ void testPrintEnumValue() { private String name;""")); } + @Test + void testMetaAnnotation() { + Field field = Field.builder() + .accessModifier(AccessModifier.PRIVATE) + .type(Annotation.class) + .name("annotation") + .addContentCreate(Annotation.builder() + .typeName(ANNOTATION_TYPE) + .putValue("value", "someValue") + .addMetaAnnotation(Annotation.builder() + .typeName(ANNOTATION_TYPE) + .putValue("value", "string") + .build()) + .build()) + .build(); + String text = write(field); + + String expected = """ + private Annotation annotation = Annotation.builder() + .typeName(TypeName.create("org.junit.jupiter.api.Test")) + .putValue("value", "someValue") + .addMetaAnnotation(Annotation.builder() + .typeName(TypeName.create("org.junit.jupiter.api.Test")) + .putValue("value", "string") + .build() + ) + .build();"""; + + assertThat(text, is(expected)); + } + @Test void testContentCreateEnumValue() { TypeName enumType = TypeName.create(TestEnum.class); diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenFiler.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenFiler.java index d8381653f8f..abd03ec7392 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenFiler.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenFiler.java @@ -37,11 +37,21 @@ public interface CodegenFiler { * * @param classModel class model to write out * @param originatingElements elements that caused this type to be generated - * (you can use {@link io.helidon.common.types.TypeInfo#originatingElement()} for example + * (you can use {@link io.helidon.common.types.TypeInfo#originatingElementValue()}) * @return written path, we expect to always run on local file system */ Path writeSourceFile(ClassModel classModel, Object... originatingElements); + /** + * Write a source file using string content. + * + * @param type type of the file to generate + * @param content source code to write + * @param originatingElements elements that caused this type to be generated + * @return written path, we expect to always run on local file system + */ + Path writeSourceFile(TypeName type, String content, Object... originatingElements); + /** * Write a resource file. * diff --git a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenValidator.java b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenValidator.java index ff3d21ebb2d..97b61cc9da1 100644 --- a/codegen/codegen/src/main/java/io/helidon/codegen/CodegenValidator.java +++ b/codegen/codegen/src/main/java/io/helidon/codegen/CodegenValidator.java @@ -53,8 +53,7 @@ public static String validateUri(TypeName enclosingType, + property + "(): " + "\"" + value + "\" cannot be parsed. Invalid URI.", e, - element.originatingElement().orElseGet(() -> enclosingType.fqName() + "." - + element.elementName())); + element.originatingElementValue()); } } @@ -84,8 +83,7 @@ public static String validateDuration(TypeName enclosingType, + " expression such as 'PT1S' (1 second), 'PT0.1S' (tenth of a second)." + " Please check javadoc of " + Duration.class.getName() + " class.", e, - element.originatingElement().orElseGet(() -> enclosingType.fqName() + "." - + element.elementName())); + element.originatingElementValue()); } } } diff --git a/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanAnnotationFactory.java b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanAnnotationFactory.java index 6861e4d6b2d..8d4c1738f24 100644 --- a/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanAnnotationFactory.java +++ b/codegen/scan/src/main/java/io/helidon/codegen/scan/ScanAnnotationFactory.java @@ -18,19 +18,23 @@ import java.lang.reflect.Array; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import io.helidon.common.types.Annotation; import io.helidon.common.types.EnumValue; import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; import io.github.classgraph.AnnotationClassRef; import io.github.classgraph.AnnotationEnumValue; import io.github.classgraph.AnnotationInfo; import io.github.classgraph.AnnotationParameterValue; import io.github.classgraph.AnnotationParameterValueList; +import io.github.classgraph.ClassInfo; /** * Factory for annotations. @@ -50,7 +54,44 @@ public static Annotation createAnnotation(ScanContext ctx, AnnotationInfo am) { TypeName typeName = ScanTypeFactory.create(am.getClassInfo()); - return Annotation.create(typeName, extractAnnotationValues(ctx, am)); + // ignore these annotations, unless one of them was explicitly requested + var set = new HashSet(); + set.add(TypeNames.INHERITED); + set.add(TypeNames.TARGET); + set.add(TypeNames.RETENTION); + set.add(TypeNames.DOCUMENTED); + set.remove(typeName); + + return createAnnotation(ctx, am, set) + .orElseThrow(); + } + + private static Optional createAnnotation(ScanContext ctx, AnnotationInfo am, HashSet processedTypes) { + ClassInfo classInfo = am.getClassInfo(); + if (classInfo == null) { + // cannot analyze this annotation + return Optional.empty(); + } + TypeName typeName = ScanTypeFactory.create(classInfo); + + if (processedTypes.contains(typeName)) { + return Optional.empty(); + } + var builder = Annotation.builder(); + + classInfo.getAnnotationInfo() + .stream() + .map(it -> { + var newProcessed = new HashSet<>(processedTypes); + newProcessed.add(typeName); + return createAnnotation(ctx, it, newProcessed); + }) + .flatMap(Optional::stream) + .forEach(builder::addMetaAnnotation); + + return Optional.of(builder.typeName(typeName) + .values(extractAnnotationValues(ctx, am)) + .build()); } /** diff --git a/common/features/features/src/main/java/io/helidon/common/features/FeatureCatalog.java b/common/features/features/src/main/java/io/helidon/common/features/FeatureCatalog.java index 6fd7f937c01..35db736c3a4 100644 --- a/common/features/features/src/main/java/io/helidon/common/features/FeatureCatalog.java +++ b/common/features/features/src/main/java/io/helidon/common/features/FeatureCatalog.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or 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,8 @@ static List features(ClassLoader classLoader) { } String module = props.getProperty("m"); if (module == null) { - LOGGER.log(Level.WARNING, "Got module descriptor with no module name. Available properties: " + props); + LOGGER.log(Level.WARNING, "Got module descriptor with no module name. Available properties: " + props + + " at " + url); continue; } FeatureDescriptor.Builder builder = FeatureDescriptor.builder(); diff --git a/common/types/src/main/java/io/helidon/common/types/Annotated.java b/common/types/src/main/java/io/helidon/common/types/Annotated.java index 292368d72de..3aab5620bdf 100644 --- a/common/types/src/main/java/io/helidon/common/types/Annotated.java +++ b/common/types/src/main/java/io/helidon/common/types/Annotated.java @@ -43,6 +43,8 @@ public interface Annotated { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @return list of all meta annotations of this element */ diff --git a/common/types/src/main/java/io/helidon/common/types/Annotation.java b/common/types/src/main/java/io/helidon/common/types/Annotation.java index f08439985d6..bd27d407599 100644 --- a/common/types/src/main/java/io/helidon/common/types/Annotation.java +++ b/common/types/src/main/java/io/helidon/common/types/Annotation.java @@ -17,8 +17,10 @@ package io.helidon.common.types; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -153,7 +155,9 @@ static Annotation create(TypeName annoTypeName, Map values) { */ abstract class BuilderBase, PROTOTYPE extends Annotation> implements Prototype.Builder { + private final List metaAnnotations = new ArrayList<>(); private final Map values = new LinkedHashMap<>(); + private boolean isMetaAnnotationsMutated; private TypeName typeName; /** @@ -171,6 +175,10 @@ protected BuilderBase() { public BUILDER from(Annotation prototype) { typeName(prototype.typeName()); addValues(prototype.values()); + if (!isMetaAnnotationsMutated) { + metaAnnotations.clear(); + } + addMetaAnnotations(prototype.metaAnnotations()); return self(); } @@ -182,7 +190,15 @@ public BUILDER from(Annotation prototype) { */ public BUILDER from(Annotation.BuilderBase builder) { builder.typeName().ifPresent(this::typeName); - addValues(builder.values()); + addValues(builder.values); + if (isMetaAnnotationsMutated) { + if (builder.isMetaAnnotationsMutated) { + addMetaAnnotations(builder.metaAnnotations); + } + } else { + metaAnnotations.clear(); + addMetaAnnotations(builder.metaAnnotations); + } return self(); } @@ -293,6 +309,64 @@ public BUILDER putValue(String key, Object value) { return self(); } + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @param metaAnnotations list of all annotations declared on the annotation type, or inherited from them + * @return updated builder instance + * @see #metaAnnotations() + */ + public BUILDER metaAnnotations(List metaAnnotations) { + Objects.requireNonNull(metaAnnotations); + isMetaAnnotationsMutated = true; + this.metaAnnotations.clear(); + this.metaAnnotations.addAll(metaAnnotations); + return self(); + } + + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @param metaAnnotations list of all annotations declared on the annotation type, or inherited from them + * @return updated builder instance + * @see #metaAnnotations() + */ + public BUILDER addMetaAnnotations(List metaAnnotations) { + Objects.requireNonNull(metaAnnotations); + isMetaAnnotationsMutated = true; + this.metaAnnotations.addAll(metaAnnotations); + return self(); + } + + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @param metaAnnotation list of all annotations declared on the annotation type, or inherited from them + * @return updated builder instance + * @see #metaAnnotations() + */ + public BUILDER addMetaAnnotation(Annotation metaAnnotation) { + Objects.requireNonNull(metaAnnotation); + this.metaAnnotations.add(metaAnnotation); + isMetaAnnotationsMutated = true; + return self(); + } + + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @param consumer list of all annotations declared on the annotation type, or inherited from them + * @return updated builder instance + * @see #metaAnnotations() + */ + public BUILDER addMetaAnnotation(Consumer consumer) { + Objects.requireNonNull(consumer); + var builder = Annotation.builder(); + consumer.accept(builder); + this.metaAnnotations.add(builder.build()); + return self(); + } + /** * The type name, e.g., {@link java.util.Objects} -> "java.util.Objects". * @@ -311,6 +385,15 @@ public Map values() { return values; } + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @return the meta annotations + */ + public List metaAnnotations() { + return metaAnnotations; + } + @Override public String toString() { return "AnnotationBuilder{" @@ -341,6 +424,7 @@ protected void validatePrototype() { */ protected static class AnnotationImpl implements Annotation { + private final List metaAnnotations; private final Map values; private final TypeName typeName; @@ -352,6 +436,7 @@ protected static class AnnotationImpl implements Annotation { protected AnnotationImpl(Annotation.BuilderBase builder) { this.typeName = builder.typeName().get(); this.values = Collections.unmodifiableMap(new LinkedHashMap<>(builder.values())); + this.metaAnnotations = List.copyOf(builder.metaAnnotations()); } @Override @@ -369,6 +454,11 @@ public Map values() { return values; } + @Override + public List metaAnnotations() { + return metaAnnotations; + } + @Override public String toString() { return "Annotation{" @@ -386,7 +476,7 @@ public boolean equals(Object o) { return false; } return Objects.equals(typeName, other.typeName()) - && Objects.equals(values, other.values()); + && Objects.equals(values, other.values()); } @Override diff --git a/common/types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java b/common/types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java index 13e2a99d5f6..b51fb659b6d 100644 --- a/common/types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java +++ b/common/types/src/main/java/io/helidon/common/types/AnnotationBlueprint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -79,6 +79,15 @@ interface AnnotationBlueprint { @Option.Singular Map values(); + /** + * A list of inherited annotations (from the whole hierarchy). + * + * @return list of all annotations declared on the annotation type, or inherited from them + */ + @Option.Redundant + @Option.Singular + List metaAnnotations(); + /** * The value property. * @@ -653,4 +662,19 @@ default > Optional> enumValues(String property, Class< return AnnotationSupport.asEnums(typeName(), values(), property, type); } + /** + * Check if {@link io.helidon.common.types.Annotation#metaAnnotations()} contains an annotation of the provided type. + *

+ * Note: we ignore {@link java.lang.annotation.Target}, {@link java.lang.annotation.Inherited}, + * {@link java.lang.annotation.Documented}, and {@link java.lang.annotation.Retention}. + * + * @param annotationType type of annotation + * @return {@code true} if the annotation is declared on this annotation, or is inherited from a declared annotation + */ + default boolean hasMetaAnnotation(TypeName annotationType) { + return metaAnnotations() + .stream() + .map(Annotation::typeName) + .anyMatch(annotationType::equals); + } } diff --git a/common/types/src/main/java/io/helidon/common/types/AnnotationSupport.java b/common/types/src/main/java/io/helidon/common/types/AnnotationSupport.java index 242940fb555..b21a8a1a917 100644 --- a/common/types/src/main/java/io/helidon/common/types/AnnotationSupport.java +++ b/common/types/src/main/java/io/helidon/common/types/AnnotationSupport.java @@ -628,18 +628,15 @@ private static Class asClass(TypeName typeName, String property, Object value return theClass; } - String className; - - if (value instanceof TypeName tn) { - className = tn.fqName(); - } else if (value instanceof String str) { - className = str; - } else { - - throw new IllegalArgumentException(typeName.fqName() + " property " + property - + " of type " + value.getClass().getName() - + " cannot be converted to Class"); - } + String className = switch (value) { + case TypeName tn -> tn.name(); + case String str -> str; + default -> { + throw new IllegalArgumentException(typeName.fqName() + " property " + property + + " of type " + value.getClass().getName() + + " cannot be converted to Class"); + } + }; try { return Class.forName(className); diff --git a/common/types/src/main/java/io/helidon/common/types/ElementSignature.java b/common/types/src/main/java/io/helidon/common/types/ElementSignature.java new file mode 100644 index 00000000000..a167313516d --- /dev/null +++ b/common/types/src/main/java/io/helidon/common/types/ElementSignature.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 Oracle and/or 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.types; + +import java.util.List; + +/** + * Signature of a {@link io.helidon.common.types.TypedElementInfo}. + *

+ * The {@link io.helidon.common.types.TypedElementInfo#signature()} is intended to compare + * fields, methods, and constructors across type hierarchy - for example when looking for a method + * that we override. + *

+ * The following information is used for equals and hash-code: + *

    + *
  • Field: field name
  • + *
  • Constructor: parameter types
  • + *
  • Method: method name, parameter types
  • + *
  • Parameter: this signature is not useful, as we cannot depend on parameter names
  • + *
+ * + * The signature has well-defined {@code hashCode} and {@code equals} methods, + * so it can be safely used as a key in a {@link java.util.Map}. + *

+ * This interface is sealed, an instance can only be obtained + * from {@link io.helidon.common.types.TypedElementInfo#signature()}. + * + * @see #text() + */ +public sealed interface ElementSignature permits ElementSignatures.FieldSignature, + ElementSignatures.MethodSignature, + ElementSignatures.ParameterSignature, + ElementSignatures.NoSignature { + /** + * Type of the element. Resolves as follows: + *

    + *
  • Field: type of the field
  • + *
  • Constructor: void
  • + *
  • Method: method return type
  • + *
  • Parameter: parameter type
  • + *
+ * + * @return type of this element, never used for equals or hashCode + */ + TypeName type(); + + /** + * Name of the element. For constructor, this always returns {@code }, + * for parameters, this method may return the real parameter name or an index + * parameter name depending on the source of the information (during annotation processing, + * this would be the actual parameter name, when classpath scanning, this would be something like + * {@code param0}. + * + * @return name of this element + */ + String name(); + + /** + * Types of parameters if this represents a method or a constructor, + * empty {@link java.util.List} otherwise. + * + * @return parameter types + */ + List parameterTypes(); + + /** + * A text representation of this signature. + * + *
    + *
  • Field: field name (such as {@code myNiceField}
  • + *
  • Constructor: comma separated parameter types (no generics) in parentheses (such as + * {@code (java.lang.String,java.util.List)})
  • + *
  • Method: method name, parameter types (no generics) in parentheses (such as + * {@code methodName(java.lang.String,java.util.List)}
  • + *
  • Parameter: parameter name (such as {@code myParameter} or {@code param0} - not very useful, as parameter names + * are not carried over to compiled code in Java
  • + *
+ * + * @return text representation + */ + String text(); +} diff --git a/common/types/src/main/java/io/helidon/common/types/ElementSignatures.java b/common/types/src/main/java/io/helidon/common/types/ElementSignatures.java new file mode 100644 index 00000000000..4f5be563f29 --- /dev/null +++ b/common/types/src/main/java/io/helidon/common/types/ElementSignatures.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2024 Oracle and/or 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.types; + +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +final class ElementSignatures { + private ElementSignatures() { + } + + static ElementSignature createNone() { + return new NoSignature(); + } + + static ElementSignature createField(TypeName type, + String name) { + Objects.requireNonNull(type); + Objects.requireNonNull(name); + return new FieldSignature(type, name); + } + + static ElementSignature createConstructor(List parameters) { + Objects.requireNonNull(parameters); + return new MethodSignature(TypeNames.PRIMITIVE_VOID, + "", + parameters); + } + + static ElementSignature createMethod(TypeName returnType, String name, List parameters) { + Objects.requireNonNull(returnType); + Objects.requireNonNull(name); + Objects.requireNonNull(parameters); + return new MethodSignature(returnType, + name, + parameters); + } + + static ElementSignature createParameter(TypeName type, String name) { + Objects.requireNonNull(type); + Objects.requireNonNull(name); + return new ParameterSignature(type, name); + } + + static final class FieldSignature implements ElementSignature { + private final TypeName type; + private final String name; + + private FieldSignature(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return List.of(); + } + + @Override + public String text() { + return name; + } + + @Override + public String toString() { + return type.resolvedName() + " " + name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FieldSignature that)) { + return false; + } + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + } + + static final class MethodSignature implements ElementSignature { + private final TypeName type; + private final String name; + private final List parameters; + private final String text; + private final boolean constructor; + + private MethodSignature(TypeName type, + String name, + List parameters) { + this.type = type; + this.name = name; + this.parameters = parameters; + if (name.equals("")) { + this.constructor = true; + this.text = parameterTypesSection(parameters, ",", TypeName::fqName); + } else { + this.constructor = false; + this.text = name + parameterTypesSection(parameters, ",", TypeName::fqName); + } + + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return parameters; + } + + @Override + public String text() { + return text; + } + + @Override + public String toString() { + if (constructor) { + return text; + } else { + return type.resolvedName() + " " + name + parameterTypesSection(parameters, + ", ", + TypeName::resolvedName); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MethodSignature that)) { + return false; + } + return Objects.equals(name, that.name) && Objects.equals(parameters, that.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(name, parameters); + } + } + + static final class ParameterSignature implements ElementSignature { + private final TypeName type; + private final String name; + + private ParameterSignature(TypeName type, String name) { + this.type = type; + this.name = name; + } + + @Override + public TypeName type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public List parameterTypes() { + return List.of(); + } + + @Override + public String text() { + return name; + } + + @Override + public String toString() { + return type.resolvedName() + " " + name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ParameterSignature that)) { + return false; + } + return name.equals(that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + } + + static final class NoSignature implements ElementSignature { + @Override + public TypeName type() { + return TypeNames.PRIMITIVE_VOID; + } + + @Override + public String name() { + return ""; + } + + @Override + public String text() { + return ""; + } + + @Override + public String toString() { + return text(); + } + + @Override + public List parameterTypes() { + return List.of(); + } + } + + private static String parameterTypesSection(List parameters, + String delimiter, + Function typeMapper) { + return parameters.stream() + .map(typeMapper) + .collect(Collectors.joining(delimiter, "(", ")")); + } +} diff --git a/common/types/src/main/java/io/helidon/common/types/EnumValueImpl.java b/common/types/src/main/java/io/helidon/common/types/EnumValueImpl.java index 76bd62f24dc..f2205b2452e 100644 --- a/common/types/src/main/java/io/helidon/common/types/EnumValueImpl.java +++ b/common/types/src/main/java/io/helidon/common/types/EnumValueImpl.java @@ -52,4 +52,9 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(type, name); } + + @Override + public String toString() { + return type.fqName() + "." + name; + } } 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 d47b9f04df1..cc027638df8 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 @@ -75,6 +75,11 @@ abstract class BuilderBase elementModifiers = new LinkedHashSet<>(); private final Set modifiers = new LinkedHashSet<>(); private AccessModifier accessModifier; + private boolean isAnnotationsMutated; + private boolean isElementInfoMutated; + private boolean isInheritedAnnotationsMutated; + private boolean isInterfaceTypeInfoMutated; + private boolean isOtherElementInfoMutated; private ElementKind kind; private Object originatingElement; private String description; @@ -90,7 +95,7 @@ protected BuilderBase() { } /** - * Update this builder from an existing prototype instance. + * Update this builder from an existing prototype instance. This method disables automatic service discovery. * * @param prototype existing prototype to update this builder from * @return updated builder instance @@ -100,18 +105,33 @@ public BUILDER from(TypeInfo prototype) { description(prototype.description()); typeKind(prototype.typeKind()); kind(prototype.kind()); + if (!isElementInfoMutated) { + elementInfo.clear(); + } addElementInfo(prototype.elementInfo()); + if (!isOtherElementInfoMutated) { + otherElementInfo.clear(); + } addOtherElementInfo(prototype.otherElementInfo()); addReferencedTypeNamesToAnnotations(prototype.referencedTypeNamesToAnnotations()); addReferencedModuleNames(prototype.referencedModuleNames()); superTypeInfo(prototype.superTypeInfo()); + if (!isInterfaceTypeInfoMutated) { + interfaceTypeInfo.clear(); + } addInterfaceTypeInfo(prototype.interfaceTypeInfo()); addModifiers(prototype.modifiers()); addElementModifiers(prototype.elementModifiers()); accessModifier(prototype.accessModifier()); module(prototype.module()); originatingElement(prototype.originatingElement()); + if (!isAnnotationsMutated) { + annotations.clear(); + } addAnnotations(prototype.annotations()); + if (!isInheritedAnnotationsMutated) { + inheritedAnnotations.clear(); + } addInheritedAnnotations(prototype.inheritedAnnotations()); return self(); } @@ -127,19 +147,54 @@ public BUILDER from(TypeInfo.BuilderBase builder) { builder.description().ifPresent(this::description); builder.typeKind().ifPresent(this::typeKind); builder.kind().ifPresent(this::kind); - addElementInfo(builder.elementInfo()); - addOtherElementInfo(builder.otherElementInfo()); - addReferencedTypeNamesToAnnotations(builder.referencedTypeNamesToAnnotations()); - addReferencedModuleNames(builder.referencedModuleNames()); + if (isElementInfoMutated) { + if (builder.isElementInfoMutated) { + addElementInfo(builder.elementInfo); + } + } else { + elementInfo.clear(); + addElementInfo(builder.elementInfo); + } + if (isOtherElementInfoMutated) { + if (builder.isOtherElementInfoMutated) { + addOtherElementInfo(builder.otherElementInfo); + } + } else { + otherElementInfo.clear(); + addOtherElementInfo(builder.otherElementInfo); + } + addReferencedTypeNamesToAnnotations(builder.referencedTypeNamesToAnnotations); + addReferencedModuleNames(builder.referencedModuleNames); builder.superTypeInfo().ifPresent(this::superTypeInfo); - addInterfaceTypeInfo(builder.interfaceTypeInfo()); - addModifiers(builder.modifiers()); - addElementModifiers(builder.elementModifiers()); + if (isInterfaceTypeInfoMutated) { + if (builder.isInterfaceTypeInfoMutated) { + addInterfaceTypeInfo(builder.interfaceTypeInfo); + } + } else { + interfaceTypeInfo.clear(); + addInterfaceTypeInfo(builder.interfaceTypeInfo); + } + addModifiers(builder.modifiers); + addElementModifiers(builder.elementModifiers); builder.accessModifier().ifPresent(this::accessModifier); builder.module().ifPresent(this::module); builder.originatingElement().ifPresent(this::originatingElement); - addAnnotations(builder.annotations()); - addInheritedAnnotations(builder.inheritedAnnotations()); + if (isAnnotationsMutated) { + if (builder.isAnnotationsMutated) { + addAnnotations(builder.annotations); + } + } else { + annotations.clear(); + addAnnotations(builder.annotations); + } + if (isInheritedAnnotationsMutated) { + if (builder.isInheritedAnnotationsMutated) { + addInheritedAnnotations(builder.inheritedAnnotations); + } + } else { + inheritedAnnotations.clear(); + addInheritedAnnotations(builder.inheritedAnnotations); + } return self(); } @@ -262,6 +317,7 @@ public BUILDER kind(ElementKind kind) { */ public BUILDER elementInfo(List elementInfo) { Objects.requireNonNull(elementInfo); + isElementInfoMutated = true; this.elementInfo.clear(); this.elementInfo.addAll(elementInfo); return self(); @@ -276,6 +332,7 @@ public BUILDER elementInfo(List elementInfo) { */ public BUILDER addElementInfo(List elementInfo) { Objects.requireNonNull(elementInfo); + isElementInfoMutated = true; this.elementInfo.addAll(elementInfo); return self(); } @@ -290,6 +347,7 @@ public BUILDER addElementInfo(List elementInfo) { public BUILDER addElementInfo(TypedElementInfo elementInfo) { Objects.requireNonNull(elementInfo); this.elementInfo.add(elementInfo); + isElementInfoMutated = true; return self(); } @@ -318,6 +376,7 @@ public BUILDER addElementInfo(Consumer consumer) { */ public BUILDER otherElementInfo(List otherElementInfo) { Objects.requireNonNull(otherElementInfo); + isOtherElementInfoMutated = true; this.otherElementInfo.clear(); this.otherElementInfo.addAll(otherElementInfo); return self(); @@ -333,6 +392,7 @@ public BUILDER otherElementInfo(List otherElementInf */ public BUILDER addOtherElementInfo(List otherElementInfo) { Objects.requireNonNull(otherElementInfo); + isOtherElementInfoMutated = true; this.otherElementInfo.addAll(otherElementInfo); return self(); } @@ -348,6 +408,7 @@ public BUILDER addOtherElementInfo(List otherElement public BUILDER addOtherElementInfo(TypedElementInfo otherElementInfo) { Objects.requireNonNull(otherElementInfo); this.otherElementInfo.add(otherElementInfo); + isOtherElementInfoMutated = true; return self(); } @@ -374,8 +435,8 @@ public BUILDER addOtherElementInfo(Consumer consumer) * @return updated builder instance * @see #referencedTypeNamesToAnnotations() */ - public BUILDER referencedTypeNamesToAnnotations(Map> referencedTypeNamesToAnnotations) { + public BUILDER referencedTypeNamesToAnnotations( + Map> referencedTypeNamesToAnnotations) { Objects.requireNonNull(referencedTypeNamesToAnnotations); this.referencedTypeNamesToAnnotations.clear(); this.referencedTypeNamesToAnnotations.putAll(referencedTypeNamesToAnnotations); @@ -539,6 +600,7 @@ public BUILDER superTypeInfo(Consumer consumer) { */ public BUILDER interfaceTypeInfo(List interfaceTypeInfo) { Objects.requireNonNull(interfaceTypeInfo); + isInterfaceTypeInfoMutated = true; this.interfaceTypeInfo.clear(); this.interfaceTypeInfo.addAll(interfaceTypeInfo); return self(); @@ -553,6 +615,7 @@ public BUILDER interfaceTypeInfo(List interfaceTypeInfo) { */ public BUILDER addInterfaceTypeInfo(List interfaceTypeInfo) { Objects.requireNonNull(interfaceTypeInfo); + isInterfaceTypeInfoMutated = true; this.interfaceTypeInfo.addAll(interfaceTypeInfo); return self(); } @@ -567,6 +630,7 @@ public BUILDER addInterfaceTypeInfo(List interfaceTypeInfo) public BUILDER addInterfaceTypeInfo(TypeInfo interfaceTypeInfo) { Objects.requireNonNull(interfaceTypeInfo); this.interfaceTypeInfo.add(interfaceTypeInfo); + isInterfaceTypeInfoMutated = true; return self(); } @@ -744,6 +808,7 @@ public BUILDER originatingElement(Object originatingElement) { */ public BUILDER annotations(List annotations) { Objects.requireNonNull(annotations); + isAnnotationsMutated = true; this.annotations.clear(); this.annotations.addAll(annotations); return self(); @@ -760,6 +825,7 @@ public BUILDER annotations(List annotations) { */ public BUILDER addAnnotations(List annotations) { Objects.requireNonNull(annotations); + isAnnotationsMutated = true; this.annotations.addAll(annotations); return self(); } @@ -776,6 +842,7 @@ public BUILDER addAnnotations(List annotations) { public BUILDER addAnnotation(Annotation annotation) { Objects.requireNonNull(annotation); this.annotations.add(annotation); + isAnnotationsMutated = true; return self(); } @@ -802,6 +869,8 @@ public BUILDER addAnnotation(Consumer consumer) { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotations list of all meta annotations of this element * @return updated builder instance @@ -809,6 +878,7 @@ public BUILDER addAnnotation(Consumer consumer) { */ public BUILDER inheritedAnnotations(List inheritedAnnotations) { Objects.requireNonNull(inheritedAnnotations); + isInheritedAnnotationsMutated = true; this.inheritedAnnotations.clear(); this.inheritedAnnotations.addAll(inheritedAnnotations); return self(); @@ -820,6 +890,8 @@ public BUILDER inheritedAnnotations(List inheritedAnnotati *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotations list of all meta annotations of this element * @return updated builder instance @@ -827,6 +899,7 @@ public BUILDER inheritedAnnotations(List inheritedAnnotati */ public BUILDER addInheritedAnnotations(List inheritedAnnotations) { Objects.requireNonNull(inheritedAnnotations); + isInheritedAnnotationsMutated = true; this.inheritedAnnotations.addAll(inheritedAnnotations); return self(); } @@ -837,6 +910,8 @@ public BUILDER addInheritedAnnotations(List inheritedAnnot *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotation list of all meta annotations of this element * @return updated builder instance @@ -845,6 +920,7 @@ public BUILDER addInheritedAnnotations(List inheritedAnnot public BUILDER addInheritedAnnotation(Annotation inheritedAnnotation) { Objects.requireNonNull(inheritedAnnotation); this.inheritedAnnotations.add(inheritedAnnotation); + isInheritedAnnotationsMutated = true; return self(); } @@ -854,6 +930,8 @@ public BUILDER addInheritedAnnotation(Annotation inheritedAnnotation) { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @param consumer list of all meta annotations of this element * @return updated builder instance @@ -1049,6 +1127,8 @@ public List annotations() { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @return the inherited annotations */ diff --git a/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java b/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java index 4bb3da96300..704c34d139c 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeInfoBlueprint.java @@ -16,10 +16,13 @@ package io.helidon.common.types; +import java.util.ArrayDeque; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Queue; import java.util.Set; import io.helidon.builder.api.Option; @@ -228,6 +231,54 @@ default Optional metaAnnotation(TypeName annotation, TypeName metaAn @Option.Redundant Optional originatingElement(); + /** + * The element used to create this instance, or {@link io.helidon.common.types.TypeInfo#typeName()} if none provided. + * The type of the object depends on the environment we are in - it may be an {@code TypeElement} in annotation processing, + * or a {@code ClassInfo} when using classpath scanning. + * + * @return originating element, or the type of this type info + */ + default Object originatingElementValue() { + return originatingElement().orElseGet(this::typeName); + } + + /** + * Checks if the current type implements, or extends the provided type. + * This method analyzes the whole dependency tree of the current type. + * + * @param typeName type of interface to check + * @return the super type info, or interface type info matching the provided type, with appropriate generic declarations + */ + default Optional findInHierarchy(TypeName typeName) { + if (typeName.equals(typeName())) { + return Optional.of((TypeInfo) this); + } + // scan super types + Optional superClass = superTypeInfo(); + if (superClass.isPresent() && !superClass.get().typeName().equals(TypeNames.OBJECT)) { + var superType = superClass.get(); + var foundInSuper = superType.findInHierarchy(typeName); + if (foundInSuper.isPresent()) { + return foundInSuper; + } + } + // nope, let's try interfaces + Queue interfaces = new ArrayDeque<>(interfaceTypeInfo()); + Set processed = new HashSet<>(); + + while (!interfaces.isEmpty()) { + TypeInfo type = interfaces.remove(); + // make sure we process each type only once + if (processed.add(type.typeName())) { + if (typeName.equals(type.typeName())) { + return Optional.of(type); + } + interfaces.addAll(type.interfaceTypeInfo()); + } + } + return Optional.empty(); + } + /** * Uses {@link io.helidon.common.types.TypeInfo#referencedModuleNames()} to determine if the module name is known for the * given type. diff --git a/common/types/src/main/java/io/helidon/common/types/TypeNames.java b/common/types/src/main/java/io/helidon/common/types/TypeNames.java index 62fe59da5a3..f539e5849af 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypeNames.java +++ b/common/types/src/main/java/io/helidon/common/types/TypeNames.java @@ -16,8 +16,10 @@ package io.helidon.common.types; +import java.lang.annotation.Documented; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; +import java.lang.annotation.Target; import java.time.Duration; import java.util.Collection; import java.util.List; @@ -73,10 +75,18 @@ public final class TypeNames { * Type name for {@link java.lang.annotation.Retention}. */ public static final TypeName RETENTION = TypeName.create(Retention.class); + /** + * Type name for {@link java.lang.annotation.Documented}. + */ + public static final TypeName DOCUMENTED = TypeName.create(Documented.class); /** * Type name for {@link java.lang.annotation.Inherited}. */ public static final TypeName INHERITED = TypeName.create(Inherited.class); + /** + * Type name for {@link java.lang.annotation.Target}. + */ + public static final TypeName TARGET = TypeName.create(Target.class); /* Primitive types and their boxed counterparts diff --git a/common/types/src/main/java/io/helidon/common/types/TypedElementInfo.java b/common/types/src/main/java/io/helidon/common/types/TypedElementInfo.java index 905b540268f..9560152bd7e 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypedElementInfo.java +++ b/common/types/src/main/java/io/helidon/common/types/TypedElementInfo.java @@ -79,7 +79,13 @@ abstract class BuilderBase elementModifiers = new LinkedHashSet<>(); private final Set modifiers = new LinkedHashSet<>(); private AccessModifier accessModifier; + private boolean isAnnotationsMutated; + private boolean isComponentTypesMutated; + private boolean isElementTypeAnnotationsMutated; + private boolean isInheritedAnnotationsMutated; + private boolean isParameterArgumentsMutated; private ElementKind kind; + private ElementSignature signature; private Object originatingElement; private String defaultValue; private String description; @@ -95,7 +101,7 @@ protected BuilderBase() { } /** - * Update this builder from an existing prototype instance. + * Update this builder from an existing prototype instance. This method disables automatic service discovery. * * @param prototype existing prototype to update this builder from * @return updated builder instance @@ -107,16 +113,32 @@ public BUILDER from(TypedElementInfo prototype) { elementTypeKind(prototype.elementTypeKind()); kind(prototype.kind()); defaultValue(prototype.defaultValue()); + if (!isElementTypeAnnotationsMutated) { + elementTypeAnnotations.clear(); + } addElementTypeAnnotations(prototype.elementTypeAnnotations()); + if (!isComponentTypesMutated) { + componentTypes.clear(); + } addComponentTypes(prototype.componentTypes()); addModifiers(prototype.modifiers()); addElementModifiers(prototype.elementModifiers()); accessModifier(prototype.accessModifier()); enclosingType(prototype.enclosingType()); + if (!isParameterArgumentsMutated) { + parameterArguments.clear(); + } addParameterArguments(prototype.parameterArguments()); addThrowsChecked(prototype.throwsChecked()); originatingElement(prototype.originatingElement()); + signature(prototype.signature()); + if (!isAnnotationsMutated) { + annotations.clear(); + } addAnnotations(prototype.annotations()); + if (!isInheritedAnnotationsMutated) { + inheritedAnnotations.clear(); + } addInheritedAnnotations(prototype.inheritedAnnotations()); return self(); } @@ -134,17 +156,53 @@ public BUILDER from(TypedElementInfo.BuilderBase builder) { builder.elementTypeKind().ifPresent(this::elementTypeKind); builder.kind().ifPresent(this::kind); builder.defaultValue().ifPresent(this::defaultValue); - addElementTypeAnnotations(builder.elementTypeAnnotations()); - addComponentTypes(builder.componentTypes()); - addModifiers(builder.modifiers()); - addElementModifiers(builder.elementModifiers()); + if (isElementTypeAnnotationsMutated) { + if (builder.isElementTypeAnnotationsMutated) { + addElementTypeAnnotations(builder.elementTypeAnnotations); + } + } else { + elementTypeAnnotations.clear(); + addElementTypeAnnotations(builder.elementTypeAnnotations); + } + if (isComponentTypesMutated) { + if (builder.isComponentTypesMutated) { + addComponentTypes(builder.componentTypes); + } + } else { + componentTypes.clear(); + addComponentTypes(builder.componentTypes); + } + addModifiers(builder.modifiers); + addElementModifiers(builder.elementModifiers); builder.accessModifier().ifPresent(this::accessModifier); builder.enclosingType().ifPresent(this::enclosingType); - addParameterArguments(builder.parameterArguments()); - addThrowsChecked(builder.throwsChecked()); + if (isParameterArgumentsMutated) { + if (builder.isParameterArgumentsMutated) { + addParameterArguments(builder.parameterArguments); + } + } else { + parameterArguments.clear(); + addParameterArguments(builder.parameterArguments); + } + addThrowsChecked(builder.throwsChecked); builder.originatingElement().ifPresent(this::originatingElement); - addAnnotations(builder.annotations()); - addInheritedAnnotations(builder.inheritedAnnotations()); + builder.signature().ifPresent(this::signature); + if (isAnnotationsMutated) { + if (builder.isAnnotationsMutated) { + addAnnotations(builder.annotations); + } + } else { + annotations.clear(); + addAnnotations(builder.annotations); + } + if (isInheritedAnnotationsMutated) { + if (builder.isInheritedAnnotationsMutated) { + addInheritedAnnotations(builder.inheritedAnnotations); + } + } else { + inheritedAnnotations.clear(); + addInheritedAnnotations(builder.inheritedAnnotations); + } return self(); } @@ -286,7 +344,7 @@ public BUILDER defaultValue(String defaultValue) { } /** - * The list of known annotations on the type name referenced by {@link #typeName()}. + * The list of known annotations on the type name referenced by {@link io.helidon.common.types.TypedElementInfo#typeName()}. * * @param elementTypeAnnotations the list of annotations on this element's (return) type. * @return updated builder instance @@ -294,13 +352,14 @@ public BUILDER defaultValue(String defaultValue) { */ public BUILDER elementTypeAnnotations(List elementTypeAnnotations) { Objects.requireNonNull(elementTypeAnnotations); + isElementTypeAnnotationsMutated = true; this.elementTypeAnnotations.clear(); this.elementTypeAnnotations.addAll(elementTypeAnnotations); return self(); } /** - * The list of known annotations on the type name referenced by {@link #typeName()}. + * The list of known annotations on the type name referenced by {@link io.helidon.common.types.TypedElementInfo#typeName()}. * * @param elementTypeAnnotations the list of annotations on this element's (return) type. * @return updated builder instance @@ -308,6 +367,7 @@ public BUILDER elementTypeAnnotations(List elementTypeAnno */ public BUILDER addElementTypeAnnotations(List elementTypeAnnotations) { Objects.requireNonNull(elementTypeAnnotations); + isElementTypeAnnotationsMutated = true; this.elementTypeAnnotations.addAll(elementTypeAnnotations); return self(); } @@ -321,6 +381,7 @@ public BUILDER addElementTypeAnnotations(List elementTypeA */ public BUILDER componentTypes(List componentTypes) { Objects.requireNonNull(componentTypes); + isComponentTypesMutated = true; this.componentTypes.clear(); this.componentTypes.addAll(componentTypes); return self(); @@ -335,6 +396,7 @@ public BUILDER componentTypes(List componentTypes) { */ public BUILDER addComponentTypes(List componentTypes) { Objects.requireNonNull(componentTypes); + isComponentTypesMutated = true; this.componentTypes.addAll(componentTypes); return self(); } @@ -493,6 +555,7 @@ public BUILDER enclosingType(Consumer consumer) { */ public BUILDER parameterArguments(List parameterArguments) { Objects.requireNonNull(parameterArguments); + isParameterArgumentsMutated = true; this.parameterArguments.clear(); this.parameterArguments.addAll(parameterArguments); return self(); @@ -509,6 +572,7 @@ public BUILDER parameterArguments(List parameterArgu */ public BUILDER addParameterArguments(List parameterArguments) { Objects.requireNonNull(parameterArguments); + isParameterArgumentsMutated = true; this.parameterArguments.addAll(parameterArguments); return self(); } @@ -525,6 +589,7 @@ public BUILDER addParameterArguments(List parameterA public BUILDER addParameterArgument(TypedElementInfo parameterArgument) { Objects.requireNonNull(parameterArgument); this.parameterArguments.add(parameterArgument); + isParameterArgumentsMutated = true; return self(); } @@ -609,6 +674,7 @@ public BUILDER originatingElement(Object originatingElement) { */ public BUILDER annotations(List annotations) { Objects.requireNonNull(annotations); + isAnnotationsMutated = true; this.annotations.clear(); this.annotations.addAll(annotations); return self(); @@ -625,6 +691,7 @@ public BUILDER annotations(List annotations) { */ public BUILDER addAnnotations(List annotations) { Objects.requireNonNull(annotations); + isAnnotationsMutated = true; this.annotations.addAll(annotations); return self(); } @@ -641,6 +708,7 @@ public BUILDER addAnnotations(List annotations) { public BUILDER addAnnotation(Annotation annotation) { Objects.requireNonNull(annotation); this.annotations.add(annotation); + isAnnotationsMutated = true; return self(); } @@ -667,6 +735,8 @@ public BUILDER addAnnotation(Consumer consumer) { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotations list of all meta annotations of this element * @return updated builder instance @@ -674,6 +744,7 @@ public BUILDER addAnnotation(Consumer consumer) { */ public BUILDER inheritedAnnotations(List inheritedAnnotations) { Objects.requireNonNull(inheritedAnnotations); + isInheritedAnnotationsMutated = true; this.inheritedAnnotations.clear(); this.inheritedAnnotations.addAll(inheritedAnnotations); return self(); @@ -685,6 +756,8 @@ public BUILDER inheritedAnnotations(List inheritedAnnotati *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotations list of all meta annotations of this element * @return updated builder instance @@ -692,6 +765,7 @@ public BUILDER inheritedAnnotations(List inheritedAnnotati */ public BUILDER addInheritedAnnotations(List inheritedAnnotations) { Objects.requireNonNull(inheritedAnnotations); + isInheritedAnnotationsMutated = true; this.inheritedAnnotations.addAll(inheritedAnnotations); return self(); } @@ -702,6 +776,8 @@ public BUILDER addInheritedAnnotations(List inheritedAnnot *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @param inheritedAnnotation list of all meta annotations of this element * @return updated builder instance @@ -710,6 +786,7 @@ public BUILDER addInheritedAnnotations(List inheritedAnnot public BUILDER addInheritedAnnotation(Annotation inheritedAnnotation) { Objects.requireNonNull(inheritedAnnotation); this.inheritedAnnotations.add(inheritedAnnotation); + isInheritedAnnotationsMutated = true; return self(); } @@ -719,6 +796,8 @@ public BUILDER addInheritedAnnotation(Annotation inheritedAnnotation) { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @param consumer list of all meta annotations of this element * @return updated builder instance @@ -794,7 +873,7 @@ public Optional defaultValue() { } /** - * The list of known annotations on the type name referenced by {@link #typeName()}. + * The list of known annotations on the type name referenced by {@link io.helidon.common.types.TypedElementInfo#typeName()}. * * @return the element type annotations */ @@ -888,6 +967,17 @@ public Optional originatingElement() { return Optional.ofNullable(originatingElement); } + /** + * Signature of this element. + * + * @return the signature + * @see io.helidon.common.types.ElementSignature + * @see #signature() + */ + public Optional signature() { + return Optional.ofNullable(signature); + } + /** * List of declared and known annotations for this element. * Note that "known" implies that the annotation is visible, which depends @@ -905,6 +995,8 @@ public List annotations() { *

* The returned list does not contain {@link #annotations()}. If a meta-annotation is present on multiple * annotations, it will be returned once for each such declaration. + *

+ * This method does not return annotations on super types or interfaces! * * @return the inherited annotations */ @@ -939,6 +1031,9 @@ protected void validatePrototype() { if (accessModifier == null) { collector.fatal(getClass(), "Property \"accessModifier\" must not be null, but not set"); } + if (signature == null) { + collector.fatal(getClass(), "Property \"signature\" must not be null, but not set"); + } collector.collect().checkValid(); } @@ -999,6 +1094,20 @@ BUILDER originatingElement(Optional originatingElement) { return self(); } + /** + * Signature of this element. + * + * @param signature signature of this element + * @return updated builder instance + * @see io.helidon.common.types.ElementSignature + * @see #signature() + */ + BUILDER signature(ElementSignature signature) { + Objects.requireNonNull(signature); + this.signature = signature; + return self(); + } + /** * Generated implementation of the prototype, can be extended by descendant prototype implementations. */ @@ -1006,6 +1115,7 @@ protected static class TypedElementInfoImpl implements TypedElementInfo { private final AccessModifier accessModifier; private final ElementKind kind; + private final ElementSignature signature; private final List annotations; private final List elementTypeAnnotations; private final List inheritedAnnotations; @@ -1043,6 +1153,7 @@ protected TypedElementInfoImpl(TypedElementInfo.BuilderBase builder) { this.parameterArguments = List.copyOf(builder.parameterArguments()); this.throwsChecked = Collections.unmodifiableSet(new LinkedHashSet<>(builder.throwsChecked())); this.originatingElement = builder.originatingElement(); + this.signature = builder.signature().get(); this.annotations = List.copyOf(builder.annotations()); this.inheritedAnnotations = List.copyOf(builder.inheritedAnnotations()); } @@ -1132,6 +1243,11 @@ public Optional originatingElement() { return originatingElement; } + @Override + public ElementSignature signature() { + return signature; + } + @Override public List annotations() { return annotations; @@ -1156,6 +1272,7 @@ public boolean equals(Object o) { && Objects.equals(enclosingType, other.enclosingType()) && Objects.equals(parameterArguments, other.parameterArguments()) && Objects.equals(throwsChecked, other.throwsChecked()) + && Objects.equals(signature, other.signature()) && Objects.equals(annotations, other.annotations()) && Objects.equals(inheritedAnnotations, other.inheritedAnnotations()); } @@ -1168,6 +1285,7 @@ public int hashCode() { enclosingType, parameterArguments, throwsChecked, + signature, annotations, inheritedAnnotations); } diff --git a/common/types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java b/common/types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java index b52ba88a4bd..3de312eb13c 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java +++ b/common/types/src/main/java/io/helidon/common/types/TypedElementInfoBlueprint.java @@ -167,4 +167,25 @@ interface TypedElementInfoBlueprint extends Annotated { */ @Option.Redundant Optional originatingElement(); + + /** + * The element used to create this instance, or {@link io.helidon.common.types.TypedElementInfo#signature()} + * if none provided. + * The type of the object depends on the environment we are in - it may be an {@code TypeElement} in annotation processing, + * or a {@code MethodInfo} (and such) when using classpath scanning. + * + * @return originating element, or the signature of this element + */ + default Object originatingElementValue() { + return originatingElement().orElseGet(this::signature); + } + + /** + * Signature of this element. + * + * @return signature of this element + * @see io.helidon.common.types.ElementSignature + */ + @Option.Access("") + ElementSignature signature(); } diff --git a/common/types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java b/common/types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java index 9edf23abd11..0cba7edaff8 100644 --- a/common/types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java +++ b/common/types/src/main/java/io/helidon/common/types/TypedElementInfoSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or 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.types; +import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.stream.Collectors; @@ -60,10 +61,67 @@ static class BuilderDecorator implements Prototype.BuilderDecorator target) { -/* + backwardCompatibility(target); + constructorName(target); + signature(target); + } + + private void signature(TypedElementInfo.BuilderBase target) { + if (target.kind().isEmpty()) { + // this will fail when validating + target.signature(ElementSignatures.createNone()); + return; + } else { + target.signature(signature(target, target.kind().get())); + } + } + + private ElementSignature signature(TypedElementInfo.BuilderBase target, ElementKind elementKind) { + if (elementKind == ElementKind.CONSTRUCTOR) { + return ElementSignatures.createConstructor(toTypes(target.parameterArguments())); + } + // for everything else we need the type (it is required) + if (target.typeName().isEmpty() || target.elementName().isEmpty()) { + return ElementSignatures.createNone(); + } + + TypeName typeName = target.typeName().get(); + String name = target.elementName().get(); + + if (elementKind == ElementKind.FIELD + || elementKind == ElementKind.RECORD_COMPONENT + || elementKind == ElementKind.ENUM_CONSTANT) { + return ElementSignatures.createField(typeName, name); + } + if (elementKind == ElementKind.METHOD) { + return ElementSignatures.createMethod(typeName, name, toTypes(target.parameterArguments())); + } + if (elementKind == ElementKind.PARAMETER) { + return ElementSignatures.createParameter(typeName, name); + } + return ElementSignatures.createNone(); + } + + private List toTypes(List typedElementInfos) { + return typedElementInfos.stream() + .map(TypedElementInfo::typeName) + .collect(Collectors.toUnmodifiableList()); + } + + private void constructorName(TypedElementInfo.BuilderBase target) { + Optional elementKind = target.kind(); + if (elementKind.isPresent()) { + if (elementKind.get() == ElementKind.CONSTRUCTOR) { + target.elementName(""); + } + } + } + + @SuppressWarnings("removal") + private void backwardCompatibility(TypedElementInfo.BuilderBase target) { + /* Backward compatibility for deprecated methods. */ if (target.kind().isEmpty() && target.elementTypeKind().isPresent()) { @@ -103,14 +161,6 @@ public void decorate(TypedElementInfo.BuilderBase target) { target.addModifier(typeModifier.modifierName()); } target.addModifier(target.accessModifier().get().modifierName()); - - - Optional elementKind = target.kind(); - if (elementKind.isPresent()) { - if (elementKind.get() == ElementKind.CONSTRUCTOR) { - target.elementName(""); - } - } } } } diff --git a/common/types/src/test/java/io/helidon/common/types/SignatureTest.java b/common/types/src/test/java/io/helidon/common/types/SignatureTest.java new file mode 100644 index 00000000000..2dce94997b9 --- /dev/null +++ b/common/types/src/test/java/io/helidon/common/types/SignatureTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2024 Oracle and/or 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.types; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +class SignatureTest { + + @Test + void testMethodSignature() { + TypedElementInfo m1 = TypedElementInfo.builder() + .kind(ElementKind.METHOD) + .elementName("method") + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m2 = TypedElementInfo.builder() + .kind(ElementKind.METHOD) + .elementName("method") + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m3 = TypedElementInfo.builder() + .kind(ElementKind.METHOD) + .elementName("method2") + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m4 = TypedElementInfo.builder() + .kind(ElementKind.METHOD) + .elementName("method") + .typeName(TypeNames.STRING) + .parameterArguments(List.of(TypedElementInfo.builder() + .kind(ElementKind.PARAMETER) + .typeName(TypeNames.STRING) + .elementName("param1") + .buildPrototype())) + .build(); + + ElementSignature s1 = m1.signature(); + ElementSignature s2 = m2.signature(); + ElementSignature s3 = m3.signature(); + ElementSignature s4 = m4.signature(); + + // this is specified in Javadoc and must not be changed + assertThat(s1.text(), is("method()")); + assertThat(s2.text(), is("method()")); + assertThat(s3.text(), is("method2()")); + assertThat(s4.text(), is("method(java.lang.String)")); + + assertThat(s1, is(s2)); + assertThat(s1, not(s3)); + assertThat(s1, not(s4)); + + assertThat(s1.hashCode(), is(s2.hashCode())); + + assertThat(s1.name(), is("method")); + assertThat(s1.type(), is(TypeNames.STRING)); + assertThat(s1.parameterTypes(), is(List.of())); + } + + @Test + void testConstructorSignature() { + TypedElementInfo m1 = TypedElementInfo.builder() + .kind(ElementKind.CONSTRUCTOR) + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m2 = TypedElementInfo.builder() + .kind(ElementKind.CONSTRUCTOR) + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m3 = TypedElementInfo.builder() + .kind(ElementKind.CONSTRUCTOR) + .typeName(TypeNames.STRING) + .parameterArguments(List.of(TypedElementInfo.builder() + .kind(ElementKind.PARAMETER) + .typeName(TypeNames.STRING) + .elementName("param1") + .buildPrototype())) + .build(); + + ElementSignature s1 = m1.signature(); + ElementSignature s2 = m2.signature(); + ElementSignature s3 = m3.signature(); + + // this is specified in Javadoc and must not be changed + assertThat(s1.text(), is("()")); + assertThat(s2.text(), is("()")); + assertThat(s3.text(), is("(java.lang.String)")); + + assertThat(s1, is(s2)); + assertThat(s1, not(s3)); + + assertThat(s1.hashCode(), is(s2.hashCode())); + + assertThat(s1.name(), is("")); + assertThat(s1.type(), is(TypeNames.PRIMITIVE_VOID)); + assertThat(s1.parameterTypes(), is(List.of())); + } + + @Test + void testFieldSignature() { + TypedElementInfo m1 = TypedElementInfo.builder() + .kind(ElementKind.FIELD) + .elementName("field") + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m2 = TypedElementInfo.builder() + .kind(ElementKind.FIELD) + .elementName("field") + .typeName(TypeNames.STRING) + .build(); + + TypedElementInfo m3 = TypedElementInfo.builder() + .kind(ElementKind.FIELD) + .elementName("field2") + .typeName(TypeNames.STRING) + .build(); + + ElementSignature s1 = m1.signature(); + ElementSignature s2 = m2.signature(); + ElementSignature s3 = m3.signature(); + + // this is specified in Javadoc and must not be changed + assertThat(s1.text(), is("field")); + assertThat(s2.text(), is("field")); + assertThat(s3.text(), is("field2")); + + assertThat(s1, is(s2)); + assertThat(s1, not(s3)); + + assertThat(s1.hashCode(), is(s2.hashCode())); + + assertThat(s1.name(), is("field")); + assertThat(s1.type(), is(TypeNames.STRING)); + assertThat(s1.parameterTypes(), is(List.of())); + } +} diff --git a/common/types/src/test/java/io/helidon/common/types/TypeInfoTest.java b/common/types/src/test/java/io/helidon/common/types/TypeInfoTest.java new file mode 100644 index 00000000000..f00f182dc2b --- /dev/null +++ b/common/types/src/test/java/io/helidon/common/types/TypeInfoTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024 Oracle and/or 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.types; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; + +class TypeInfoTest { + @Test + void testFindInHierarchyInterfaces() { + TypeName ifaceA = TypeName.create("io.helidon.common.types.test.A"); + TypeName ifaceB = TypeName.create("io.helidon.common.types.test.B"); + TypeName ifaceC = TypeName.create("io.helidon.common.types.test.C"); + TypeInfo aInfo = TypeInfo.builder() + .typeName(ifaceA) + .kind(ElementKind.INTERFACE) + .build(); + TypeInfo bInfo = TypeInfo.builder() + .typeName(ifaceB) + .kind(ElementKind.INTERFACE) + .addInterfaceTypeInfo(aInfo) + .build(); + TypeInfo cInfo = TypeInfo.builder() + .typeName(ifaceC) + .kind(ElementKind.INTERFACE) + .addInterfaceTypeInfo(bInfo) + .build(); + + Optional foundInfo = cInfo.findInHierarchy(ifaceA); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(aInfo)); + + foundInfo = cInfo.findInHierarchy(ifaceB); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(bInfo)); + + foundInfo = bInfo.findInHierarchy(ifaceA); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(aInfo)); + + foundInfo = aInfo.findInHierarchy(ifaceB); + assertThat(foundInfo, is(Optional.empty())); + } + + @Test + void testFindInHierarchyTypes() { + TypeName ifaceA = TypeName.create("io.helidon.common.types.test.A"); + TypeName classB = TypeName.create("io.helidon.common.types.test.B"); + TypeName classC = TypeName.create("io.helidon.common.types.test.C"); + TypeInfo aInfo = TypeInfo.builder() + .typeName(ifaceA) + .kind(ElementKind.INTERFACE) + .build(); + TypeInfo bInfo = TypeInfo.builder() + .typeName(classB) + .kind(ElementKind.CLASS) + .addInterfaceTypeInfo(aInfo) + .build(); + TypeInfo cInfo = TypeInfo.builder() + .typeName(classC) + .kind(ElementKind.INTERFACE) + .superTypeInfo(bInfo) + .build(); + + Optional foundInfo = cInfo.findInHierarchy(ifaceA); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(aInfo)); + + foundInfo = cInfo.findInHierarchy(classB); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(bInfo)); + + foundInfo = bInfo.findInHierarchy(ifaceA); + assertThat(foundInfo, not(Optional.empty())); + assertThat(foundInfo.get(), sameInstance(aInfo)); + + foundInfo = aInfo.findInHierarchy(classB); + assertThat(foundInfo, is(Optional.empty())); + } + +} diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigImpl.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigImpl.java index 8299a1c1cc5..d35fdb91b16 100644 --- a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigImpl.java +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigImpl.java @@ -30,6 +30,7 @@ import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; @@ -452,7 +453,11 @@ private Optional> findImplicit(Class type) { if (Enum.class.isAssignableFrom(type)) { return Optional.of(value -> { Class enumClass = (Class) type; - return (T) Enum.valueOf(enumClass, value); + try { + return (T) Enum.valueOf(enumClass, value); + } catch (Exception e) { + return (T) Enum.valueOf(enumClass, value.toUpperCase(Locale.ROOT)); + } }); } // any class that has a "public static T method()" diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigProviderResolver.java b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigProviderResolver.java index 9ecd670227d..4b6f57efdc1 100644 --- a/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigProviderResolver.java +++ b/config/config-mp/src/main/java/io/helidon/config/mp/MpConfigProviderResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or 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,6 +32,7 @@ import java.util.stream.Stream; import io.helidon.common.GenericType; +import io.helidon.common.config.GlobalConfig; import io.helidon.config.ConfigValue; import io.helidon.config.MetaConfig; import io.helidon.config.spi.ConfigMapper; @@ -167,8 +168,8 @@ public static void buildTimeEnd() { private ConfigDelegate doRegisterConfig(Config config, ClassLoader classLoader) { ConfigDelegate currentConfig = CONFIGS.remove(classLoader); - if (config instanceof ConfigDelegate) { - config = ((ConfigDelegate) config).delegate(); + if (config instanceof ConfigDelegate delegate) { + config = delegate.delegate(); } if (null != currentConfig) { @@ -178,6 +179,11 @@ private ConfigDelegate doRegisterConfig(Config config, ClassLoader classLoader) ConfigDelegate newConfig = new ConfigDelegate(config); CONFIGS.put(classLoader, newConfig); + if (classLoader == Thread.currentThread().getContextClassLoader()) { + // this should be the default class loader (we do not support classloader magic in Helidon) + GlobalConfig.config(() -> newConfig, true); + } + return newConfig; } diff --git a/config/config-mp/src/main/java/io/helidon/config/mp/SeConfig.java b/config/config-mp/src/main/java/io/helidon/config/mp/SeConfig.java index 514830dab44..5ec1c8d3e43 100644 --- a/config/config-mp/src/main/java/io/helidon/config/mp/SeConfig.java +++ b/config/config-mp/src/main/java/io/helidon/config/mp/SeConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -84,8 +84,8 @@ class SeConfig implements Config { this.stringKey = prefix.child(key).toString(); this.stringPrefix = prefix.toString(); - if (delegate instanceof MpConfigImpl) { - this.delegateImpl = (MpConfigImpl) delegate; + if (delegate instanceof MpConfigImpl mpConfig) { + this.delegateImpl = mpConfig; } else { this.delegateImpl = null; } diff --git a/config/config/src/main/java/io/helidon/config/BuilderImpl.java b/config/config/src/main/java/io/helidon/config/BuilderImpl.java index 28e60577289..a1bd6f09681 100644 --- a/config/config/src/main/java/io/helidon/config/BuilderImpl.java +++ b/config/config/src/main/java/io/helidon/config/BuilderImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023 Oracle and/or its affiliates. + * Copyright (c) 2017, 2024 Oracle and/or 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,6 +61,7 @@ class BuilderImpl implements Config.Builder { private MergingStrategy mergingStrategy = MergingStrategy.fallback(); private boolean hasSystemPropertiesSource; private boolean hasEnvVarSource; + private boolean sourcesConfigured; /* * Config mapper providers */ @@ -123,6 +124,8 @@ public Config.Builder sources(List> sourceSuppl sourceSuppliers.stream() .map(Supplier::get) .forEach(this::addSource); + // this was intentional, even if empty (such as from Config.just()) + this.sourcesConfigured = true; return this; } @@ -427,14 +430,14 @@ private ConfigSourcesRuntime buildConfigSources(ConfigContextImpl context) { envVarAliasGeneratorEnabled = true; } - boolean nothingConfigured = sources.isEmpty(); + boolean nothingConfigured = sources.isEmpty() && !sourcesConfigured; if (nothingConfigured) { // use meta configuration to load all sources - MetaConfig.configSources(mediaType -> context.findParser(mediaType).isPresent(), context.supportedSuffixes()) - .stream() + MetaConfigFinder.findConfigSource(mediaType -> context.findParser(mediaType).isPresent(), + context.supportedSuffixes()) .map(context::sourceRuntimeBase) - .forEach(targetSources::add); + .ifPresent(targetSources::add); } else { // add all configured or discovered sources @@ -702,56 +705,6 @@ public String toString() { } } - private static final class WeightedConfigSource implements Weighted { - private final HelidonSourceWithPriority source; - private final ConfigContext context; - - private WeightedConfigSource(HelidonSourceWithPriority source, ConfigContext context) { - this.source = source; - this.context = context; - } - - @Override - public double weight() { - return source.weight(context); - } - - private ConfigSourceRuntimeImpl runtime(ConfigContextImpl context) { - return context.sourceRuntimeBase(source.unwrap()); - } - } - - private static final class HelidonSourceWithPriority { - private final ConfigSource configSource; - private final Double explicitWeight; - - private HelidonSourceWithPriority(ConfigSource configSource, Double explicitWeight) { - this.configSource = configSource; - this.explicitWeight = explicitWeight; - } - - ConfigSource unwrap() { - return configSource; - } - - double weight(ConfigContext context) { - // first - explicit priority. If configured by user, return it - if (null != explicitWeight) { - return explicitWeight; - } - - // ordinal from data - return context.sourceRuntime(configSource) - .node("config_priority") - .flatMap(node -> node.value() - .map(Double::parseDouble)) - .orElseGet(() -> { - // the config source does not have an ordinal configured, I need to get it from other places - return Weights.find(configSource, Weighted.DEFAULT_WEIGHT); - }); - } - } - private static class LoadedFilterProvider implements Function { private final ConfigFilter filter; diff --git a/config/config/src/main/java/io/helidon/config/Config.java b/config/config/src/main/java/io/helidon/config/Config.java index f34c4a88d38..bf59b50f5c3 100644 --- a/config/config/src/main/java/io/helidon/config/Config.java +++ b/config/config/src/main/java/io/helidon/config/Config.java @@ -1635,8 +1635,14 @@ default Builder sources(Supplier configSource, * @see #config(Config) */ default Builder metaConfig() { - MetaConfig.metaConfig() - .ifPresent(this::config); + try { + MetaConfig.metaConfig() + .ifPresent(this::config); + } catch (Exception e) { + System.getLogger(getClass().getName()) + .log(System.Logger.Level.WARNING, "Failed to load SE meta-configuration," + + " please make sure it has correct format.", e); + } return this; } diff --git a/config/config/src/main/java/io/helidon/config/ConfigDiff.java b/config/config/src/main/java/io/helidon/config/ConfigDiff.java index 13126dbf629..156c5652121 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigDiff.java +++ b/config/config/src/main/java/io/helidon/config/ConfigDiff.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2020 Oracle and/or its affiliates. + * Copyright (c) 2017, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,8 +86,8 @@ private static boolean notEqual(Config left, Config right) { } private static Optional value(Config node) { - if (node instanceof AbstractConfigImpl) { - return ((AbstractConfigImpl) node).value(); + if (node instanceof AbstractConfigImpl abstractConfig) { + return abstractConfig.value(); } return node.asString().asOptional(); } diff --git a/config/config/src/main/java/io/helidon/config/ConfigKeyImpl.java b/config/config/src/main/java/io/helidon/config/ConfigKeyImpl.java index f920e4f8883..30e36d84dd2 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigKeyImpl.java +++ b/config/config/src/main/java/io/helidon/config/ConfigKeyImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -109,8 +109,8 @@ ConfigKeyImpl child(String key) { @Override public ConfigKeyImpl child(io.helidon.common.config.Config.Key key) { final List path; - if (key instanceof ConfigKeyImpl) { - path = ((ConfigKeyImpl) key).path; + if (key instanceof ConfigKeyImpl configKey) { + path = configKey.path; } else { path = new LinkedList<>(); while (!key.isRoot()) { diff --git a/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeImpl.java b/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeImpl.java index 7c91359737f..f95c91bfbeb 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeImpl.java +++ b/config/config/src/main/java/io/helidon/config/ConfigSourceRuntimeImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,16 +83,15 @@ class ConfigSourceRuntimeImpl implements ConfigSourceRuntime { // content source AtomicReference lastStamp = new AtomicReference<>(); - if (configSource instanceof ParsableSource) { + if (configSource instanceof ParsableSource parsableSource) { // eager parsable config source - reloader = new ParsableConfigSourceReloader(configContext, (ParsableSource) source, lastStamp); + reloader = new ParsableConfigSourceReloader(configContext, parsableSource, lastStamp); singleNodeFunction = objectNodeToSingleNode(); - } else if (configSource instanceof NodeConfigSource) { + } else if (configSource instanceof NodeConfigSource nodeConfigSource) { // eager node config source - reloader = new NodeConfigSourceReloader((NodeConfigSource) source, lastStamp); + reloader = new NodeConfigSourceReloader(nodeConfigSource, lastStamp); singleNodeFunction = objectNodeToSingleNode(); - } else if (configSource instanceof LazyConfigSource) { - LazyConfigSource lazySource = (LazyConfigSource) source; + } else if (configSource instanceof LazyConfigSource lazySource) { // lazy config source reloader = Optional::empty; singleNodeFunction = lazySource::node; @@ -143,8 +142,7 @@ class ConfigSourceRuntimeImpl implements ConfigSourceRuntime { } } - if (!changesSupported && (configSource instanceof EventConfigSource)) { - EventConfigSource event = (EventConfigSource) source; + if (!changesSupported && (configSource instanceof EventConfigSource event)) { changesSupported = true; changesRunnable = () -> event.onChange((key, config) -> listeners.forEach(it -> it.accept(key, config))); } @@ -222,8 +220,8 @@ private synchronized void initialLoad() { } // we may have media type mapping per node configured as well - if (configSource instanceof AbstractConfigSource) { - loadedData = loadedData.map(it -> ((AbstractConfigSource) configSource) + if (configSource instanceof AbstractConfigSource abstractConfigSource) { + loadedData = loadedData.map(it -> abstractConfigSource .processNodeMapping(configContext::findParser, ConfigKeyImpl.of(), it)); } diff --git a/config/config/src/main/java/io/helidon/config/MetaConfig.java b/config/config/src/main/java/io/helidon/config/MetaConfig.java index afbe1b9c502..f78a7df2edb 100644 --- a/config/config/src/main/java/io/helidon/config/MetaConfig.java +++ b/config/config/src/main/java/io/helidon/config/MetaConfig.java @@ -22,7 +22,6 @@ import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; -import java.util.function.Function; import io.helidon.common.HelidonServiceLoader; import io.helidon.common.media.type.MediaType; @@ -241,18 +240,6 @@ static List configSources(Config metaConfig) { return configSources; } - // only interested in config source - static List configSources(Function supportedMediaType, List supportedSuffixes) { - Optional metaConfigOpt = metaConfig(); - - return metaConfigOpt - .map(MetaConfig::configSources) - .orElseGet(() -> MetaConfigFinder.findConfigSource(supportedMediaType, supportedSuffixes) - .map(List::of) - .orElseGet(List::of)); - - } - private static Config createDefault() { // use defaults Config.Builder builder = Config.builder(); diff --git a/config/config/src/main/java/io/helidon/config/UrlConfigSource.java b/config/config/src/main/java/io/helidon/config/UrlConfigSource.java index 24ebeda0c94..c936442e014 100644 --- a/config/config/src/main/java/io/helidon/config/UrlConfigSource.java +++ b/config/config/src/main/java/io/helidon/config/UrlConfigSource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -139,8 +139,8 @@ public Optional load() throws ConfigException { try { URLConnection urlConnection = url.openConnection(); - if (urlConnection instanceof HttpURLConnection) { - return httpContent((HttpURLConnection) urlConnection); + if (urlConnection instanceof HttpURLConnection httpURLConnection) { + return httpContent(httpURLConnection); } else { return genericContent(urlConnection); } diff --git a/config/config/src/main/java/io/helidon/config/UrlHelper.java b/config/config/src/main/java/io/helidon/config/UrlHelper.java index 1267390e555..673c4564a32 100644 --- a/config/config/src/main/java/io/helidon/config/UrlHelper.java +++ b/config/config/src/main/java/io/helidon/config/UrlHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2024 Oracle and/or 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,8 +44,7 @@ static Optional dataStamp(URL url) { // the URL may not be an HTTP URL try { URLConnection urlConnection = url.openConnection(); - if (urlConnection instanceof HttpURLConnection) { - HttpURLConnection connection = (HttpURLConnection) urlConnection; + if (urlConnection instanceof HttpURLConnection connection) { try { connection.setRequestMethod(HEAD_METHOD); if (STATUS_NOT_FOUND == connection.getResponseCode()) { diff --git a/config/config/src/main/resources/META-INF/helidon/service.loader b/config/config/src/main/resources/META-INF/helidon/service.loader index d07172ed9b3..c951c168384 100644 --- a/config/config/src/main/resources/META-INF/helidon/service.loader +++ b/config/config/src/main/resources/META-INF/helidon/service.loader @@ -1,4 +1,6 @@ # List of service contracts we want to support either from service registry, or from service loader io.helidon.config.spi.ConfigParser io.helidon.config.spi.ConfigFilter -io.helidon.config.spi.ConfigMapperProvider +# This cannot be done for now, as ObjectConfigMapper ends up before built-ins when +# we disable mapper services +# io.helidon.config.spi.ConfigMapperProvider diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenExtension.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenExtension.java index 482d7bbb3d0..47e4bf2f96b 100644 --- a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenExtension.java +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenExtension.java @@ -109,10 +109,12 @@ private void storeMetadata() { .build()); } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (PrintWriter w = new PrintWriter(baos, true, StandardCharsets.UTF_8)) { - Hson.Array.create(root).write(w); + if (!root.isEmpty()) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (PrintWriter w = new PrintWriter(baos, true, StandardCharsets.UTF_8)) { + Hson.Array.create(root).write(w); + } + ctx.filer().writeResource(baos.toByteArray(), META_FILE); } - ctx.filer().writeResource(baos.toByteArray(), META_FILE); } } diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerBuilderApi.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerBuilderApi.java index 11b94b43380..afc3b65a1fa 100644 --- a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerBuilderApi.java +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerBuilderApi.java @@ -157,14 +157,14 @@ private OptionType typeForBlueprintFromSignature(TypedElementInfo element, if (!ElementInfoPredicates.hasNoArgs(element)) { throw new CodegenException("Method " + element + " is annotated with @Configured, " + "yet it has a parameter. Interface methods must not have parameters.", - element.originatingElement().orElse(element.elementName())); + element.originatingElementValue()); } TypeName returnType = element.typeName(); if (ElementInfoPredicates.isVoid(element)) { throw new CodegenException("Method " + element + " is annotated with @Configured, " + "yet it is void. Interface methods must return the property type.", - element.originatingElement().orElse(element.elementName())); + element.originatingElementValue()); } if (returnType.isOptional()) { diff --git a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerMetaApi.java b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerMetaApi.java index aaf0587ae7c..70b1ef5ce7c 100644 --- a/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerMetaApi.java +++ b/config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerMetaApi.java @@ -217,7 +217,7 @@ private void processTargetType(TypeInfo typeInfo, ConfiguredType type, TypeName throw new CodegenException("Type " + typeName.fqName() + " is marked with @Configured" + ", yet it has a static builder() method. Please mark the builder instead " + "of this class.", - typeInfo.originatingElement().orElseGet(typeInfo::typeName)); + typeInfo.originatingElementValue()); } } @@ -243,14 +243,14 @@ private void processTargetType(TypeInfo typeInfo, ConfiguredType type, TypeName + validMethod + " does not have value defined. It is mandatory on non-builder " + "methods", - typeInfo.originatingElement().orElseGet(typeInfo::typeName)); + typeInfo.originatingElementValue()); } if (data.description() == null || data.description().isBlank()) { throw new CodegenException("ConfiguredOption on " + typeName.fqName() + "." + validMethod + " does not have description defined. It is mandatory on non-builder " + "methods", - typeInfo.originatingElement().orElseGet(typeInfo::typeName)); + typeInfo.originatingElementValue()); } if (data.type() == null) { @@ -281,7 +281,7 @@ private void processTargetType(TypeInfo typeInfo, ConfiguredType type, TypeName throw new CodegenException("Type " + typeName.fqName() + " is marked as standalone configuration unit, " + "yet it does have " + "neither a builder method, nor a create method", - typeInfo.originatingElement().orElseGet(typeInfo::typeName)); + typeInfo.originatingElementValue()); } typeInfo.elementInfo() @@ -342,7 +342,7 @@ private OptionType optionType(TypedElementInfo elementInfo, ConfiguredOptionData throw new CodegenException("Method " + elementInfo.elementName() + " is annotated with @ConfiguredOption, " + "yet it does not have explicit type, or exactly one parameter", - typeInfo.originatingElement().orElseGet(typeInfo::typeName)); + typeInfo.originatingElementValue()); } else { TypedElementInfo parameter = parameters.iterator().next(); TypeName paramType = parameter.typeName(); diff --git a/config/tests/pom.xml b/config/tests/pom.xml index 28d007e3c1f..870b7571945 100644 --- a/config/tests/pom.xml +++ b/config/tests/pom.xml @@ -64,5 +64,8 @@ config-metadata-meta-api config-metadata-builder-api test-lazy-source + test-no-config-sources + test-default-config-source + test-mp-se-meta diff --git a/config/tests/test-default-config-source/pom.xml b/config/tests/test-default-config-source/pom.xml new file mode 100644 index 00000000000..f5357e2b230 --- /dev/null +++ b/config/tests/test-default-config-source/pom.xml @@ -0,0 +1,63 @@ + + + + + 4.0.0 + + io.helidon.config.tests + helidon-config-tests-project + 4.1.0-SNAPSHOT + ../pom.xml + + helidon-test-default-config-source + Helidon Config Tests Default Config Source + + + Test that when no config sources are configured, we do not fall to meta config, but we want to + use default config sources + + + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-yaml + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + diff --git a/config/tests/test-default-config-source/src/main/resources/application.yaml b/config/tests/test-default-config-source/src/main/resources/application.yaml new file mode 100644 index 00000000000..cf3e2c3af54 --- /dev/null +++ b/config/tests/test-default-config-source/src/main/resources/application.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2024 Oracle and/or 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. +# + +value: "from-file" diff --git a/config/tests/test-default-config-source/src/main/resources/meta-config.yaml b/config/tests/test-default-config-source/src/main/resources/meta-config.yaml new file mode 100644 index 00000000000..01468e56507 --- /dev/null +++ b/config/tests/test-default-config-source/src/main/resources/meta-config.yaml @@ -0,0 +1,21 @@ +# +# Copyright (c) 2024 Oracle and/or 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. +# + +# MP style, which would normally fail in Helidon SE, but should be ignored, as we do not specify meta config +add-default-sources: true +sources: + - type: "properties" + classpath: "app.properties" \ No newline at end of file diff --git a/config/tests/test-default-config-source/src/test/java/io/helidon/config/tests/nosources/DefaultSourceTest.java b/config/tests/test-default-config-source/src/test/java/io/helidon/config/tests/nosources/DefaultSourceTest.java new file mode 100644 index 00000000000..6775f4a3211 --- /dev/null +++ b/config/tests/test-default-config-source/src/test/java/io/helidon/config/tests/nosources/DefaultSourceTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 Oracle and/or 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.config.tests.nosources; + +import java.util.Optional; + +import io.helidon.config.Config; + +import org.junit.jupiter.api.Test; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class DefaultSourceTest { + @Test + public void testDefaultSource() { + Config config = Config.builder() + .disableSystemPropertiesSource() + .disableEnvironmentVariablesSource() + .build(); + + Optional value = config.get("value") + .asString() + .asOptional(); + + // meta config MUST be ignored + assertThat("We defined not sources, we should fall back to default application.yaml", + value, + optionalValue(is("from-file"))); + } +} diff --git a/config/tests/test-mp-se-meta/pom.xml b/config/tests/test-mp-se-meta/pom.xml new file mode 100644 index 00000000000..54c23faa0b1 --- /dev/null +++ b/config/tests/test-mp-se-meta/pom.xml @@ -0,0 +1,67 @@ + + + + + 4.0.0 + + io.helidon.config.tests + helidon-config-tests-project + 4.1.0-SNAPSHOT + ../pom.xml + + helidon-config-tests-mp-se-meta + Helidon Config Tests MP SE Meta + + + Test that when using MP config with meta-config file name that conflicts with Helidon SE, everything + works as expected. + + + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-mp + + + io.helidon.config + helidon-config-yaml + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + diff --git a/config/tests/test-mp-se-meta/src/main/resources/app.properties b/config/tests/test-mp-se-meta/src/main/resources/app.properties new file mode 100644 index 00000000000..763fa6bd7c6 --- /dev/null +++ b/config/tests/test-mp-se-meta/src/main/resources/app.properties @@ -0,0 +1,17 @@ +# +# Copyright (c) 2024 Oracle and/or 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.test.value=value diff --git a/config/tests/test-mp-se-meta/src/main/resources/application.yaml b/config/tests/test-mp-se-meta/src/main/resources/application.yaml new file mode 100644 index 00000000000..496e0c1ed88 --- /dev/null +++ b/config/tests/test-mp-se-meta/src/main/resources/application.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2024 Oracle and/or 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.app.value: "app-value" \ No newline at end of file diff --git a/config/tests/test-mp-se-meta/src/main/resources/meta-config.yaml b/config/tests/test-mp-se-meta/src/main/resources/meta-config.yaml new file mode 100644 index 00000000000..607b6f02e6d --- /dev/null +++ b/config/tests/test-mp-se-meta/src/main/resources/meta-config.yaml @@ -0,0 +1,22 @@ +# +# Copyright (c) 2024 Oracle and/or 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. +# + +# This is an MP meta-config. It should not be named like this, but if the user does it, we should +# honor it +add-default-sources: true +sources: + - type: "properties" + classpath: "app.properties" \ No newline at end of file diff --git a/config/tests/test-mp-se-meta/src/test/java/io/helidon/config/tests/mpsemeta/MpSeMetaTest.java b/config/tests/test-mp-se-meta/src/test/java/io/helidon/config/tests/mpsemeta/MpSeMetaTest.java new file mode 100644 index 00000000000..46ef7958ee4 --- /dev/null +++ b/config/tests/test-mp-se-meta/src/test/java/io/helidon/config/tests/mpsemeta/MpSeMetaTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 Oracle and/or 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.config.tests.mpsemeta; + +import io.helidon.common.config.GlobalConfig; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class MpSeMetaTest { + @Order(0) + @Test + public void testSeMeta() { + // this should not fail + io.helidon.config.Config config = io.helidon.config.Config.create(); + assertThat(config.get("helidon.app.value").asString().asOptional(), + optionalValue(is("app-value"))); + } + + @Order(1) + @Test + public void testMpMeta() { + System.setProperty("io.helidon.config.mp.meta-config", "meta-config.yaml"); + Config config = ConfigProvider.getConfig(); + assertThat(config.getValue("helidon.test.value", String.class), is("value")); + + assertThat(GlobalConfig.config() + .get("helidon.test.value") + .asString() + .asOptional(), optionalValue(is("value"))); + } +} diff --git a/config/tests/test-no-config-sources/pom.xml b/config/tests/test-no-config-sources/pom.xml new file mode 100644 index 00000000000..e07222e3728 --- /dev/null +++ b/config/tests/test-no-config-sources/pom.xml @@ -0,0 +1,63 @@ + + + + + 4.0.0 + + io.helidon.config.tests + helidon-config-tests-project + 4.1.0-SNAPSHOT + ../pom.xml + + helidon-config-tests-no-config-sources + Helidon Config Tests No Config Sources + + + Test that when no config sources are defined (such as when using Config.just()), we do not + fallback to meta configuration. + + + + + io.helidon.config + helidon-config + + + io.helidon.config + helidon-config-yaml + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + diff --git a/config/tests/test-no-config-sources/src/main/resources/application.yaml b/config/tests/test-no-config-sources/src/main/resources/application.yaml new file mode 100644 index 00000000000..cf3e2c3af54 --- /dev/null +++ b/config/tests/test-no-config-sources/src/main/resources/application.yaml @@ -0,0 +1,17 @@ +# +# Copyright (c) 2024 Oracle and/or 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. +# + +value: "from-file" diff --git a/config/tests/test-no-config-sources/src/main/resources/meta-config.yaml b/config/tests/test-no-config-sources/src/main/resources/meta-config.yaml new file mode 100644 index 00000000000..feac5d69543 --- /dev/null +++ b/config/tests/test-no-config-sources/src/main/resources/meta-config.yaml @@ -0,0 +1,20 @@ +# +# Copyright (c) 2024 Oracle and/or 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. +# + +sources: + - type: "inlined" + properties: + value: "from-meta-config" \ No newline at end of file diff --git a/config/tests/test-no-config-sources/src/test/java/io/helidon/config/tests/nosources/NoSourcesTest.java b/config/tests/test-no-config-sources/src/test/java/io/helidon/config/tests/nosources/NoSourcesTest.java new file mode 100644 index 00000000000..f0bf1ff212e --- /dev/null +++ b/config/tests/test-no-config-sources/src/test/java/io/helidon/config/tests/nosources/NoSourcesTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2024 Oracle and/or 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.config.tests.nosources; + +import java.util.List; +import java.util.Optional; + +import io.helidon.config.Config; + +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 org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class NoSourcesTest { + @Test + public void testJust() { + Config config = Config.just(); + + Optional value = config.get("value") + .asString() + .asOptional(); + + assertThat("We have used Config.just(), there should be NO config source", value, is(optionalEmpty())); + } + + @Test + public void testBuilder() { + Config config = Config.builder() + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .sources(List.of()) + .build(); + + Optional value = config.get("value") + .asString() + .asOptional(); + + assertThat("We have used Config.builder() without specifying any source, there should be NO config source", + value, + is(optionalEmpty())); + } + + @Test + public void testMetaConfigExplicit() { + // a sanity check that meta configuration works when requested + + Config config = Config.builder() + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .metaConfig() + .build(); + + Optional value = config.get("value") + .asString() + .asOptional(); + + assertThat("We have used metaConfig(), there should be the inlined config source configured", + value, + optionalValue(is("from-meta-config"))); + } + + @Test + public void testMetaConfigCreate() { + // a sanity check that meta configuration works when requested + + Config config = Config.create(); + + Optional value = config.get("value") + .asString() + .asOptional(); + + assertThat("We have used Config.create(), there should be the inlined config source configured", + value, + optionalValue(is("from-meta-config"))); + } +} diff --git a/docs/src/main/asciidoc/mp/config/advanced-configuration.adoc b/docs/src/main/asciidoc/mp/config/advanced-configuration.adoc index 16d3b1e85cf..af3239ac18d 100644 --- a/docs/src/main/asciidoc/mp/config/advanced-configuration.adoc +++ b/docs/src/main/asciidoc/mp/config/advanced-configuration.adoc @@ -119,7 +119,9 @@ If a file named `mp-meta-config.yaml`, or `mp-meta-config.properties` is in the on the classpath, and there is no explicit setup of configuration in the code, the configuration will be loaded from the `meta-config` file. The location of the file can be overridden using system property `io.helidon.config.mp.meta-config`, -or environment variable `HELIDON_MP_META_CONFIG` +or environment variable `HELIDON_MP_META_CONFIG`. + +*Important Note:* Do not use custom files named `meta-config.*`, as even when using Micro-Profile, we still use Helidon configuration in some of our components, and this file would be recognized as a Helidon SE Meta Configuration file, which may cause erroneous behavior. [source,yaml] .Example of a YAML meta configuration file: diff --git a/etc/scripts/release.sh b/etc/scripts/release.sh index 8b0ac750564..bf7f0632574 100755 --- a/etc/scripts/release.sh +++ b/etc/scripts/release.sh @@ -23,7 +23,7 @@ on_error(){ CODE="${?}" && \ set +x && \ printf "[ERROR] Error(code=%s) occurred at %s:%s command: %s\n" \ - "${CODE}" "${BASH_SOURCE[0]}" "${LINENO}" "${BASH_COMMAND}" >&2 + "${CODE}" "${BASH_SOURCE[0]}" "${LINENO}" "${BASH_COMMAND}" >&2 } trap on_error ERR @@ -99,16 +99,16 @@ readonly COMMAND exec 6>&1 1>&2 if [ -z "${COMMAND+x}" ] ; then - echo "ERROR: no command provided" - exit 1 + echo "ERROR: no command provided" + exit 1 fi case ${COMMAND} in "update_version") if [ -z "${VERSION}" ] ; then - echo "ERROR: version required" >&2 - usage - exit 1 + echo "ERROR: version required" >&2 + usage + exit 1 fi ;; "create_tag"|"get_version") diff --git a/etc/scripts/smoketest.sh b/etc/scripts/smoketest.sh index a64cf6bba96..db49aba72b5 100755 --- a/etc/scripts/smoketest.sh +++ b/etc/scripts/smoketest.sh @@ -15,55 +15,26 @@ # limitations under the License. # -# Smoke test a Helidon release. This assumes: -# 1. The release has a source tag in the Helidon GitHub repo -# 2. The bits are in either the OSS Sonatype Staging Repo or Maven Central -# 3. You have a profile defined as "ossrh-staging" that configures -# https://oss.sonatype.org/content/groups/staging/ as a repository -# See bottom of RELEASE.md for details - set -o pipefail || true # trace ERR through pipes set -o errtrace || true # trace ERR through commands and functions set -o errexit || true # exit the script if any statement returns a non-true return value on_error(){ - CODE="${?}" && \ - set +x && \ - printf "[ERROR] Error(code=%s) occurred at %s:%s command: %s\n" \ - "${CODE}" "${BASH_SOURCE[0]}" "${LINENO}" "${BASH_COMMAND}" + CODE="${?}" && \ + set +x && \ + printf "[ERROR] Error(code=%s) occurred at %s:%s command: %s\n" \ + "${CODE}" "${BASH_SOURCE[0]}" "${LINENO}" "${BASH_COMMAND}" } trap on_error ERR -# Path to this script -if [ -h "${0}" ] ; then - SCRIPT_PATH="$(readlink "${0}")" -else - SCRIPT_PATH="${0}" -fi -readonly SCRIPT_PATH - - -SCRIPT_DIR=$(dirname "${SCRIPT_PATH}") -readonly SCRIPT_DIR - -# Local error handler -smoketest_on_error(){ - on_error - echo "===== Log file: ${OUTPUT_FILE} =====" - # In case there is a process left running -} - -# Setup error handling using local error handler (defined in includes/error_handlers.sh) -error_trap_setup 'smoketest_on_error' - usage(){ cat <&2 + usage exit 1 fi -set -u - -full(){ - echo "===== Full Test =====" - cd "${SCRATCH}" - quick - cd "${SCRATCH}" - - if [[ "${VERSION}" =~ .*SNAPSHOT ]]; then - echo "WARNING! SNAPSHOT version. Skipping tag checkout" - else - echo "===== Cloning Workspace ${GIT_URL} =====" - git clone "${GIT_URL}" - cd "${SCRATCH}/helidon" - echo "===== Checking out tags/${VERSION} =====" - git checkout "tags/${VERSION}" - fi - - echo "===== Building examples =====" - cd "${SCRATCH}/helidon/examples" - # XXX we exclude todo-app frontend due to the issues with npm behind firewall - mvn "${MAVEN_ARGS}" clean install -pl '!todo-app/frontend' ${STAGED_PROFILE} - cd "${SCRATCH}" - - echo "===== Building test support =====" - cd "${SCRATCH}/helidon/microprofile/tests/" - mvn -N "${MAVEN_ARGS}" clean install ${STAGED_PROFILE} - cd "${SCRATCH}/helidon/microprofile/tests/junit5" - mvn "${MAVEN_ARGS}" clean install ${STAGED_PROFILE} - cd "${SCRATCH}/helidon/microprofile/tests/junit5-tests" - mvn "${MAVEN_ARGS}" clean install ${STAGED_PROFILE} - - echo "===== Running tests =====" - cd "${SCRATCH}/helidon/tests" - mvn "${MAVEN_ARGS}" clean install ${STAGED_PROFILE} - - # Primes dependencies for native-image builds - cd "${SCRATCH}/helidon/tests/integration/native-image" - mvn "${MAVEN_ARGS}" clean install ${STAGED_PROFILE} - - echo "===== Running native image tests =====" - if [ -z "${GRAALVM_HOME}" ]; then - echo "WARNING! GRAALVM_HOME is not set. Skipping native image tests" - else - echo "GRAALVM_HOME=${GRAALVM_HOME}" - readonly native_image_tests="mp-1 mp-2 mp-3" - for native_test in ${native_image_tests}; do - cd "${SCRATCH}/helidon/tests/integration/native-image/${native_test}" - mvn "${MAVEN_ARGS}" clean package -Pnative-image ${STAGED_PROFILE} - done - - # Run this one because it has no pre-reqs and self-tests - cd "${SCRATCH}/helidon/tests/integration/native-image/mp-1" - target/helidon-tests-native-image-mp-1 - fi - +PID="" +trap '[ -n "${PID}" ] && kill ${PID} 2> /dev/null || true' 0 + +maven_proxies() { + [ -f "${HOME}/.m2/settings.xml" ] && \ + awk -f- "${HOME}/.m2/settings.xml" </{ + IN_PROXIES="true" + next + } + /<\/proxies>/{ + IN_PROXIES="false" + } + { + if (IN_PROXIES=="true") { + print \$0 + } + } +EOF } -waituntilready() { - # Give app a chance to start --retry will retry until it is up - # --retry-connrefused requires curl 7.51.0 or newer - sleep 6 - #curl -s --retry-connrefused --retry 3 -X GET http://localhost:8080/health/live - curl -s --retry 3 -X GET http://localhost:8080/health/live - echo +maven_settings() { + cat < + + $(maven_proxies) + + + + ossrh-staging + + + ossrh-staging + OSS Sonatype Staging + https://oss.sonatype.org/content/groups/staging/ + + false + + + true + + + + + + ossrh-staging + OSS Sonatype Staging + https://oss.sonatype.org/content/groups/staging/ + + false + + + true + + + + + + +EOF } -testGET() { - echo "GET $1" - http_code=$(curl -s -o /dev/null -w "%{http_code}" -X GET "${1}") - if [ "${http_code}" -ne "200" ]; then - echo "ERROR: Bad HTTP code. Expected 200 got ${http_code}. GET ${1}" - kill "${PID}" - return 1 - fi - return 0 +# arg1: uri +wait_ready() { + sleep 6 + if ! kill -0 "${PID}" 2> /dev/null ; then + echo "ERROR: process not alive" >&2 + return 1 + fi + + case ${1} in + bare-*) + # no-op + ;; + *-mp) + curl -q -s -f \ + --retry 3 \ + -o /dev/null \ + -w "%{http_code} %{url_effective}\n" \ + http://localhost:8080/health/live + ;; + *) + curl -q -s -f \ + --retry 3 \ + -o /dev/null \ + -w "%{http_code} %{url_effective}\n" \ + http://localhost:8080/observe/health/live + ;; + esac } -# -# $1 = archetype name: "quickstart-se" -buildAndTestArchetype(){ - archetype_name=${1} - archetype_pkg=$(echo "${archetype_name}" | tr "\-" "\.") - - echo "===== Testing Archetype ${archetype_name} =====" - - mvn "${MAVEN_ARGS}" -U archetype:generate -DinteractiveMode=false \ - -DarchetypeGroupId=io.helidon.archetypes \ - -DarchetypeArtifactId="helidon-${archetype_name}" \ - -DarchetypeVersion="${VERSION}" \ - -DgroupId=io.helidon.examples \ - -DartifactId=helidon-"${archetype_name}" \ - -Dpackage=io.helidon.examples."${archetype_pkg}" \ - ${STAGED_PROFILE} - - - echo "===== ${archetype_name}: building jar =====" - mvn "${MAVEN_ARGS}" -f helidon-"${archetype_name}"/pom.xml ${STAGED_PROFILE} clean package - - echo "===== Running and pinging ${archetype_name} app using jar =====" - java -jar "helidon-${archetype_name}/target/helidon-${archetype_name}.jar" & - PID=$! - testApp "${archetype_name}" - kill ${PID} - - echo "===== ${archetype_name}: building jlink image =====" - mvn "${MAVEN_ARGS}" -f "helidon-${archetype_name}/pom.xml" ${STAGED_PROFILE} -Pjlink-image package -DskipTests - - echo "===== Running and pinging ${archetype_name} app using jlink image =====" - "helidon-${archetype_name}/target/helidon-${archetype_name}-jri/bin/start" & - PID=$! - testApp "${archetype_name}" - kill ${PID} - sleep 1 +# arg1: url +http_get() { + curl -q -s -f \ + -w "\n%{http_code} %{url_effective}\n" \ + "${1}" } -testApp(){ - # Wait for app to come up - waituntilready - - # Hit some endpoints - if [ "${archetype_name}" = "quickstart-se" ] || [ "${archetype_name}" = "quickstart-mp" ]; then - testGET http://localhost:8080/greet - testGET http://localhost:8080/greet/Joe - fi - testGET http://localhost:8080/health - testGET http://localhost:8080/metrics +# arg1: archetype +test_app(){ + # health & metrics + case ${1} in + bare-*) + # no-op + ;; + *-se) + http_get http://localhost:8080/observe/health + http_get http://localhost:8080/observe/metrics + ;; + *-mp) + http_get http://localhost:8080/health + http_get http://localhost:8080/metrics + ;; + esac + + # app endpoint + case ${1} in + database-*) + # no-op + ;; + bare-se|quickstart-*) + http_get http://localhost:8080/greet + http_get http://localhost:8080/greet/Joe + ;; + bare-mp) + http_get http://localhost:8080/simple-greet + ;; + esac } -quick(){ - readonly archetypes=" - quickstart-se \ - quickstart-mp \ - bare-se \ - bare-mp \ - database-se \ - database-mp \ - " +# arg1: archetype +test_archetype(){ + printf "\n*******************************************" + printf "\nINFO: %s - Generating project" "${ARCHETYPE}" + printf "\n*******************************************\n\n" + + # shellcheck disable=SC2086 + mvn ${MAVEN_ARGS} -U \ + -DinteractiveMode=false \ + -DarchetypeGroupId=io.helidon.archetypes \ + -DarchetypeArtifactId="helidon-${ARCHETYPE}" \ + -DarchetypeVersion="${VERSION}" \ + -DgroupId=io.helidon.smoketest \ + -DartifactId=helidon-"${ARCHETYPE}" \ + -Dpackage=io.helidon.smoketest."${ARCHETYPE/-/.}" \ + archetype:generate + + printf "\n*******************************************" + printf "\nINFO: %s - Building jar" "${ARCHETYPE}" + printf "\n*******************************************\n\n" + + # shellcheck disable=SC2086 + mvn ${MAVEN_ARGS} \ + -f "helidon-${ARCHETYPE}/pom.xml" \ + clean package + + printf "\n*******************************************" + printf "\nINFO: %s - Running and pinging app using jar image" "${ARCHETYPE}" + printf "\n*******************************************\n\n" + + java -jar "helidon-${ARCHETYPE}/target/helidon-${ARCHETYPE}.jar" & + PID=${!} + wait_ready "${ARCHETYPE}" + test_app "${ARCHETYPE}" + kill ${PID} + + printf "\n*******************************************" + printf "\nINFO: %s - Building jlink image" "${ARCHETYPE}" + printf "\n*******************************************\n\n" + + # shellcheck disable=SC2086 + mvn ${MAVEN_ARGS} \ + -f "helidon-${ARCHETYPE}/pom.xml" \ + -DskipTests \ + -Pjlink-image \ + package + + printf "\n*******************************************" + printf "\nINFO: %s - Running and pinging app using jlink image" "${ARCHETYPE}" + printf "\n*******************************************\n\n" + + "helidon-${ARCHETYPE}/target/helidon-${ARCHETYPE}-jri/bin/start" & + PID=${!} + wait_ready "${ARCHETYPE}" + test_app "${ARCHETYPE}" + kill ${PID} +} - echo "===== Quick Test =====" - cd "${SCRATCH}" +WORK_DIR="${TMPDIR:-$(mktemp -d)}/helidon-smoke/${VERSION}-$(date +%Y-%m-%d-%H-%M-%S)" +readonly WORK_DIR - echo "===== Testing Archetypes =====" +LOG_FILE="${WORK_DIR}/test.log" +readonly LOG_FILE - for a in ${archetypes}; do - buildAndTestArchetype "${a}" - done -} +mkdir -p "${WORK_DIR}" -cd "${SCRATCH}" +maven_settings > "${WORK_DIR}/settings.xml" +MAVEN_ARGS="${MAVEN_ARGS} -s ${WORK_DIR}/settings.xml" -OUTPUT_FILE=${SCRATCH}/helidon-smoketest-log.txt -LOCAL_MVN_REPO=$(mvn "${MAVEN_ARGS}" help:evaluate -Dexpression=settings.localRepository | grep -v '\[INFO\]') -readonly OUTPUT_FILE LOCAL_MVN_REPO +exec 1>> >(tee "${LOG_FILE}") +exec 2>> >(tee "${LOG_FILE}") -echo "===== Running in ${SCRATCH} =====" -echo "===== Log file: ${OUTPUT_FILE} =====" +cd "${WORK_DIR}" -if [ -n "${CLEAN_MVN_REPO}" ] && [ -d "${LOCAL_MVN_REPO}" ]; then - echo "===== Cleaning release from local maven repository ${LOCAL_MVN_REPO} =====" - find "${LOCAL_MVN_REPO}/io/helidon" -depth -name "${VERSION}" -type d -exec rm -rf {} \; -fi +printf "\n*******************************************" +printf "\nINFO: Directory - %s" "${WORK_DIR}" +printf "\nINFO: Log - %s" "${LOG_FILE}" +printf "\n*******************************************\n\n" -# Invoke command -${COMMAND} | tee "${OUTPUT_FILE}" +test_archetype "${ARCHETYPE}" -echo "===== Log file: ${OUTPUT_FILE} =====" +printf "\n*******************************************" +printf "\nINFO: Directory - %s" "${WORK_DIR}" +printf "\nINFO: Log - %s" "${LOG_FILE}" +printf "\n*******************************************\n\n" diff --git a/inject/maven-plugin/src/main/java/io/helidon/inject/maven/plugin/QualifierConfig.java b/inject/maven-plugin/src/main/java/io/helidon/inject/maven/plugin/QualifierConfig.java index 6753c140f44..454cdcad0e8 100644 --- a/inject/maven-plugin/src/main/java/io/helidon/inject/maven/plugin/QualifierConfig.java +++ b/inject/maven-plugin/src/main/java/io/helidon/inject/maven/plugin/QualifierConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or 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.inject.maven.plugin; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -76,6 +77,11 @@ public Map values() { return Map.of("value", value); } + @Override + public List metaAnnotations() { + return List.of(); + } + @Override public int compareTo(Annotation o) { return this.typeName().compareTo(o.typeName()); diff --git a/metadata/hson/src/test/java/io/helidon/metadata/hson/ExistingTypesTest.java b/metadata/hson/src/test/java/io/helidon/metadata/hson/ExistingTypesTest.java index 9d5cf77d9fa..357fa7b092c 100644 --- a/metadata/hson/src/test/java/io/helidon/metadata/hson/ExistingTypesTest.java +++ b/metadata/hson/src/test/java/io/helidon/metadata/hson/ExistingTypesTest.java @@ -33,6 +33,16 @@ import static org.hamcrest.collection.IsCollectionWithSize.hasSize; class ExistingTypesTest { + @Test + void testNewServiceRegistry() throws IOException { + Hson.Array modules; + try (InputStream inputStream = resource("/new-service-registry.json")) { + assertThat(inputStream, notNullValue()); + modules = Hson.parse(inputStream) + .asArray(); + } + assertThat(modules, notNullValue()); + } @Test void testServiceRegistry() throws IOException { Hson.Struct object; diff --git a/metadata/hson/src/test/resources/new-service-registry.json b/metadata/hson/src/test/resources/new-service-registry.json new file mode 100644 index 00000000000..de9d76b7038 --- /dev/null +++ b/metadata/hson/src/test/resources/new-service-registry.json @@ -0,0 +1,16 @@ +[ + { + "module": "unnamed/io.helidon.metrics", + "services": [ + { + "type": "inject", + "weight": 90.0, + "descriptor": "io.helidon.metrics.CountedInterceptor__ServiceDescriptor", + "contracts": [ + "io.helidon.metrics.CountedInterceptor", + "io.helidon.service.inject.api.Interception.Interceptor" + ] + } + ] + } +] \ No newline at end of file diff --git a/parent/pom.xml b/parent/pom.xml index d41cbb96500..073609364b3 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -199,7 +199,7 @@ - staging + ossrh-staging ossrh-staging diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/GenerateServiceDescriptor.java b/service/codegen/src/main/java/io/helidon/service/codegen/GenerateServiceDescriptor.java index b5f0349996f..f1b21454c94 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/GenerateServiceDescriptor.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/GenerateServiceDescriptor.java @@ -194,6 +194,8 @@ private ClassModel.Builder generate() { dependenciesMethod(classModel, params, superType); isAbstractMethod(classModel, superType, isAbstractClass); instantiateMethod(classModel, serviceType, params, isAbstractClass); + postConstructMethod(typeInfo, classModel, serviceType); + preDestroyMethod(typeInfo, classModel, serviceType); weightMethod(typeInfo, classModel, superType); // service type is an implicit contract @@ -759,6 +761,62 @@ private void instantiateMethod(ClassModel.Builder classModel, .update(it -> createInstantiateBody(serviceType, it, params))); } + private void postConstructMethod(TypeInfo typeInfo, ClassModel.Builder classModel, TypeName serviceType) { + // postConstruct() + lifecycleMethod(typeInfo, ServiceCodegenTypes.SERVICE_ANNOTATION_POST_CONSTRUCT).ifPresent(method -> { + classModel.addMethod(postConstruct -> postConstruct.name("postConstruct") + .addAnnotation(Annotations.OVERRIDE) + .addParameter(instance -> instance.type(serviceType) + .name("instance")) + .addContentLine("instance." + method.elementName() + "();")); + }); + } + + private void preDestroyMethod(TypeInfo typeInfo, ClassModel.Builder classModel, TypeName serviceType) { + // preDestroy + lifecycleMethod(typeInfo, ServiceCodegenTypes.SERVICE_ANNOTATION_PRE_DESTROY).ifPresent(method -> { + classModel.addMethod(preDestroy -> preDestroy.name("preDestroy") + .addAnnotation(Annotations.OVERRIDE) + .addParameter(instance -> instance.type(serviceType) + .name("instance")) + .addContentLine("instance." + method.elementName() + "();")); + }); + } + + private Optional lifecycleMethod(TypeInfo typeInfo, TypeName annotationType) { + List list = typeInfo.elementInfo() + .stream() + .filter(ElementInfoPredicates.hasAnnotation(annotationType)) + .toList(); + if (list.isEmpty()) { + return Optional.empty(); + } + if (list.size() > 1) { + throw new IllegalStateException("There is more than one method annotated with " + annotationType.fqName() + + ", which is not allowed on type " + typeInfo.typeName().fqName()); + } + TypedElementInfo method = list.getFirst(); + if (method.accessModifier() == AccessModifier.PRIVATE) { + throw new CodegenException("Method annotated with " + annotationType.fqName() + + ", is private, which is not supported: " + typeInfo.typeName().fqName() + + "#" + method.elementName(), + method.originatingElement().orElseGet(method::elementName)); + } + if (!method.parameterArguments().isEmpty()) { + throw new CodegenException("Method annotated with " + annotationType.fqName() + + ", has parameters, which is not supported: " + typeInfo.typeName().fqName() + + "#" + method.elementName(), + method.originatingElement().orElseGet(method::elementName)); + } + if (!method.typeName().equals(TypeNames.PRIMITIVE_VOID)) { + throw new CodegenException("Method annotated with " + annotationType.fqName() + + ", is not void, which is not supported: " + typeInfo.typeName().fqName() + + "#" + method.elementName(), + method.originatingElement().orElseGet(method::elementName)); + } + return Optional.of(method); + } + private void createInstantiateBody(TypeName serviceType, Method.Builder method, List params) { diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/RegistryCodegenExtension.java b/service/codegen/src/main/java/io/helidon/service/codegen/RegistryCodegenExtension.java index 5414ed3cd00..c093f37add4 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/RegistryCodegenExtension.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/RegistryCodegenExtension.java @@ -17,7 +17,7 @@ package io.helidon.service.codegen; /** - * Code generation extension for Helidon Service REgistry. + * Code generation extension for Helidon Service Registry. */ interface RegistryCodegenExtension { /** diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceCodegenTypes.java b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceCodegenTypes.java index 785d9567cf0..317ac1222b7 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceCodegenTypes.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceCodegenTypes.java @@ -26,6 +26,16 @@ public final class ServiceCodegenTypes { * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.Service.Provider}. */ public static final TypeName SERVICE_ANNOTATION_PROVIDER = TypeName.create("io.helidon.service.registry.Service.Provider"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.Service.PreDestroy}. + */ + public static final TypeName SERVICE_ANNOTATION_PRE_DESTROY = + TypeName.create("io.helidon.service.registry.Service.PreDestroy"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.Service.PostConstruct}. + */ + public static final TypeName SERVICE_ANNOTATION_POST_CONSTRUCT = + TypeName.create("io.helidon.service.registry.Service.PostConstruct"); /** * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.Service.Contract}. */ diff --git a/service/registry/src/main/java/io/helidon/service/registry/CoreServiceRegistry.java b/service/registry/src/main/java/io/helidon/service/registry/CoreServiceRegistry.java index 39ef78db293..b6b13b517aa 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/CoreServiceRegistry.java +++ b/service/registry/src/main/java/io/helidon/service/registry/CoreServiceRegistry.java @@ -26,7 +26,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.TreeSet; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -46,11 +45,14 @@ class CoreServiceRegistry implements ServiceRegistry { Comparator.comparing(ServiceProvider::weight).reversed() .thenComparing(ServiceProvider::descriptorType); - private final Map> providersByContract; + private final Map> providersByContract; private final Map providersByService; + private final List allProviders; + @SuppressWarnings({"rawtypes", "unchecked"}) CoreServiceRegistry(ServiceRegistryConfig config, ServiceDiscovery serviceDiscovery) { - Map> providers = new HashMap<>(); + List allProviders = new ArrayList<>(); + Map> providers = new HashMap<>(); Map providersByService = new IdentityHashMap<>(); // each just once @@ -65,18 +67,22 @@ class CoreServiceRegistry implements ServiceRegistry { config.serviceInstances().forEach((descriptor, instance) -> { if (processedDescriptorTypes.add(descriptor.descriptorType())) { BoundInstance bi = new BoundInstance(descriptor, Optional.of(instance)); + allProviders.add(bi); providersByService.put(descriptor, bi); addContracts(providers, descriptor.contracts(), bi); } }); // add configured descriptors - for (Descriptor descriptor : config.serviceDescriptors()) { - if (processedDescriptorTypes.add(descriptor.descriptorType())) { - BoundDescriptor bd = new BoundDescriptor(this, descriptor, LazyValue.create(() -> instance(descriptor))); - providersByService.put(descriptor, bd); - addContracts(providers, descriptor.contracts(), bd); - } + for (Descriptor descriptor : config.serviceDescriptors()) { + BoundDescriptor bd = new BoundDescriptor(this, descriptor, LazyValue.create(() -> { + var instance = instance(descriptor); + instance.ifPresent(descriptor::postConstruct); + return instance; + })); + allProviders.add(bd); + providersByService.put(descriptor, bd); + addContracts(providers, descriptor.contracts(), bd); } boolean logUnsupported = LOGGER.isLoggable(Level.TRACE); @@ -95,12 +101,20 @@ class CoreServiceRegistry implements ServiceRegistry { DiscoveredDescriptor dd = new DiscoveredDescriptor(this, descriptorMeta, instanceSupplier(descriptorMeta)); + allProviders.add(dd); providersByService.put(descriptorMeta.descriptor(), dd); addContracts(providers, descriptorMeta.contracts(), dd); } } + // sort all the providers + providers.values() + .forEach(it -> it.sort(PROVIDER_COMPARATOR)); + allProviders.sort(PROVIDER_COMPARATOR); + allProviders.reversed(); + this.providersByContract = Map.copyOf(providers); this.providersByService = providersByService; + this.allProviders = List.copyOf(allProviders); } @Override @@ -157,39 +171,57 @@ public Optional get(ServiceInfo serviceInfo) { @Override public List allServices(TypeName contract) { return Optional.ofNullable(providersByContract.get(contract)) - .orElseGet(Set::of) + .orElseGet(List::of) .stream() .map(ServiceProvider::descriptor) .collect(Collectors.toUnmodifiableList()); } - private static void addContracts(Map> providers, + void shutdown() { + allProviders.forEach(ServiceProvider::close); + } + + private static void addContracts(Map> providers, Set contracts, ServiceProvider provider) { for (TypeName contract : contracts) { - providers.computeIfAbsent(contract, it -> new TreeSet<>(PROVIDER_COMPARATOR)) + providers.computeIfAbsent(contract, it -> new ArrayList<>()) .add(provider); } } - private Supplier> instanceSupplier(DescriptorHandler descriptorMeta) { - LazyValue> serviceInstance = LazyValue.create(() -> instance(descriptorMeta.descriptor())); + @SuppressWarnings({"rawtypes", "unchecked"}) + private ServiceAndInstance instanceSupplier(DescriptorHandler descriptorMeta) { + LazyValue> serviceInstance = LazyValue.create(() -> { + Descriptor descriptor = descriptorMeta.descriptor(); + var instance = instance(descriptor); + instance.ifPresent(descriptor::postConstruct); + return instance; + }); if (descriptorMeta.contracts().contains(TypeNames.SUPPLIER)) { - return () -> instanceFromSupplier(descriptorMeta.descriptor(), serviceInstance); + return new ServiceAndInstance(serviceInstance, + () -> instanceFromSupplier(descriptorMeta.descriptor(), serviceInstance)); } else { - return serviceInstance; + return new ServiceAndInstance(serviceInstance); + } + } + + private record ServiceAndInstance(LazyValue> serviceSupplier, + Supplier> instanceSupplier) { + ServiceAndInstance(LazyValue> serviceSupplier) { + this(serviceSupplier, serviceSupplier); } } private List allProviders(TypeName contract) { - Set serviceProviders = providersByContract.get(contract); + List serviceProviders = providersByContract.get(contract); if (serviceProviders == null) { return List.of(); } - return new ArrayList<>(serviceProviders); + return List.copyOf(serviceProviders); } private Optional instanceFromSupplier(Descriptor descriptor, LazyValue> serviceInstanceSupplier) { @@ -264,6 +296,8 @@ private interface ServiceProvider { double weight(); TypeName descriptorType(); + + void close(); } private record BoundInstance(Descriptor descriptor, Optional instance) implements ServiceProvider { @@ -276,6 +310,11 @@ public double weight() { public TypeName descriptorType() { return descriptor.descriptorType(); } + + @Override + public void close() { + // as the instance was provided from outside, we do not call pre-destroy + } } private record BoundDescriptor(CoreServiceRegistry registry, @@ -316,17 +355,25 @@ public double weight() { public TypeName descriptorType() { return descriptor.descriptorType(); } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public void close() { + if (lazyInstance.isLoaded()) { + lazyInstance.get().ifPresent(it -> ((Descriptor) descriptor).preDestroy(it)); + } + } } private record DiscoveredDescriptor(CoreServiceRegistry registry, DescriptorHandler metadata, - Supplier> instanceSupplier, + ServiceAndInstance instances, ReentrantLock lock) implements ServiceProvider { private DiscoveredDescriptor(CoreServiceRegistry registry, DescriptorHandler metadata, - Supplier> instanceSupplier) { - this(registry, metadata, instanceSupplier, new ReentrantLock()); + ServiceAndInstance instances) { + this(registry, metadata, instances, new ReentrantLock()); } @Override @@ -336,6 +383,7 @@ public Descriptor descriptor() { @Override public Optional instance() { + var instanceSupplier = instances.instanceSupplier(); if ((instanceSupplier instanceof LazyValue lv) && lv.isLoaded()) { return instanceSupplier.get(); } @@ -361,5 +409,14 @@ public double weight() { public TypeName descriptorType() { return metadata.descriptorType(); } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public void close() { + var serviceSupplier = instances.serviceSupplier(); + if (serviceSupplier.isLoaded()) { + serviceSupplier.get().ifPresent(it -> ((Descriptor) metadata.descriptor()).preDestroy(it)); + } + } } } diff --git a/service/registry/src/main/java/io/helidon/service/registry/CoreServiceRegistryManager.java b/service/registry/src/main/java/io/helidon/service/registry/CoreServiceRegistryManager.java index 1c8f1a8aa28..a6d61242da5 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/CoreServiceRegistryManager.java +++ b/service/registry/src/main/java/io/helidon/service/registry/CoreServiceRegistryManager.java @@ -63,6 +63,7 @@ public void shutdown() { Lock lock = lifecycleLock.writeLock(); try { lock.lock(); + registry.shutdown(); registry = null; } finally { lock.unlock(); diff --git a/service/registry/src/main/java/io/helidon/service/registry/GeneratedService.java b/service/registry/src/main/java/io/helidon/service/registry/GeneratedService.java index 624b9becb40..7061c0dd63a 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/GeneratedService.java +++ b/service/registry/src/main/java/io/helidon/service/registry/GeneratedService.java @@ -344,6 +344,22 @@ default Object instantiate(DependencyContext ctx) { throw new IllegalStateException("Cannot instantiate type " + serviceType().fqName() + ", as it is either abstract," + " or an interface."); } + + /** + * Invoke {@link io.helidon.service.registry.Service.PostConstruct} annotated method(s). + * + * @param instance instance to use + */ + default void postConstruct(T instance) { + } + + /** + * Invoke {@link io.helidon.service.registry.Service.PreDestroy} annotated method(s). + * + * @param instance instance to use + */ + default void preDestroy(T instance) { + } } private record TypeAndName(String type, String name) { diff --git a/service/registry/src/main/java/io/helidon/service/registry/Service.java b/service/registry/src/main/java/io/helidon/service/registry/Service.java index 572ef7f9bcf..8f3d29eeafd 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/Service.java +++ b/service/registry/src/main/java/io/helidon/service/registry/Service.java @@ -72,6 +72,36 @@ private Service() { TypeName TYPE = TypeName.create(Provider.class); } + /** + * A method annotated with this annotation will be invoked after the constructor is finished + * and all dependencies are satisfied. + *

+ * The method must not have any parameters and must be accessible (not {@code private}). + */ + @Documented + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.METHOD) + public @interface PostConstruct { + } + + /** + * A method annotated with this annotation will be invoked when the service registry shuts down. + *

+ * Behavior of this annotation may differ based on the service registry implementation used. For example + * when using Helidon Service Inject (to be introduced), a pre-destroy method would be used when the scope + * a service is created in is finished. The core service registry behaves similar like a singleton scope - instance + * is created once, and pre-destroy is called when the registry is shut down. + * This also implies that instances that are NOT created within a scope cannot have their pre-destroy methods + * invoked, as we do not control their lifecycle. + *

+ * The method must not have any parameters and must be accessible (not {@code private}). + */ + @Documented + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.METHOD) + public @interface PreDestroy { + } + /** * The {@code Contract} annotation is used to relay significance to the type that it annotates. While remaining optional in * its use, it is typically placed on an interface definition to signify that the given type can be used for lookup in the diff --git a/service/tests/codegen/src/test/java/io/helidon/service/tests/codegen/ServiceCodegenTypesTest.java b/service/tests/codegen/src/test/java/io/helidon/service/tests/codegen/ServiceCodegenTypesTest.java index c0ccc588d80..3e6bf64c623 100644 --- a/service/tests/codegen/src/test/java/io/helidon/service/tests/codegen/ServiceCodegenTypesTest.java +++ b/service/tests/codegen/src/test/java/io/helidon/service/tests/codegen/ServiceCodegenTypesTest.java @@ -62,6 +62,8 @@ void testTypes() { } checkField(toCheck, checked, fields, "SERVICE_ANNOTATION_PROVIDER", Service.Provider.class); + checkField(toCheck, checked, fields, "SERVICE_ANNOTATION_PRE_DESTROY", Service.PreDestroy.class); + checkField(toCheck, checked, fields, "SERVICE_ANNOTATION_POST_CONSTRUCT", Service.PostConstruct.class); checkField(toCheck, checked, fields, "SERVICE_ANNOTATION_CONTRACT", Service.Contract.class); checkField(toCheck, checked, fields, "SERVICE_ANNOTATION_EXTERNAL_CONTRACTS", Service.ExternalContracts.class); checkField(toCheck, checked, fields, "SERVICE_ANNOTATION_DESCRIPTOR", Service.Descriptor.class); diff --git a/service/tests/registry/pom.xml b/service/tests/registry/pom.xml index 50b807e0f8e..0a66188f428 100644 --- a/service/tests/registry/pom.xml +++ b/service/tests/registry/pom.xml @@ -39,7 +39,6 @@ io.helidon.service helidon-service-registry - io.helidon.config helidon-config diff --git a/service/tests/registry/src/main/java/io/helidon/service/test/registry/TestLifecycle.java b/service/tests/registry/src/main/java/io/helidon/service/test/registry/TestLifecycle.java new file mode 100644 index 00000000000..b9d05877736 --- /dev/null +++ b/service/tests/registry/src/main/java/io/helidon/service/test/registry/TestLifecycle.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 Oracle and/or 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.service.test.registry; + +import io.helidon.service.registry.Service; + +@Service.Contract +interface TestLifecycle { + int postConstructCalled(); +} diff --git a/service/tests/registry/src/main/java/io/helidon/service/test/registry/TestLifecycleService.java b/service/tests/registry/src/main/java/io/helidon/service/test/registry/TestLifecycleService.java new file mode 100644 index 00000000000..a03509b86f7 --- /dev/null +++ b/service/tests/registry/src/main/java/io/helidon/service/test/registry/TestLifecycleService.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 Oracle and/or 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.service.test.registry; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.service.registry.Service; + +@Service.Provider +class TestLifecycleService implements TestLifecycle { + static final AtomicInteger PRE_DESTROY = new AtomicInteger(); + + private final AtomicInteger postConstruct = new AtomicInteger(); + + @Override + public int postConstructCalled() { + return postConstruct.get(); + } + + @Service.PostConstruct + void postConstruct() { + this.postConstruct.incrementAndGet(); + } + + @Service.PreDestroy + void preDestroy() { + PRE_DESTROY.incrementAndGet(); + } +} diff --git a/service/tests/registry/src/test/java/io/helidon/service/test/registry/ServiceLifecycleTest.java b/service/tests/registry/src/test/java/io/helidon/service/test/registry/ServiceLifecycleTest.java new file mode 100644 index 00000000000..219d5ea1f95 --- /dev/null +++ b/service/tests/registry/src/test/java/io/helidon/service/test/registry/ServiceLifecycleTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 Oracle and/or 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.service.test.registry; + +import io.helidon.service.registry.ServiceRegistry; +import io.helidon.service.registry.ServiceRegistryManager; + +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; + +public class ServiceLifecycleTest { + private static ServiceRegistryManager registryManager; + private static ServiceRegistry registry; + + @BeforeAll + public static void init() { + registryManager = ServiceRegistryManager.create(); + registry = registryManager.registry(); + } + + @AfterAll + public static void shutdown() { + if (registryManager != null) { + registryManager.shutdown(); + assertThat("Pre destroy should be called on registry shutdown", + TestLifecycleService.PRE_DESTROY.get(), is(1)); + } + registryManager = null; + registry = null; + } + + @Test + void testServiceLifecycle() { + assertThat("Pre destroy should not be called", TestLifecycleService.PRE_DESTROY.get(), is(0)); + TestLifecycle testLifecycle = registry.get(TestLifecycle.class); + assertThat("There should be 1 post-construct call on the instance", testLifecycle.postConstructCalled(), is(1)); + assertThat("Pre destroy should not be called", TestLifecycleService.PRE_DESTROY.get(), is(0)); + } +} diff --git a/tests/functional/bookstore/src/test/java/io/helidon/tests/bookstore/MainTest.java b/tests/functional/bookstore/src/test/java/io/helidon/tests/bookstore/MainTest.java index 3592a504a35..eed2ed38863 100644 --- a/tests/functional/bookstore/src/test/java/io/helidon/tests/bookstore/MainTest.java +++ b/tests/functional/bookstore/src/test/java/io/helidon/tests/bookstore/MainTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 2024 Oracle and/or 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,6 +22,7 @@ import java.lang.System.Logger.Level; import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.nio.file.Paths; import java.util.ArrayList; @@ -87,11 +88,11 @@ void stop() throws Exception { } URL getHealthUrl() throws MalformedURLException { - return new URL("http://localhost:" + this.port + "/health"); + return URI.create("http://localhost:" + this.port + "/health").toURL(); } URL getBaseUrl() throws MalformedURLException { - return new URL("http://localhost:" + this.port); + return URI.create("http://localhost:" + this.port).toURL(); } void waitForApplicationDown() throws Exception { @@ -210,7 +211,7 @@ private void runExitOnStartedTest(String edition) throws Exception { Thread.sleep(500); } while (System.currentTimeMillis() < maxTime); - String eol = System.getProperty("line.separator"); + String eol = System.lineSeparator(); Assertions.fail("quickstart " + edition + " did not exit as expected." + eol + eol + "stdOut: " + stdOut + eol diff --git a/tests/integration/dbclient/pgsql/etc/docker/Dockerfile b/tests/integration/dbclient/pgsql/etc/docker/Dockerfile index f0778bb6852..587335551ec 100644 --- a/tests/integration/dbclient/pgsql/etc/docker/Dockerfile +++ b/tests/integration/dbclient/pgsql/etc/docker/Dockerfile @@ -17,8 +17,9 @@ FROM oraclelinux:9-slim RUN microdnf install postgresql-server && microdnf clean all +RUN mkdir -p /var/run/postgresql && \ + chown postgres:postgres /var/run/postgresql ADD entrypoint.sh /usr/local/bin/ - ENV PGDATA /var/lib/pgsql/data USER postgres ENTRYPOINT ["entrypoint.sh"] diff --git a/webserver/observe/metrics/src/test/resources/application.yaml b/webserver/observe/metrics/src/test/resources/application.yaml index 204c874771f..4e4e2cc8413 100644 --- a/webserver/observe/metrics/src/test/resources/application.yaml +++ b/webserver/observe/metrics/src/test/resources/application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 Oracle and/or its affiliates. +# Copyright (c) 2023, 2024 Oracle and/or 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,6 +22,6 @@ server: scopes: - name: vendor filter: - include: requests.l.* + include: requests\.l.* key-performance-indicators: extended: true diff --git a/webserver/security/src/test/resources/security-application.yaml b/webserver/security/src/test/resources/security-application.yaml index 504929aa6fe..f2ae0014db3 100644 --- a/webserver/security/src/test/resources/security-application.yaml +++ b/webserver/security/src/test/resources/security-application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2016, 2023 Oracle and/or its affiliates. +# Copyright (c) 2016, 2024 Oracle and/or 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,6 +47,7 @@ server: - path: "/auditOnly" # method - any # audit all methods (by default GET and HEAD are not audited) + methods: ["get"] audit: true audit-event-type: "unit_test" audit-message-format: "Unit test message format" diff --git a/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsRouting.java b/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsRouting.java index f6ee7f1454e..52d94519c3f 100644 --- a/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsRouting.java +++ b/webserver/websocket/src/main/java/io/helidon/webserver/websocket/WsRouting.java @@ -52,7 +52,7 @@ public static Builder builder() { } /** - * Emtpy WebSocket routing. + * Empty WebSocket routing. * * @return empty routing */