diff --git a/CHANGELOG.md b/CHANGELOG.md
index 573a1ee9..a390b48e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
# Changelog
## [Unreleased]
+### Added
+- `AttributeParser.retrieveAttributes(Executable)` method, by @HardNorth
+### Changed
+- Attribute annotations and their extraction improved, by @HardNorth
## [5.4.7]
### Added
diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/Attribute.java b/src/main/java/com/epam/reportportal/annotations/attribute/Attribute.java
index cdc7a099..6cf4fbbf 100644
--- a/src/main/java/com/epam/reportportal/annotations/attribute/Attribute.java
+++ b/src/main/java/com/epam/reportportal/annotations/attribute/Attribute.java
@@ -16,18 +16,17 @@
package com.epam.reportportal.annotations.attribute;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
+import java.lang.annotation.*;
/**
- * Annotation used in {@link Attributes} as field, to provide {@link com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ}
- * with both 'key' and 'value' fields specified
- *
- * @author Ivan Budayeu
+ * Annotation used per se or in {@link Attributes} as field, to provide {@link com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ}
+ * with both 'key' and 'value' fields specified.
*/
+@Inherited
+@Documented
@Retention(RetentionPolicy.RUNTIME)
-@Target({})
+@Repeatable(AttributeGroup.class)
+@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR })
public @interface Attribute {
String key();
diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/AttributeGroup.java b/src/main/java/com/epam/reportportal/annotations/attribute/AttributeGroup.java
new file mode 100644
index 00000000..2f9ffdc1
--- /dev/null
+++ b/src/main/java/com/epam/reportportal/annotations/attribute/AttributeGroup.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2025 EPAM Systems
+ *
+ * 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
+ *
+ * https://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 com.epam.reportportal.annotations.attribute;
+
+import java.lang.annotation.*;
+
+/**
+ * Gathering annotation for {@link Attribute}
+ */
+@Inherited
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR })
+public @interface AttributeGroup {
+ Attribute[] value();
+}
diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/AttributeValue.java b/src/main/java/com/epam/reportportal/annotations/attribute/AttributeValue.java
index 21b319a4..29e35a7d 100644
--- a/src/main/java/com/epam/reportportal/annotations/attribute/AttributeValue.java
+++ b/src/main/java/com/epam/reportportal/annotations/attribute/AttributeValue.java
@@ -16,18 +16,17 @@
package com.epam.reportportal.annotations.attribute;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
+import java.lang.annotation.*;
/**
- * Annotation used in {@link Attributes} as field, to provide {@link com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ}
- * with only 'value' field specified ('key' in the resulted entity will be NULL)
- *
- * @author Ivan Budayeu
+ * Annotation used per se or in {@link Attributes} as field, to provide {@link com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ}
+ * with only 'value' field specified ('key' in the resulted entity will be NULL). In this case it will appear as a tag on ReportPortal.
*/
+@Inherited
+@Documented
@Retention(RetentionPolicy.RUNTIME)
-@Target({})
+@Repeatable(AttributeValueGroup.class)
+@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR })
public @interface AttributeValue {
String value();
diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/AttributeValueGroup.java b/src/main/java/com/epam/reportportal/annotations/attribute/AttributeValueGroup.java
new file mode 100644
index 00000000..644f1cb1
--- /dev/null
+++ b/src/main/java/com/epam/reportportal/annotations/attribute/AttributeValueGroup.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2025 EPAM Systems
+ *
+ * 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
+ *
+ * https://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 com.epam.reportportal.annotations.attribute;
+
+import java.lang.annotation.*;
+
+/**
+ * Gathering annotation for {@link AttributeValue}
+ */
+@Inherited
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR })
+public @interface AttributeValueGroup {
+ AttributeValue[] value();
+}
diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/Attributes.java b/src/main/java/com/epam/reportportal/annotations/attribute/Attributes.java
index 5e509e38..35ae67b5 100644
--- a/src/main/java/com/epam/reportportal/annotations/attribute/Attributes.java
+++ b/src/main/java/com/epam/reportportal/annotations/attribute/Attributes.java
@@ -28,7 +28,7 @@
* @see MultiValueAttribute
*/
@Retention(RetentionPolicy.RUNTIME)
-@Target({ ElementType.METHOD, ElementType.TYPE })
+@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR })
@Inherited
public @interface Attributes {
diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/MultiKeyAttribute.java b/src/main/java/com/epam/reportportal/annotations/attribute/MultiKeyAttribute.java
index c48c8cad..e192642e 100644
--- a/src/main/java/com/epam/reportportal/annotations/attribute/MultiKeyAttribute.java
+++ b/src/main/java/com/epam/reportportal/annotations/attribute/MultiKeyAttribute.java
@@ -16,19 +16,18 @@
package com.epam.reportportal.annotations.attribute;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
+import java.lang.annotation.*;
/**
- * Annotation used in {@link Attributes} as field, to provide multiple {@link com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ}
- * with both 'key' and 'value' fields specified.
- * Used to prevent duplication of {@link Attribute} annotation with the same value and different keys
- *
- * @author Ivan Budayeu
+ * Annotation used per se or in {@link Attributes} as field, to provide multiple
+ * {@link com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ} with both 'key' and 'value' fields specified.
+ * Used to prevent duplication of {@link Attribute} annotation with the same value and different keys.
*/
+@Inherited
+@Documented
@Retention(RetentionPolicy.RUNTIME)
-@Target({})
+@Repeatable(MultiKeyAttributeGroup.class)
+@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR })
public @interface MultiKeyAttribute {
String[] keys();
diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/MultiKeyAttributeGroup.java b/src/main/java/com/epam/reportportal/annotations/attribute/MultiKeyAttributeGroup.java
new file mode 100644
index 00000000..ca2227d5
--- /dev/null
+++ b/src/main/java/com/epam/reportportal/annotations/attribute/MultiKeyAttributeGroup.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2025 EPAM Systems
+ *
+ * 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
+ *
+ * https://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 com.epam.reportportal.annotations.attribute;
+
+import java.lang.annotation.*;
+
+/**
+ * Gathering annotation for {@link MultiKeyAttribute}
+ */
+@Inherited
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR })
+public @interface MultiKeyAttributeGroup {
+ MultiKeyAttribute[] value();
+}
diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/MultiValueAttribute.java b/src/main/java/com/epam/reportportal/annotations/attribute/MultiValueAttribute.java
index 39186326..ecaa72d5 100644
--- a/src/main/java/com/epam/reportportal/annotations/attribute/MultiValueAttribute.java
+++ b/src/main/java/com/epam/reportportal/annotations/attribute/MultiValueAttribute.java
@@ -16,19 +16,18 @@
package com.epam.reportportal.annotations.attribute;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
+import java.lang.annotation.*;
/**
- * Annotation used in {@link Attributes} as field, to provide multiple {@link com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ}
- * with both 'key' (optional) and 'value' fields specified.
- * Used to prevent duplication of {@link Attribute} annotation with the same key and different values
- *
- * @author Ivan Budayeu
+ * Annotation used per se or in {@link Attributes} as field, to provide multiple
+ * {@link com.epam.ta.reportportal.ws.model.attribute.ItemAttributesRQ} with both 'key' (optional) and 'value' fields specified.
+ * Used to prevent duplication of {@link Attribute} annotation with the same key and different values.
*/
+@Inherited
+@Documented
@Retention(RetentionPolicy.RUNTIME)
-@Target({})
+@Repeatable(MultiValueAttributeGroup.class)
+@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR })
public @interface MultiValueAttribute {
String key() default "";
@@ -36,7 +35,7 @@
String[] values();
/**
- * @return 'true' if key of the resulted entity should be NULL, otherwise {@link MultiValueAttribute#key()} will be used
+ * @return if key of the resulted entity should be {@code null}, otherwise {@link MultiValueAttribute#key()} will be used.
*/
boolean isNullKey() default false;
}
diff --git a/src/main/java/com/epam/reportportal/annotations/attribute/MultiValueAttributeGroup.java b/src/main/java/com/epam/reportportal/annotations/attribute/MultiValueAttributeGroup.java
new file mode 100644
index 00000000..f1cf648b
--- /dev/null
+++ b/src/main/java/com/epam/reportportal/annotations/attribute/MultiValueAttributeGroup.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2025 EPAM Systems
+ *
+ * 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
+ *
+ * https://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 com.epam.reportportal.annotations.attribute;
+
+import java.lang.annotation.*;
+
+/**
+ * Gathering annotation for {@link MultiValueAttribute}
+ */
+@Inherited
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR })
+public @interface MultiValueAttributeGroup {
+ MultiValueAttribute[] value();
+}
diff --git a/src/main/java/com/epam/reportportal/utils/AttributeParser.java b/src/main/java/com/epam/reportportal/utils/AttributeParser.java
index 98ef372a..ae7341ae 100644
--- a/src/main/java/com/epam/reportportal/utils/AttributeParser.java
+++ b/src/main/java/com/epam/reportportal/utils/AttributeParser.java
@@ -21,6 +21,7 @@
import jakarta.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
+import java.lang.reflect.Executable;
import java.util.*;
import java.util.stream.Collectors;
@@ -74,7 +75,7 @@ public static Set parseAsSet(@Nullable String rawAttributes) {
*/
@Nullable
public static ItemAttributesRQ splitKeyValue(@Nullable String attribute) {
- if (null == attribute || attribute.trim().isEmpty()) {
+ if (StringUtils.isBlank(attribute)) {
return null;
}
String[] keyValue = attribute.split(KEY_VALUE_SPLITTER);
@@ -97,25 +98,87 @@ public static ItemAttributesRQ splitKeyValue(@Nullable String attribute) {
* @return a set of ReportPortal attributes
*/
@Nonnull
- public static Set retrieveAttributes(@Nonnull Attributes attributesAnnotation) {
+ public static Set retrieveAttributes(@Nullable Attributes attributesAnnotation) {
Set itemAttributes = new LinkedHashSet<>();
- for (Attribute attribute : attributesAnnotation.attributes()) {
- if (!attribute.value().trim().isEmpty()) {
- itemAttributes.add(createItemAttribute(attribute.key(), attribute.value()));
- }
+ if (attributesAnnotation == null) {
+ return itemAttributes;
}
- for (AttributeValue attributeValue : attributesAnnotation.attributeValues()) {
- if (!attributeValue.value().trim().isEmpty()) {
- itemAttributes.add(createItemAttribute(null, attributeValue.value()));
+ itemAttributes.addAll(retrieveAttributes(attributesAnnotation.attributes()));
+ itemAttributes.addAll(retrieveAttributes(attributesAnnotation.attributeValues()));
+ itemAttributes.addAll(retrieveAttributes(attributesAnnotation.multiKeyAttributes()));
+ itemAttributes.addAll(retrieveAttributes(attributesAnnotation.multiValueAttributes()));
+ return itemAttributes;
+ }
+
+ /**
+ * Parse ReportPortal attributes from {@link Attribute} annotations.
+ *
+ * @param attributes annotation instances
+ * @return a set of ReportPortal attributes
+ */
+ @Nonnull
+ public static Set retrieveAttributes(@Nullable Attribute... attributes) {
+ Set itemAttributes = new LinkedHashSet<>();
+ if (attributes != null) {
+ for (Attribute attribute : attributes) {
+ if (StringUtils.isNotBlank(attribute.value())) {
+ itemAttributes.add(createItemAttribute(attribute.key(), attribute.value()));
+ }
}
}
- for (MultiKeyAttribute attribute : attributesAnnotation.multiKeyAttributes()) {
- itemAttributes.addAll(createItemAttributes(attribute.keys(), attribute.value()));
+ return itemAttributes;
+ }
+
+ /**
+ * Parse ReportPortal attributes from {@link AttributeValue} annotations.
+ *
+ * @param attributeValues annotation instances
+ * @return a set of ReportPortal attributes
+ */
+ @Nonnull
+ public static Set retrieveAttributes(@Nullable AttributeValue... attributeValues) {
+ Set itemAttributes = new LinkedHashSet<>();
+ if (attributeValues != null) {
+ for (AttributeValue attributeValue : attributeValues) {
+ if (StringUtils.isNotBlank(attributeValue.value())) {
+ itemAttributes.add(createItemAttribute(null, attributeValue.value()));
+ }
+ }
}
- for (MultiValueAttribute attribute : attributesAnnotation.multiValueAttributes()) {
- itemAttributes.addAll(createItemAttributes(attribute.isNullKey() ? null : attribute.key(), attribute.values()));
+ return itemAttributes;
+ }
+
+ /**
+ * Parse ReportPortal attributes from {@link MultiKeyAttribute} annotations.
+ *
+ * @param multiKeyAttributes annotation instances
+ * @return a set of ReportPortal attributes
+ */
+ @Nonnull
+ public static Set retrieveAttributes(@Nullable MultiKeyAttribute... multiKeyAttributes) {
+ Set itemAttributes = new LinkedHashSet<>();
+ if (multiKeyAttributes != null) {
+ for (MultiKeyAttribute attribute : multiKeyAttributes) {
+ itemAttributes.addAll(createItemAttributes(attribute.keys(), attribute.value()));
+ }
}
+ return itemAttributes;
+ }
+ /**
+ * Parse ReportPortal attributes from {@link MultiValueAttribute} annotations.
+ *
+ * @param multiValueAttributes annotation instances
+ * @return a set of ReportPortal attributes
+ */
+ @Nonnull
+ public static Set retrieveAttributes(@Nullable MultiValueAttribute... multiValueAttributes) {
+ Set itemAttributes = new LinkedHashSet<>();
+ if (multiValueAttributes != null) {
+ for (MultiValueAttribute attribute : multiValueAttributes) {
+ itemAttributes.addAll(createItemAttributes(attribute.isNullKey() ? null : attribute.key(), attribute.values()));
+ }
+ }
return itemAttributes;
}
@@ -128,7 +191,7 @@ public static Set retrieveAttributes(@Nonnull Attributes attri
*/
@Nonnull
public static List createItemAttributes(@Nullable String[] keys, @Nullable String value) {
- if (value == null || value.trim().isEmpty()) {
+ if (StringUtils.isBlank(value)) {
return Collections.emptyList();
}
if (keys == null || keys.length < 1) {
@@ -164,4 +227,21 @@ public static List createItemAttributes(@Nullable String key,
public static ItemAttributesRQ createItemAttribute(@Nullable String key, @Nonnull String value) {
return new ItemAttributesRQ(key, value);
}
+
+ /**
+ * Scan for attributes annotations on the given executable and its declaration.
+ *
+ * @param executable the executable to scan
+ * @return a non-null set of ReportPortal attributes (may be empty if none are found)
+ */
+ @Nonnull
+ public static Set retrieveAttributes(@Nonnull Executable executable) {
+ Set itemAttributes = new LinkedHashSet<>();
+ itemAttributes.addAll(retrieveAttributes(executable.getAnnotation(Attributes.class)));
+ itemAttributes.addAll(retrieveAttributes(executable.getAnnotationsByType(Attribute.class)));
+ itemAttributes.addAll(retrieveAttributes(executable.getAnnotationsByType(AttributeValue.class)));
+ itemAttributes.addAll(retrieveAttributes(executable.getAnnotationsByType(MultiKeyAttribute.class)));
+ itemAttributes.addAll(retrieveAttributes(executable.getAnnotationsByType(MultiValueAttribute.class)));
+ return itemAttributes;
+ }
}
diff --git a/src/test/java/com/epam/reportportal/utils/AnnotationAttributeParserTest.java b/src/test/java/com/epam/reportportal/utils/AnnotationAttributeParserTest.java
index 0ea2db37..0a138a37 100644
--- a/src/test/java/com/epam/reportportal/utils/AnnotationAttributeParserTest.java
+++ b/src/test/java/com/epam/reportportal/utils/AnnotationAttributeParserTest.java
@@ -325,4 +325,73 @@ public void verify_all_attributes_in_one_annotation() throws NoSuchMethodExcepti
assertThat(result, hasSize(4));
}
+
+ private static final class StandaloneAttributeVerify {
+ @Attribute(key = "standaloneKey", value = "standaloneValue")
+ public void testMethod() {
+ }
+ }
+
+ @Test
+ public void verify_standalone_attribute_on_method() throws NoSuchMethodException {
+ java.lang.reflect.Method method = StandaloneAttributeVerify.class.getMethod("testMethod");
+ Set result = AttributeParser.retrieveAttributes(method);
+
+ assertThat(result, hasSize(1));
+ ItemAttributesRQ request = result.iterator().next();
+ assertThat(request.getKey(), equalTo("standaloneKey"));
+ assertThat(request.getValue(), equalTo("standaloneValue"));
+ }
+
+ private static final class RepeatedAttributeVerify {
+ @Attribute(key = "key1", value = "value1")
+ @Attribute(key = "key2", value = "value2")
+ public void testMethod() {
+ }
+ }
+
+ @Test
+ public void verify_repeated_attribute_on_method() throws NoSuchMethodException {
+ java.lang.reflect.Method method = RepeatedAttributeVerify.class.getMethod("testMethod");
+ Set result = AttributeParser.retrieveAttributes(method);
+
+ assertThat(result, hasSize(2));
+ assertThat(result.stream().map(KEY_EXTRACT).collect(toList()), containsInAnyOrder("key1", "key2"));
+ assertThat(result.stream().map(VALUE_EXTRACT).collect(toList()), containsInAnyOrder("value1", "value2"));
+ }
+
+ private static final class MixedAttributeVerify {
+ @Attributes(attributes = @Attribute(key = "innerKey", value = "innerValue"))
+ @Attribute(key = "outerKey", value = "outerValue")
+ public void testMethod() {
+ }
+ }
+
+ @Test
+ public void verify_mixed_attributes_on_method() throws NoSuchMethodException {
+ java.lang.reflect.Method method = MixedAttributeVerify.class.getMethod("testMethod");
+ Set result = AttributeParser.retrieveAttributes(method);
+
+ assertThat(result, hasSize(2));
+ assertThat(result.stream().map(KEY_EXTRACT).collect(toList()), containsInAnyOrder("innerKey", "outerKey"));
+ assertThat(result.stream().map(VALUE_EXTRACT).collect(toList()), containsInAnyOrder("innerValue", "outerValue"));
+ }
+
+ private static final class AllTypesOnMethodVerify {
+ @Attribute(key = "attrKey", value = "attrValue")
+ @AttributeValue("attrValueOnly")
+ @MultiKeyAttribute(keys = { "mk1", "mk2" }, value = "mkValue")
+ @MultiValueAttribute(key = "mvKey", values = { "mv1", "mv2" })
+ public void testMethod() {
+ }
+ }
+
+ @Test
+ public void verify_all_types_on_method() throws NoSuchMethodException {
+ java.lang.reflect.Method method = AllTypesOnMethodVerify.class.getMethod("testMethod");
+ Set result = AttributeParser.retrieveAttributes(method);
+
+ // 1 (Attribute) + 1 (AttributeValue) + 2 (MultiKey) + 2 (MultiValue) = 6
+ assertThat(result, hasSize(6));
+ }
}