Skip to content

Commit

Permalink
Adding search API (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
SuppieRK authored Jan 25, 2025
1 parent 72429c1 commit 73e9d66
Show file tree
Hide file tree
Showing 13 changed files with 1,336 additions and 36 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ methods outputs should be exposed to other classes.
<dependency>
<groupId>io.github.suppierk</groupId>
<artifactId>inject</artifactId>
<version>1.1.1</version>
<version>1.2.0</version>
</dependency>
```

- **Gradle** (_works for both Groovy and Kotlin_)

```groovy
implementation("io.github.suppierk:inject:1.1.1")
implementation("io.github.suppierk:inject:1.2.0")
```

[![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-orange.svg)](https://sonarcloud.io/summary/overall?id=SuppieRK_inject)
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ SONATYPE_AUTOMATIC_RELEASE=true

GROUP=io.github.suppierk
POM_ARTIFACT_ID=inject
VERSION_NAME=1.1.1
VERSION_NAME=1.2.0

POM_NAME=Simple Dependency Injector
POM_DESCRIPTION=Java 11 compatible implementation of the JSR 330 dependency injection
Expand Down
91 changes: 91 additions & 0 deletions src/main/java/io/github/suppierk/inject/AnnotationWrapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* MIT License
*
* Copyright 2025 Roman Khlebnov
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the “Software”), to deal in the Software without
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/

package io.github.suppierk.inject;

import java.lang.annotation.Annotation;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.Objects;

/**
* Wraps an annotation to be suitable for querying.
*
* <p>Typically {@link Annotation} is a proxy of package-private {@link
* sun.reflect.annotation.AnnotationInvocationHandler} where we are interested to fetch its {@code
* memberValues} field.
*/
public final class AnnotationWrapper {
private static final String MEMBER_VALUES_FIELD_NAME = "memberValues";

private final Annotation annotation;
private final Map<String, Object> memberValues;
private final String stringRepresentation;

@SuppressWarnings("unchecked")
public AnnotationWrapper(Annotation annotation) {
try {
this.annotation = annotation;

final var invocationHandler = Proxy.getInvocationHandler(annotation);
final var memberValuesField =
invocationHandler.getClass().getDeclaredField(MEMBER_VALUES_FIELD_NAME);
memberValuesField.setAccessible(true);
this.memberValues =
Map.copyOf((Map<String, Object>) memberValuesField.get(invocationHandler));

this.stringRepresentation = annotation.toString();
} catch (Exception e) {
throw new IllegalStateException(
"Failed to introspect an annotation " + annotation.toString(), e);
}
}

public Annotation annotation() {
return annotation;
}

public Map<String, Object> memberValues() {
return memberValues;
}

@Override
public boolean equals(Object o) {
if (!(o instanceof AnnotationWrapper)) {
return false;
}

AnnotationWrapper that = (AnnotationWrapper) o;
return Objects.equals(annotation, that.annotation);
}

@Override
public int hashCode() {
return annotation.hashCode();
}

@Override
public String toString() {
return stringRepresentation;
}
}
115 changes: 112 additions & 3 deletions src/main/java/io/github/suppierk/inject/Injector.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import io.github.suppierk.inject.graph.RefersTo;
import io.github.suppierk.inject.graph.ReflectionNode;
import io.github.suppierk.inject.graph.Value;
import io.github.suppierk.inject.query.KeyAnnotationsPredicate;
import io.github.suppierk.utils.ConsoleConstants;
import jakarta.inject.Inject;
import jakarta.inject.Provider;
Expand Down Expand Up @@ -58,6 +59,7 @@
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
Expand All @@ -75,6 +77,8 @@ public final class Injector implements Closeable {
private static final String NULL_VALUE_TEMPLATE = "%s is null";
private static final String DUPLICATE_VALUE_TEMPLATE = "Duplicate: %s";
private static final String MISSING_VALUE_TEMPLATE = "Missing: %s";
private static final String MULTIPLE_VALUES_TEMPLATE =
"Multiple values available for %s and predicate %s";
private static final String ALREADY_REPLACED_VALUE_TEMPLATE = "Already replaced: %s";
private static final String CYCLE_TEMPLATE = "Found cycle: %s";
private static final String MULTIPLE_INJECT_CONSTRUCTORS_TEMPLATE =
Expand Down Expand Up @@ -105,7 +109,7 @@ public final class Injector implements Closeable {
private Injector(InjectorReference injectorReference, Map<Key<?>, Node<?>> providers) {
injectorReference.set(this);

this.providers = providers;
this.providers = Map.copyOf(providers);
this.currentInjector = new Value<>(injectorReference, Injector.this);
}

Expand All @@ -117,7 +121,7 @@ private Injector(InjectorReference injectorReference, Map<Key<?>, Node<?>> provi
* @param clazz to retrieve
* @param <T> is the type of the instance
* @return initialized instance
* @throws IllegalArgumentException is the class argument is {@code null}
* @throws IllegalArgumentException if the class argument is {@code null}
*/
public <T> T get(Class<T> clazz) {
if (clazz == null) {
Expand All @@ -128,6 +132,111 @@ public <T> T get(Class<T> clazz) {
return node.get();
}

/**
* Retrieve fully initialized instance of the class.
*
* <p>This call will also create all required dependencies for this class.
*
* <p>Useful in conjunction with {@link #findOne(Class, KeyAnnotationsPredicate)} or {@link
* #findAll(Class, KeyAnnotationsPredicate)}.
*
* @param key to retrieve
* @param <T> is the type of the instance
* @return initialized instance
* @throws IllegalArgumentException if the class argument is {@code null}
*/
public <T> T get(Key<T> key) {
if (key == null) {
throw new IllegalArgumentException(String.format(NULL_VALUE_TEMPLATE, "Key"));
}

final var node = getNode(key);
return node.get();
}

/**
* Find if {@link Injector} has an instance of specified class with certain annotations and their
* values.
*
* @param clazz to find
* @param keyAnnotationsPredicate to match
* @return an {@link Optional} {@link Key} which can be used to retrieve dependency
* @param <T> is the type of the instance
* @throws IllegalArgumentException if class or predicate arguments is {@code null}
* @throws IllegalStateException if there is more than one dependency match
*/
public <T> Optional<Key<T>> findOne(
Class<T> clazz, KeyAnnotationsPredicate keyAnnotationsPredicate) {
final var keys = findAll(clazz, keyAnnotationsPredicate);

if (keys.isEmpty()) {
return Optional.empty();
}

if (keys.size() > 1) {
throw new IllegalStateException(
String.format(MULTIPLE_VALUES_TEMPLATE, clazz, keyAnnotationsPredicate));
}

return Optional.of(keys.get(0));
}

/**
* Find if {@link Injector} has an instance of specified class with certain annotations and their
* values.
*
* @param clazz to find
* @param keyAnnotationsPredicate to match
* @return a {@link List} of {@link Key}s which can be used to retrieve dependencies
* @param <T> is the type of the instance
* @throws IllegalArgumentException if class or predicate arguments is {@code null}
*/
@SuppressWarnings("unchecked")
public <T> List<Key<T>> findAll(Class<T> clazz, KeyAnnotationsPredicate keyAnnotationsPredicate) {
if (clazz == null) {
throw new IllegalArgumentException(String.format(NULL_VALUE_TEMPLATE, "Class"));
}

if (keyAnnotationsPredicate == null) {
throw new IllegalArgumentException(String.format(NULL_VALUE_TEMPLATE, "Injector predicate"));
}

final var keys = new ArrayList<Key<T>>();

for (Key<?> key : providers.keySet()) {
if (key.type().isAssignableFrom(clazz) && keyAnnotationsPredicate.test(key)) {
keys.add((Key<T>) key);
}
}

return List.copyOf(keys);
}

/**
* Find if {@link Injector} has an instance of specified class.
*
* @param clazz to find
* @return an {@link Optional} {@link Key} which can be used to retrieve dependency
* @param <T> is the type of the instance
* @throws IllegalArgumentException if class or predicate arguments is {@code null}
* @throws IllegalStateException if there is more than one dependency match
*/
public <T> Optional<Key<T>> findOne(Class<T> clazz) {
return findOne(clazz, KeyAnnotationsPredicate.alwaysMatch());
}

/**
* Find if {@link Injector} has an instance of specified class.
*
* @param clazz to find
* @return a {@link List} of {@link Key}s which can be used to retrieve dependencies
* @param <T> is the type of the instance
* @throws IllegalArgumentException if class or predicate arguments is {@code null}
*/
public <T> List<Key<T>> findAll(Class<T> clazz) {
return findAll(clazz, KeyAnnotationsPredicate.alwaysMatch());
}

/**
* Package-private retriever of specific nodes to be used in {@link #providers} via {@link
* InjectorReference}.
Expand Down Expand Up @@ -537,7 +646,7 @@ public final Injector build() {
() -> {
for (Node<?> node : providers.values()) {
for (Key<?> key : node.parentKeys()) {
if (!providers.containsKey(key)) {
if (!Injector.class.equals(key.type()) && !providers.containsKey(key)) {
throw new IllegalArgumentException(String.format(MISSING_VALUE_TEMPLATE, key));
}
}
Expand Down
52 changes: 32 additions & 20 deletions src/main/java/io/github/suppierk/inject/Key.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
*/
public final class Key<T> {
private final Class<T> type;
private final Set<Annotation> annotations;
private final Set<AnnotationWrapper> annotationWrappers;

public Key(Class<T> type, Set<Annotation> annotations) {
if (type == null) {
Expand All @@ -47,37 +47,47 @@ public Key(Class<T> type, Set<Annotation> annotations) {
}

if (annotations == null) {
this.annotations = Set.of();
this.annotationWrappers = Set.of();
} else {
for (Annotation annotation : annotations) {
if (!annotation.annotationType().isAnnotationPresent(Qualifier.class)) {
throw new IllegalArgumentException(
"Annotation " + annotation.annotationType().getName() + " is not a @Qualifier");
}
}
this.annotationWrappers =
annotations.stream()
.map(
annotation -> {
if (!annotation.annotationType().isAnnotationPresent(Qualifier.class)) {
throw new IllegalArgumentException(
"Annotation @"
+ annotation.annotationType().getName()
+ " is not a @Qualifier");
}

this.annotations = Set.copyOf(annotations);
return new AnnotationWrapper(annotation);
})
.collect(Collectors.toUnmodifiableSet());
}
}

public Class<T> type() {
return type;
}

public Set<Annotation> annotations() {
return annotations;
public Set<AnnotationWrapper> annotationWrappers() {
return annotationWrappers;
}

@Override
public boolean equals(Object o) {
if (!(o instanceof Key)) return false;
if (!(o instanceof Key)) {
return false;
}

Key<?> that = (Key<?>) o;
return Objects.equals(type, that.type) && Objects.equals(annotations, that.annotations);
return Objects.equals(type, that.type)
&& Objects.equals(annotationWrappers, that.annotationWrappers);
}

@Override
public int hashCode() {
return Objects.hash(type, annotations);
return Objects.hash(type, annotationWrappers);
}

@Override
Expand All @@ -86,11 +96,13 @@ public String toString() {
"%s(%s%s)",
Key.class.getSimpleName(),
type.getName(),
annotations.isEmpty()
annotationWrappers.isEmpty()
? ""
: String.format(
" as [%s]",
annotations.stream().map(Annotation::toString).collect(Collectors.joining(", "))));
annotationWrappers.stream()
.map(AnnotationWrapper::toString)
.collect(Collectors.joining(", "))));
}

/**
Expand All @@ -113,18 +125,18 @@ public String toYamlString(boolean itemize, int indentationLevel) {
firstIndent,
ConsoleConstants.cyanBold(type.getName()),
nestedIndent,
annotations().isEmpty()
annotationWrappers().isEmpty()
? ConsoleConstants.YAML_EMPTY_ARRAY
: String.format(
"%n%s",
annotations().stream()
annotationWrappers().stream()
.map(
annotation ->
annotationWrapper ->
String.format(
"%s%s'%s'",
ConsoleConstants.indent(actualIndent + 1),
ConsoleConstants.YAML_ITEM,
annotationString(annotation)))
annotationString(annotationWrapper.annotation())))
.collect(Collectors.joining(String.format("%n")))));
}

Expand Down
Loading

0 comments on commit 73e9d66

Please sign in to comment.