diff --git a/.bazelproject b/.bazelproject index 28183481..d02b0dd7 100644 --- a/.bazelproject +++ b/.bazelproject @@ -21,3 +21,6 @@ test_sources: repository-ydb-v1/src/test repository-ydb-v2/src/test util/src/test + +# Automatically includes all relevant targets under the 'directories' above +derive_targets_from_directories: true diff --git a/databind/BUILD b/databind/BUILD index 7e7df95d..3e9155c8 100644 --- a/databind/BUILD +++ b/databind/BUILD @@ -6,6 +6,7 @@ java_library( visibility = ["//visibility:public"], deps = [ "//bom:lombok", + "//util", "@java_contribs_stable//:com_google_code_findbugs_jsr305", "@java_contribs_stable//:com_google_guava_guava", "@java_contribs_stable//:javax_annotation_javax_annotation_api", diff --git a/databind/pom.xml b/databind/pom.xml index fb887186..66f503f8 100644 --- a/databind/pom.xml +++ b/databind/pom.xml @@ -22,6 +22,10 @@ + + tech.ydb.yoj + yoj-util + com.google.code.findbugs jsr305 diff --git a/databind/src/main/java/tech/ydb/yoj/databind/CustomValueType.java b/databind/src/main/java/tech/ydb/yoj/databind/CustomValueType.java index f2ce736b..4c4546d2 100644 --- a/databind/src/main/java/tech/ydb/yoj/databind/CustomValueType.java +++ b/databind/src/main/java/tech/ydb/yoj/databind/CustomValueType.java @@ -9,6 +9,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.RECORD_COMPONENT; import static java.lang.annotation.ElementType.TYPE; @@ -26,7 +27,7 @@ */ @Inherited @Retention(RUNTIME) -@Target({TYPE, FIELD, RECORD_COMPONENT}) +@Target({TYPE, FIELD, RECORD_COMPONENT, ANNOTATION_TYPE}) @ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/24") public @interface CustomValueType { /** diff --git a/databind/src/main/java/tech/ydb/yoj/databind/CustomValueTypes.java b/databind/src/main/java/tech/ydb/yoj/databind/CustomValueTypes.java index 15afa734..0af4bc79 100644 --- a/databind/src/main/java/tech/ydb/yoj/databind/CustomValueTypes.java +++ b/databind/src/main/java/tech/ydb/yoj/databind/CustomValueTypes.java @@ -8,6 +8,7 @@ import tech.ydb.yoj.databind.schema.Column; import tech.ydb.yoj.databind.schema.CustomConverterException; import tech.ydb.yoj.databind.schema.Schema.JavaField; +import tech.ydb.yoj.util.lang.Annotations; import javax.annotation.Nullable; import java.lang.reflect.InvocationTargetException; @@ -67,7 +68,7 @@ public static CustomValueType getCustomValueType(@NonNull Type type, @Nullable C var cvtAnnotation = columnAnnotation == null ? null : columnAnnotation.customValueType(); var columnCvt = cvtAnnotation == null || cvtAnnotation.converter().equals(ValueConverter.NoConverter.class) ? null : cvtAnnotation; - var cvt = columnCvt == null ? rawType.getAnnotation(CustomValueType.class) : columnCvt; + var cvt = columnCvt == null ? Annotations.find(CustomValueType.class, rawType) : columnCvt; if (cvt != null) { var columnClass = cvt.columnClass(); diff --git a/databind/src/main/java/tech/ydb/yoj/databind/converter/StringColumn.java b/databind/src/main/java/tech/ydb/yoj/databind/converter/StringColumn.java new file mode 100644 index 00000000..21486335 --- /dev/null +++ b/databind/src/main/java/tech/ydb/yoj/databind/converter/StringColumn.java @@ -0,0 +1,23 @@ +package tech.ydb.yoj.databind.converter; + +import tech.ydb.yoj.databind.CustomValueType; +import tech.ydb.yoj.databind.schema.Column; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.RECORD_COMPONENT; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Alias for "want-it-to-be-string" columns + * {@link StringValueConverter} + */ +@Inherited +@Retention(RUNTIME) +@Target({FIELD, RECORD_COMPONENT, ANNOTATION_TYPE}) +@Column(customValueType = @CustomValueType(columnClass = String.class, converter = StringValueConverter.class)) +public @interface StringColumn {} diff --git a/databind/src/main/java/tech/ydb/yoj/databind/converter/StringValueType.java b/databind/src/main/java/tech/ydb/yoj/databind/converter/StringValueType.java new file mode 100644 index 00000000..61da7a34 --- /dev/null +++ b/databind/src/main/java/tech/ydb/yoj/databind/converter/StringValueType.java @@ -0,0 +1,23 @@ +package tech.ydb.yoj.databind.converter; + +import tech.ydb.yoj.databind.CustomValueType; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.RECORD_COMPONENT; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Easy to use annotation to mark type as String based + * {@link StringValueConverter} + */ +@Inherited +@Retention(RUNTIME) +@Target({TYPE, FIELD, RECORD_COMPONENT, ANNOTATION_TYPE}) +@CustomValueType(columnClass = String.class, converter = StringValueConverter.class) +public @interface StringValueType {} diff --git a/databind/src/main/java/tech/ydb/yoj/databind/schema/Column.java b/databind/src/main/java/tech/ydb/yoj/databind/schema/Column.java index ef31f711..6c418e13 100644 --- a/databind/src/main/java/tech/ydb/yoj/databind/schema/Column.java +++ b/databind/src/main/java/tech/ydb/yoj/databind/schema/Column.java @@ -9,6 +9,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.RECORD_COMPONENT; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -33,7 +34,7 @@ * BigSubobject subobj2; * */ -@Target({FIELD, RECORD_COMPONENT}) +@Target({FIELD, RECORD_COMPONENT, ANNOTATION_TYPE}) @Retention(RUNTIME) public @interface Column { /** diff --git a/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/KotlinDataClassComponent.java b/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/KotlinDataClassComponent.java index d504dfa7..774f042e 100644 --- a/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/KotlinDataClassComponent.java +++ b/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/KotlinDataClassComponent.java @@ -10,6 +10,7 @@ import tech.ydb.yoj.databind.FieldValueType; import tech.ydb.yoj.databind.schema.Column; import tech.ydb.yoj.databind.schema.FieldValueException; +import tech.ydb.yoj.util.lang.Annotations; import javax.annotation.Nullable; import java.lang.reflect.Type; @@ -50,7 +51,7 @@ public KotlinDataClassComponent(Reflector reflector, String name, Preconditions.checkArgument(field != null, "Could not get Java field for property '%s' of '%s'", property.getName(), kPropertyType); - this.column = field.getAnnotation(Column.class); + this.column = Annotations.find(Column.class, field); this.valueType = FieldValueType.forJavaType(genericType, column); this.reflectType = reflector.reflectFieldType(genericType, valueType); } diff --git a/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/PojoField.java b/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/PojoField.java index a572d9ca..a22401fa 100644 --- a/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/PojoField.java +++ b/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/PojoField.java @@ -6,6 +6,7 @@ import tech.ydb.yoj.databind.FieldValueType; import tech.ydb.yoj.databind.schema.Column; import tech.ydb.yoj.databind.schema.FieldValueException; +import tech.ydb.yoj.util.lang.Annotations; import javax.annotation.Nullable; import java.lang.reflect.Type; @@ -46,7 +47,7 @@ public PojoField(@NonNull Reflector reflector, @NonNull java.lang.reflect.Field this.genericType = delegate.getGenericType(); this.type = delegate.getType(); - this.column = delegate.getAnnotation(Column.class); + this.column = Annotations.find(Column.class, delegate); this.valueType = FieldValueType.forJavaType(genericType, column); this.reflectType = reflector.reflectFieldType(genericType, valueType); } diff --git a/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/RecordField.java b/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/RecordField.java index d942a08c..4b0f8ad6 100644 --- a/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/RecordField.java +++ b/databind/src/main/java/tech/ydb/yoj/databind/schema/reflect/RecordField.java @@ -5,6 +5,7 @@ import tech.ydb.yoj.databind.FieldValueType; import tech.ydb.yoj.databind.schema.Column; import tech.ydb.yoj.databind.schema.FieldValueException; +import tech.ydb.yoj.util.lang.Annotations; import javax.annotation.Nullable; import java.lang.reflect.Type; @@ -40,7 +41,7 @@ public RecordField(@NonNull Reflector reflector, @NonNull java.lang.reflect.Reco this.name = delegate.getName(); this.genericType = delegate.getGenericType(); this.type = delegate.getType(); - this.column = delegate.getAnnotation(Column.class); + this.column = Annotations.find(Column.class, delegate); this.valueType = FieldValueType.forJavaType(genericType, column); this.reflectType = reflector.reflectFieldType(genericType, valueType); } diff --git a/repository-inmemory/src/test/java/tech/ydb/yoj/repository/test/inmemory/TestInMemoryRepository.java b/repository-inmemory/src/test/java/tech/ydb/yoj/repository/test/inmemory/TestInMemoryRepository.java index 40b1ddac..2f422ebb 100644 --- a/repository-inmemory/src/test/java/tech/ydb/yoj/repository/test/inmemory/TestInMemoryRepository.java +++ b/repository-inmemory/src/test/java/tech/ydb/yoj/repository/test/inmemory/TestInMemoryRepository.java @@ -25,6 +25,7 @@ import tech.ydb.yoj.repository.test.sample.model.Team; import tech.ydb.yoj.repository.test.sample.model.TypeFreak; import tech.ydb.yoj.repository.test.sample.model.UpdateFeedEntry; +import tech.ydb.yoj.repository.test.sample.model.VersionedAliasedEntity; import tech.ydb.yoj.repository.test.sample.model.VersionedEntity; import java.util.Set; @@ -118,6 +119,10 @@ public Table networkAppliances() { public Table versionedEntities() { return table(VersionedEntity.class); } + + public Table versionedAliasedEntities() { + return table(VersionedAliasedEntity.class); + } } private static class Supabubble2InMemoryTable extends InMemoryTable implements TestEntityOperations.Supabubble2Table { diff --git a/repository-test/src/main/java/tech/ydb/yoj/repository/test/RepositoryTest.java b/repository-test/src/main/java/tech/ydb/yoj/repository/test/RepositoryTest.java index 29ee24eb..06b950fc 100644 --- a/repository-test/src/main/java/tech/ydb/yoj/repository/test/RepositoryTest.java +++ b/repository-test/src/main/java/tech/ydb/yoj/repository/test/RepositoryTest.java @@ -56,8 +56,10 @@ import tech.ydb.yoj.repository.test.sample.model.TypeFreak.Embedded; import tech.ydb.yoj.repository.test.sample.model.UpdateFeedEntry; import tech.ydb.yoj.repository.test.sample.model.Version; +import tech.ydb.yoj.repository.test.sample.model.VersionedAliasedEntity; import tech.ydb.yoj.repository.test.sample.model.VersionedEntity; import tech.ydb.yoj.repository.test.sample.model.WithUnflattenableField; +import tech.ydb.yoj.repository.test.sample.model.annotations.Sha256; import java.time.Instant; import java.util.ArrayList; @@ -70,6 +72,7 @@ import java.util.Objects; import java.util.Set; import java.util.Spliterator; +import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -2730,6 +2733,29 @@ public void customValueTypeInFilter() { )).isNull(); } + @Test + public void customValueTypeInFilterByAlias() { + UUID testPrefferedUUID = UUID.randomUUID(); + var ve = new VersionedAliasedEntity(new VersionedAliasedEntity.Id("heyhey", new Version(100L), testPrefferedUUID, new Sha256("100")), new Version(100_500L), testPrefferedUUID); + db.tx(() -> db.versionedAliasedEntities().insert(ve)); + assertThat(db.tx(() -> db.versionedAliasedEntities().find(ve.id()))).isEqualTo(ve); + assertThat(db.tx(() -> db.versionedAliasedEntities().query() + .where("id.version").eq(ve.id().version()) + .and("version2").eq(ve.version2()) + .findOne() + )).isEqualTo(ve); + assertThat(db.tx(() -> db.versionedAliasedEntities().query() + .where("id.version").eq(100L) + .and("version2").eq(100_500L) + .findOne() + )).isEqualTo(ve); + assertThat(db.tx(() -> db.versionedAliasedEntities().query() + .where("id.version").eq(100L) + .and("version2").eq(null) + .findOne() + )).isNull(); + } + protected void runInTx(Consumer action) { // We do not retry transactions, because we do not expect conflicts in our test scenarios. RepositoryTransaction transaction = startTransaction(); diff --git a/repository-test/src/main/java/tech/ydb/yoj/repository/test/entity/TestEntities.java b/repository-test/src/main/java/tech/ydb/yoj/repository/test/entity/TestEntities.java index e5159102..89ae0cc4 100644 --- a/repository-test/src/main/java/tech/ydb/yoj/repository/test/entity/TestEntities.java +++ b/repository-test/src/main/java/tech/ydb/yoj/repository/test/entity/TestEntities.java @@ -20,6 +20,7 @@ import tech.ydb.yoj.repository.test.sample.model.Team; import tech.ydb.yoj.repository.test.sample.model.TypeFreak; import tech.ydb.yoj.repository.test.sample.model.UpdateFeedEntry; +import tech.ydb.yoj.repository.test.sample.model.VersionedAliasedEntity; import tech.ydb.yoj.repository.test.sample.model.VersionedEntity; import tech.ydb.yoj.repository.test.sample.model.WithUnflattenableField; @@ -44,7 +45,8 @@ private TestEntities() { WithUnflattenableField.class, UpdateFeedEntry.class, NetworkAppliance.class, - VersionedEntity.class + VersionedEntity.class, + VersionedAliasedEntity.class ); @SuppressWarnings("unchecked") diff --git a/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/TestEntityOperations.java b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/TestEntityOperations.java index 39ee35ee..7df78c61 100644 --- a/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/TestEntityOperations.java +++ b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/TestEntityOperations.java @@ -21,6 +21,7 @@ import tech.ydb.yoj.repository.test.sample.model.Team; import tech.ydb.yoj.repository.test.sample.model.TypeFreak; import tech.ydb.yoj.repository.test.sample.model.UpdateFeedEntry; +import tech.ydb.yoj.repository.test.sample.model.VersionedAliasedEntity; import tech.ydb.yoj.repository.test.sample.model.VersionedEntity; import java.util.ArrayList; @@ -66,6 +67,8 @@ default Table bytePkEntities() { Table versionedEntities(); + Table versionedAliasedEntities(); + class ProjectTable extends AbstractDelegatingTable { public ProjectTable(Table target) { super(target); diff --git a/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/VersionColumn.java b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/VersionColumn.java new file mode 100644 index 00000000..6e33ba19 --- /dev/null +++ b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/VersionColumn.java @@ -0,0 +1,19 @@ +package tech.ydb.yoj.repository.test.sample.model; + +import tech.ydb.yoj.databind.CustomValueType; +import tech.ydb.yoj.databind.schema.Column; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.RECORD_COMPONENT; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Inherited +@Retention(RUNTIME) +@Target({FIELD, RECORD_COMPONENT, ANNOTATION_TYPE}) +@Column(customValueType = @CustomValueType(columnClass = Long.class, converter = Version.Converter.class)) +public @interface VersionColumn {} diff --git a/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/VersionedAliasedEntity.java b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/VersionedAliasedEntity.java new file mode 100644 index 00000000..42d61b67 --- /dev/null +++ b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/VersionedAliasedEntity.java @@ -0,0 +1,26 @@ +package tech.ydb.yoj.repository.test.sample.model; + +import tech.ydb.yoj.databind.converter.StringColumn; +import tech.ydb.yoj.repository.db.Entity; +import tech.ydb.yoj.repository.db.RecordEntity; +import tech.ydb.yoj.repository.test.sample.model.annotations.Sha256; + +import java.util.UUID; + +public record VersionedAliasedEntity( + Id id, + @VersionColumn + Version version2, + @StringColumn + UUID uuid +) implements RecordEntity { + public record Id( + String value, + @VersionColumn + Version version, + @StringColumn + UUID uuidId, + Sha256 hash + ) implements Entity.Id { + } +} diff --git a/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/annotations/Digest.java b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/annotations/Digest.java new file mode 100644 index 00000000..3bf30d50 --- /dev/null +++ b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/annotations/Digest.java @@ -0,0 +1,36 @@ +package tech.ydb.yoj.repository.test.sample.model.annotations; + +import java.util.Objects; + + +public class Digest implements YojString { + private final String algorithm; + private final String digest; + + protected Digest(String algorithm, String digest) { + this.algorithm = algorithm; + this.digest = digest; + } + + @Override + public String toString() { + return algorithm + ":" + digest; + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + Digest digest1 = (Digest) object; + return Objects.equals(algorithm, digest1.algorithm) && Objects.equals(digest, digest1.digest); + } + + @Override + public int hashCode() { + return Objects.hash(algorithm, digest); + } +} diff --git a/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/annotations/Sha256.java b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/annotations/Sha256.java new file mode 100644 index 00000000..baac0306 --- /dev/null +++ b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/annotations/Sha256.java @@ -0,0 +1,17 @@ +package tech.ydb.yoj.repository.test.sample.model.annotations; + +public class Sha256 extends Digest { + private static final String SHA_256 = "SHA256"; + + public Sha256(String digest) { + super(SHA_256, digest); + } + + public static Sha256 valueOf(String value) { + String[] parsed = value.split(":"); + if (parsed.length != 2 || !SHA_256.equals(parsed[0])) { + throw new IllegalArgumentException(); + } + return new Sha256(parsed[1]); + } +} diff --git a/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/annotations/YojString.java b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/annotations/YojString.java new file mode 100644 index 00000000..85c03316 --- /dev/null +++ b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/annotations/YojString.java @@ -0,0 +1,7 @@ +package tech.ydb.yoj.repository.test.sample.model.annotations; + +import tech.ydb.yoj.databind.converter.StringValueType; + +@StringValueType +public interface YojString { +} diff --git a/repository-ydb-v1/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java b/repository-ydb-v1/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java index 8e67aaa6..7db08ead 100644 --- a/repository-ydb-v1/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java +++ b/repository-ydb-v1/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java @@ -30,6 +30,7 @@ import tech.ydb.yoj.repository.test.sample.model.Team; import tech.ydb.yoj.repository.test.sample.model.TypeFreak; import tech.ydb.yoj.repository.test.sample.model.UpdateFeedEntry; +import tech.ydb.yoj.repository.test.sample.model.VersionedAliasedEntity; import tech.ydb.yoj.repository.test.sample.model.VersionedEntity; import tech.ydb.yoj.repository.ydb.table.YdbTable; import tech.ydb.yoj.repository.ydb.yql.YqlPredicate; @@ -138,6 +139,10 @@ public Table networkAppliances() { public Table versionedEntities() { return table(VersionedEntity.class); } + + public Table versionedAliasedEntities() { + return table(VersionedAliasedEntity.class); + } } private static class YdbSupabubble2Table extends YdbTable implements Supabubble2Table { diff --git a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java index b22c8fc9..83343fd4 100644 --- a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java +++ b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java @@ -28,6 +28,7 @@ import tech.ydb.yoj.repository.test.sample.model.Team; import tech.ydb.yoj.repository.test.sample.model.TypeFreak; import tech.ydb.yoj.repository.test.sample.model.UpdateFeedEntry; +import tech.ydb.yoj.repository.test.sample.model.VersionedAliasedEntity; import tech.ydb.yoj.repository.test.sample.model.VersionedEntity; import tech.ydb.yoj.repository.ydb.table.YdbTable; @@ -137,6 +138,10 @@ public Table networkAppliances() { public Table versionedEntities() { return table(VersionedEntity.class); } + + public Table versionedAliasedEntities() { + return table(VersionedAliasedEntity.class); + } } private static class YdbSupabubble2Table extends YdbTable implements TestEntityOperations.Supabubble2Table { diff --git a/util/src/main/java/tech/ydb/yoj/util/lang/Annotations.java b/util/src/main/java/tech/ydb/yoj/util/lang/Annotations.java new file mode 100644 index 00000000..c9b7d52c --- /dev/null +++ b/util/src/main/java/tech/ydb/yoj/util/lang/Annotations.java @@ -0,0 +1,75 @@ +package tech.ydb.yoj.util.lang; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class Annotations { + private Annotations() { + } + + /** + * Find first annotation that matches the class in parameter + * + * @param annotation - to look for + * @param component - entry point to search + * + * @return annotation found or null + */ + + @Nullable + public static A find(Class annotation, @Nonnull AnnotatedElement component) { + A found = component.getAnnotation(annotation); + if (found != null) { + return found; + } + Set ann; + if (component instanceof Class clazz) { + ann = collectAnnotations(clazz); + } else { + ann = Set.of(component.getAnnotations()); + } + return findInDepth(annotation, ann); + } + + @Nonnull + private static Set collectAnnotations(Class component) { + Set result = new HashSet<>(); + Set> classesToExamine = new HashSet<>(); + classesToExamine.add(component); + while (!classesToExamine.isEmpty()) { + Class candidate = classesToExamine.iterator().next(); + result.addAll(List.of(candidate.getDeclaredAnnotations())); + if (candidate.getSuperclass() != null) { + classesToExamine.add(candidate.getSuperclass()); + } + classesToExamine.addAll(List.of(candidate.getInterfaces())); + classesToExamine.remove(candidate); + } + return result; + } + + @Nullable + @SuppressWarnings("unchecked") + private static A findInDepth(Class annotation, @Nonnull Collection anns) { + Set visited = new HashSet<>(); + Set annotationToExamine = new HashSet<>(anns); + while (!annotationToExamine.isEmpty()) { + Annotation candidate = annotationToExamine.iterator().next(); + if (visited.add(candidate)) { + if (candidate.annotationType() == annotation) { + return (A) candidate; + } else { + annotationToExamine.addAll(List.of(candidate.annotationType().getDeclaredAnnotations())); + } + } + annotationToExamine.remove(candidate); + } + return null; + } +}