Skip to content

Commit 4d015f4

Browse files
author
NikolayAvramov
committed
Add Dependency resolver into the data module
1 parent 5e078e7 commit 4d015f4

File tree

5 files changed

+764
-0
lines changed

5 files changed

+764
-0
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package solutions.bellatrix.data.annotations;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
/**
9+
* Annotation to mark fields that require automatic dependency creation.
10+
* When a field is marked with this annotation and its value is null,
11+
* the system will automatically create the dependent entity using
12+
* the registered factory and repository.
13+
*/
14+
@Target(ElementType.FIELD)
15+
@Retention(RetentionPolicy.RUNTIME)
16+
public @interface Dependency {
17+
18+
/**
19+
* The entity class that should be created as a dependency.
20+
* This class must have a registered factory and repository.
21+
*/
22+
Class<?> entityType();
23+
24+
/**
25+
* Optional: Custom factory method name to use for creating the dependency.
26+
* If not specified, the default factory method will be used.
27+
*/
28+
String factoryMethod() default "createDefault";
29+
30+
/**
31+
* Optional: Whether to create the dependency even if the field is not null.
32+
* Default is false (only create if field is null).
33+
*/
34+
boolean forceCreate() default false;
35+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package solutions.bellatrix.data.http.contracts;
2+
3+
import solutions.bellatrix.data.http.infrastructure.DependencyResolver;
4+
import solutions.bellatrix.data.http.infrastructure.Entity;
5+
6+
public interface EntityFactory<T extends Entity> {
7+
8+
T buildDefault();
9+
10+
default T createWithDependencies() {
11+
T entity = buildDefault();
12+
return DependencyResolver.resolveDependencies(entity);
13+
}
14+
}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package solutions.bellatrix.data.http.infrastructure;
2+
3+
import solutions.bellatrix.data.annotations.Dependency;
4+
import solutions.bellatrix.data.configuration.RepositoryFactory;
5+
import solutions.bellatrix.data.contracts.Repository;
6+
import solutions.bellatrix.data.http.contracts.EntityFactory;
7+
8+
import java.lang.reflect.Field;
9+
import java.lang.reflect.Method;
10+
import java.util.*;
11+
12+
/**
13+
* Utility class for automatically resolving entity dependencies.
14+
* This class handles the creation of dependent entities when fields
15+
* are marked with @Dependency annotations using recursive resolution
16+
* to handle complex dependency chains.
17+
*/
18+
public class DependencyResolver {
19+
20+
// Track entities being resolved to prevent circular dependencies
21+
private static final Set<Class<?>> resolvingEntities = new HashSet<>();
22+
23+
/**
24+
* Resolves all dependencies for the given entity by creating
25+
* dependent entities where fields are marked with @Dependency
26+
* and the field value is null. Uses recursive resolution to handle
27+
* complex dependency chains.
28+
*
29+
* @param entity The entity to resolve dependencies for
30+
* @param <T> The entity type
31+
* @return The entity with all dependencies resolved
32+
*/
33+
public static <T> T resolveDependencies(T entity) {
34+
if (entity == null) {
35+
return null;
36+
}
37+
38+
// Clear the resolving set for each top-level call
39+
resolvingEntities.clear();
40+
return resolveDependenciesRecursive(entity);
41+
}
42+
43+
/**
44+
* Recursively resolves dependencies for an entity, ensuring that
45+
* dependencies are created in the correct order (bottom-up).
46+
*
47+
* @param entity The entity to resolve dependencies for
48+
* @param <T> The entity type
49+
* @return The entity with all dependencies resolved
50+
*/
51+
private static <T> T resolveDependenciesRecursive(T entity) {
52+
if (entity == null) {
53+
return null;
54+
}
55+
56+
Class<?> entityClass = entity.getClass();
57+
58+
// Check for circular dependency
59+
if (resolvingEntities.contains(entityClass)) {
60+
throw new RuntimeException("Circular dependency detected for entity: " + entityClass.getSimpleName());
61+
}
62+
63+
// Add to resolving set
64+
resolvingEntities.add(entityClass);
65+
66+
try {
67+
// Get all fields with @Dependency annotations
68+
List<Field> dependencyFields = getDependencyFields(entityClass);
69+
70+
// Resolve each dependency field
71+
for (Field field : dependencyFields) {
72+
resolveFieldDependencyRecursive(entity, field);
73+
}
74+
75+
return entity;
76+
} finally {
77+
// Remove from resolving set
78+
resolvingEntities.remove(entityClass);
79+
}
80+
}
81+
82+
/**
83+
* Gets all fields with @Dependency annotations from the given class.
84+
*
85+
* @param entityClass The entity class to scan
86+
* @return List of fields with @Dependency annotations
87+
*/
88+
private static List<Field> getDependencyFields(Class<?> entityClass) {
89+
List<Field> dependencyFields = new ArrayList<>();
90+
Field[] fields = entityClass.getDeclaredFields();
91+
92+
for (Field field : fields) {
93+
if (field.getAnnotation(Dependency.class) != null) {
94+
dependencyFields.add(field);
95+
}
96+
}
97+
98+
return dependencyFields;
99+
}
100+
101+
/**
102+
* Recursively resolves a single field dependency by creating the required entity
103+
* and setting its ID in the field. This method ensures that the dependency entity
104+
* is fully resolved (including its own dependencies) before being created.
105+
*
106+
* @param entity The parent entity
107+
* @param field The field that requires dependency resolution
108+
*/
109+
private static void resolveFieldDependencyRecursive(Object entity, Field field) {
110+
try {
111+
field.setAccessible(true);
112+
Dependency dependencyAnnotation = field.getAnnotation(Dependency.class);
113+
Object currentValue = field.get(entity);
114+
115+
// Skip if field already has a value and forceCreate is false
116+
if (currentValue != null && !dependencyAnnotation.forceCreate()) {
117+
return;
118+
}
119+
120+
// Create the dependency entity using the factory
121+
Object dependencyEntity = createDependencyEntity(dependencyAnnotation);
122+
123+
// Recursively resolve dependencies for the dependency entity
124+
// This ensures that all dependencies are resolved bottom-up
125+
dependencyEntity = resolveDependenciesRecursive(dependencyEntity);
126+
127+
// Get the repository directly using the entity type from the annotation
128+
@SuppressWarnings("unchecked")
129+
Repository<Entity> repository = (Repository<Entity>) RepositoryFactory.INSTANCE.getRepository((Class<? extends Entity>) dependencyAnnotation.entityType());
130+
131+
// Create the fully resolved entity in the repository (this will set the ID)
132+
Entity createdEntity = repository.create((Entity) dependencyEntity);
133+
134+
// Extract the ID from the created entity
135+
String entityId = extractEntityId(createdEntity);
136+
137+
// Set the ID in the field
138+
field.set(entity, entityId);
139+
140+
} catch (Exception e) {
141+
throw new RuntimeException("Failed to resolve dependency for field: " + field.getName(), e);
142+
}
143+
}
144+
145+
/**
146+
* Creates a dependency entity using the specified factory method.
147+
*
148+
* @param dependencyAnnotation The dependency annotation containing creation info
149+
* @return The created dependency entity
150+
*/
151+
private static Object createDependencyEntity(Dependency dependencyAnnotation) {
152+
try {
153+
// Find the factory for the entity type
154+
EntityFactory<?> factory = findFactoryForEntity(dependencyAnnotation.entityType());
155+
if (factory == null) {
156+
throw new IllegalStateException("No factory found for entity type: " + dependencyAnnotation.entityType().getSimpleName());
157+
}
158+
159+
// Use the specified factory method or default
160+
String methodName = dependencyAnnotation.factoryMethod();
161+
Method factoryMethod = factory.getClass().getMethod(methodName);
162+
163+
return factoryMethod.invoke(factory);
164+
165+
} catch (Exception e) {
166+
throw new RuntimeException("Failed to create dependency entity", e);
167+
}
168+
}
169+
170+
/**
171+
* Finds the factory for the given entity type by looking for
172+
* factory classes that implement EntityFactory.
173+
*
174+
* @param entityType The entity class to find a factory for
175+
* @return The factory instance or null if not found
176+
*/
177+
private static EntityFactory<?> findFactoryForEntity(Class<?> entityType) {
178+
String entityName = entityType.getSimpleName();
179+
String factoryClassName = entityName + "RepositoryFactory";
180+
181+
try {
182+
// Try to find the factory in the same package as the entity
183+
String packageName = entityType.getPackage().getName();
184+
Class<?> factoryClass = Class.forName(packageName + "." + factoryClassName);
185+
186+
// For static classes, create a temporary instance to call methods
187+
return (EntityFactory<?>) factoryClass.getDeclaredConstructor().newInstance();
188+
189+
} catch (Exception e) {
190+
throw new RuntimeException("Factory not found for entity type: " + entityType.getSimpleName(), e);
191+
}
192+
}
193+
194+
/**
195+
* Extracts the ID from an entity object by calling the getId() method.
196+
*
197+
* @param entity The entity to extract ID from
198+
* @return The entity ID as a string
199+
*/
200+
private static String extractEntityId(Object entity) {
201+
try {
202+
Method getIdMethod = entity.getClass().getMethod("getId");
203+
Object id = getIdMethod.invoke(entity);
204+
return id != null ? id.toString() : null;
205+
} catch (Exception e) {
206+
throw new RuntimeException("Failed to extract ID from entity: " + entity.getClass().getSimpleName(), e);
207+
}
208+
}
209+
}

0 commit comments

Comments
 (0)