1+ package com .redis .om .spring .indexing ;
2+
3+ import java .util .ArrayList ;
4+ import java .util .List ;
5+ import java .util .Map ;
6+ import java .util .concurrent .ConcurrentHashMap ;
7+
8+ import org .slf4j .Logger ;
9+ import org .slf4j .LoggerFactory ;
10+ import org .springframework .context .ApplicationContext ;
11+ import org .springframework .data .redis .core .RedisHash ;
12+ import org .springframework .stereotype .Component ;
13+
14+ import com .fasterxml .jackson .core .JsonProcessingException ;
15+ import com .fasterxml .jackson .databind .ObjectMapper ;
16+ import com .redis .om .spring .annotations .Document ;
17+
18+ /**
19+ * Configurable provider for managing index definitions in Redis OM Spring.
20+ * This class bridges RediSearch indexes with Spring Data Redis and provides
21+ * runtime configuration capabilities for dynamic indexing.
22+ *
23+ * Phase 4 implementation of the Dynamic Indexing Feature Design.
24+ *
25+ * @since 1.0.0
26+ */
27+ @ Component
28+ public class ConfigurableIndexDefinitionProvider {
29+ private static final Logger logger = LoggerFactory .getLogger (ConfigurableIndexDefinitionProvider .class );
30+
31+ private final RediSearchIndexer indexer ;
32+ private final ApplicationContext applicationContext ;
33+ private final Map <Class <?>, IndexDefinition > indexDefinitions = new ConcurrentHashMap <>();
34+ private final Map <Class <?>, IndexResolver > customResolvers = new ConcurrentHashMap <>();
35+ private final ObjectMapper objectMapper = new ObjectMapper ();
36+
37+ public ConfigurableIndexDefinitionProvider (RediSearchIndexer indexer , ApplicationContext applicationContext ) {
38+ this .indexer = indexer ;
39+ this .applicationContext = applicationContext ;
40+ initializeFromAnnotations ();
41+ }
42+
43+ /**
44+ * Initialize index definitions from entity annotations.
45+ */
46+ private void initializeFromAnnotations () {
47+ // Scan for @Document and @RedisHash annotated classes
48+ Map <String , Object > documentBeans = applicationContext .getBeansWithAnnotation (Document .class );
49+ Map <String , Object > hashBeans = applicationContext .getBeansWithAnnotation (RedisHash .class );
50+
51+ documentBeans .values ().forEach (bean -> {
52+ Class <?> entityClass = bean .getClass ();
53+ processEntityClass (entityClass );
54+ });
55+
56+ hashBeans .values ().forEach (bean -> {
57+ Class <?> entityClass = bean .getClass ();
58+ processEntityClass (entityClass );
59+ });
60+ }
61+
62+ private void processEntityClass (Class <?> entityClass ) {
63+ String indexName = indexer .getIndexName (entityClass );
64+ String keyPrefix = getKeyPrefix (entityClass );
65+ IndexDefinition definition = new IndexDefinition (indexName , keyPrefix , entityClass );
66+ indexDefinitions .put (entityClass , definition );
67+ }
68+
69+ private String getKeyPrefix (Class <?> entityClass ) {
70+ // Extract key prefix from annotations or use default
71+ if (entityClass .isAnnotationPresent (Document .class )) {
72+ return entityClass .getSimpleName ().toLowerCase () + ":" ;
73+ } else if (entityClass .isAnnotationPresent (RedisHash .class )) {
74+ RedisHash hash = entityClass .getAnnotation (RedisHash .class );
75+ return hash .value () + ":" ;
76+ }
77+ return entityClass .getSimpleName ().toLowerCase () + ":" ;
78+ }
79+
80+ /**
81+ * Get all configured index definitions.
82+ *
83+ * @return list of all index definitions
84+ */
85+ public List <IndexDefinition > getIndexDefinitions () {
86+ return new ArrayList <>(indexDefinitions .values ());
87+ }
88+
89+ /**
90+ * Get index definition for a specific entity class.
91+ *
92+ * @param entityClass the entity class
93+ * @return the index definition, or null if not found
94+ */
95+ public IndexDefinition getIndexDefinition (Class <?> entityClass ) {
96+ return indexDefinitions .get (entityClass );
97+ }
98+
99+ /**
100+ * Get index definition for a specific entity class with context.
101+ *
102+ * @param entityClass the entity class
103+ * @param context the Redis index context
104+ * @return the context-aware index definition
105+ */
106+ public IndexDefinition getIndexDefinition (Class <?> entityClass , RedisIndexContext context ) {
107+ IndexResolver resolver = customResolvers .getOrDefault (entityClass , new DefaultIndexResolver (applicationContext ));
108+
109+ String indexName = resolver .resolveIndexName (entityClass , context );
110+ String keyPrefix = resolver .resolveKeyPrefix (entityClass , context );
111+
112+ return new IndexDefinition (indexName , keyPrefix , entityClass );
113+ }
114+
115+ /**
116+ * Register a new index definition at runtime.
117+ *
118+ * @param entityClass the entity class
119+ * @param indexName the index name
120+ * @param keyPrefix the key prefix
121+ */
122+ public void registerIndexDefinition (Class <?> entityClass , String indexName , String keyPrefix ) {
123+ IndexDefinition definition = new IndexDefinition (indexName , keyPrefix , entityClass );
124+ indexDefinitions .put (entityClass , definition );
125+ logger .info ("Registered index definition for {} with index {} and prefix {}" , entityClass .getName (), indexName ,
126+ keyPrefix );
127+ }
128+
129+ /**
130+ * Update an existing index definition.
131+ *
132+ * @param entityClass the entity class
133+ * @param indexName the new index name
134+ * @param keyPrefix the new key prefix
135+ */
136+ public void updateIndexDefinition (Class <?> entityClass , String indexName , String keyPrefix ) {
137+ IndexDefinition definition = new IndexDefinition (indexName , keyPrefix , entityClass );
138+ indexDefinitions .put (entityClass , definition );
139+ logger .info ("Updated index definition for {} with index {} and prefix {}" , entityClass .getName (), indexName ,
140+ keyPrefix );
141+ }
142+
143+ /**
144+ * Remove an index definition.
145+ *
146+ * @param entityClass the entity class
147+ * @return true if removed, false if not found
148+ */
149+ public boolean removeIndexDefinition (Class <?> entityClass ) {
150+ IndexDefinition removed = indexDefinitions .remove (entityClass );
151+ if (removed != null ) {
152+ logger .info ("Removed index definition for {}" , entityClass .getName ());
153+ return true ;
154+ }
155+ return false ;
156+ }
157+
158+ /**
159+ * Bulk register multiple index definitions.
160+ *
161+ * @param configs map of entity classes to their configurations
162+ */
163+ public void registerIndexDefinitions (Map <Class <?>, IndexDefinitionConfig > configs ) {
164+ configs .forEach ((entityClass , config ) -> {
165+ registerIndexDefinition (entityClass , config .getIndexName (), config .getKeyPrefix ());
166+ });
167+ }
168+
169+ /**
170+ * Refresh index definitions from current annotations.
171+ */
172+ public void refreshIndexDefinitions () {
173+ indexDefinitions .clear ();
174+ initializeFromAnnotations ();
175+ logger .info ("Refreshed {} index definitions from annotations" , indexDefinitions .size ());
176+ }
177+
178+ /**
179+ * Set a custom index resolver for a specific entity.
180+ *
181+ * @param entityClass the entity class
182+ * @param resolver the custom resolver
183+ */
184+ public void setIndexResolver (Class <?> entityClass , IndexResolver resolver ) {
185+ customResolvers .put (entityClass , resolver );
186+ logger .info ("Set custom index resolver for {}" , entityClass .getName ());
187+ }
188+
189+ /**
190+ * Get index definitions for all entities managed by a repository.
191+ *
192+ * @param repositoryClass the repository class
193+ * @return list of index definitions
194+ */
195+ public List <IndexDefinition > getIndexDefinitionsForRepository (Class <?> repositoryClass ) {
196+ // This would require analyzing the repository's generic type parameters
197+ // For now, return all definitions
198+ return getIndexDefinitions ();
199+ }
200+
201+ /**
202+ * Get statistics for an index.
203+ *
204+ * @param entityClass the entity class
205+ * @return index statistics
206+ */
207+ public IndexStatistics getIndexStatistics (Class <?> entityClass ) {
208+ IndexDefinition definition = indexDefinitions .get (entityClass );
209+ if (definition == null ) {
210+ return null ;
211+ }
212+
213+ // Would query Redis for actual statistics
214+ return new IndexStatistics (definition .getIndexName (), 0 , 0 );
215+ }
216+
217+ /**
218+ * Validate an index definition.
219+ *
220+ * @param definition the definition to validate
221+ * @return validation result
222+ */
223+ public ValidationResult validateIndexDefinition (IndexDefinition definition ) {
224+ ValidationResult result = new ValidationResult ();
225+
226+ if (definition .getIndexName () == null || definition .getIndexName ().isEmpty ()) {
227+ result .addError ("Index name is required" );
228+ }
229+
230+ if (definition .getKeyPrefix () == null || definition .getKeyPrefix ().isEmpty ()) {
231+ result .addError ("Key prefix is required" );
232+ }
233+
234+ if (definition .getEntityClass () == null ) {
235+ result .addError ("Entity class is required" );
236+ }
237+
238+ return result ;
239+ }
240+
241+ /**
242+ * Export all index definitions as JSON.
243+ *
244+ * @return JSON string of all definitions
245+ */
246+ public String exportDefinitions () {
247+ try {
248+ return objectMapper .writeValueAsString (indexDefinitions );
249+ } catch (JsonProcessingException e ) {
250+ logger .error ("Failed to export index definitions" , e );
251+ return "{}" ;
252+ }
253+ }
254+
255+ /**
256+ * Import index definitions from JSON.
257+ *
258+ * @param definitionsJson JSON string with definitions
259+ * @return number of imported definitions
260+ */
261+ public int importDefinitions (String definitionsJson ) {
262+ try {
263+ Map <String , Object > imported = objectMapper .readValue (definitionsJson , Map .class );
264+ // Process imported definitions
265+ return imported .size ();
266+ } catch (JsonProcessingException e ) {
267+ logger .error ("Failed to import index definitions" , e );
268+ return 0 ;
269+ }
270+ }
271+
272+ /**
273+ * Inner class representing an index definition configuration.
274+ */
275+ public static class IndexDefinitionConfig {
276+ private final String indexName ;
277+ private final String keyPrefix ;
278+
279+ public IndexDefinitionConfig (String indexName , String keyPrefix ) {
280+ this .indexName = indexName ;
281+ this .keyPrefix = keyPrefix ;
282+ }
283+
284+ public String getIndexName () {
285+ return indexName ;
286+ }
287+
288+ public String getKeyPrefix () {
289+ return keyPrefix ;
290+ }
291+ }
292+
293+ /**
294+ * Inner class representing an index definition.
295+ */
296+ public static class IndexDefinition {
297+ private final String indexName ;
298+ private final String keyPrefix ;
299+ private final Class <?> entityClass ;
300+
301+ public IndexDefinition (String indexName , String keyPrefix , Class <?> entityClass ) {
302+ this .indexName = indexName ;
303+ this .keyPrefix = keyPrefix ;
304+ this .entityClass = entityClass ;
305+ }
306+
307+ public String getIndexName () {
308+ return indexName ;
309+ }
310+
311+ public String getKeyPrefix () {
312+ return keyPrefix ;
313+ }
314+
315+ public Class <?> getEntityClass () {
316+ return entityClass ;
317+ }
318+ }
319+
320+ /**
321+ * Inner class representing index statistics.
322+ */
323+ public static class IndexStatistics {
324+ private final String indexName ;
325+ private final long documentCount ;
326+ private final long indexSize ;
327+
328+ public IndexStatistics (String indexName , long documentCount , long indexSize ) {
329+ this .indexName = indexName ;
330+ this .documentCount = documentCount ;
331+ this .indexSize = indexSize ;
332+ }
333+
334+ public String getIndexName () {
335+ return indexName ;
336+ }
337+
338+ public long getDocumentCount () {
339+ return documentCount ;
340+ }
341+
342+ public long getIndexSize () {
343+ return indexSize ;
344+ }
345+ }
346+
347+ /**
348+ * Inner class representing validation results.
349+ */
350+ public static class ValidationResult {
351+ private final List <String > errors = new ArrayList <>();
352+
353+ public void addError (String error ) {
354+ errors .add (error );
355+ }
356+
357+ public boolean isValid () {
358+ return errors .isEmpty ();
359+ }
360+
361+ public List <String > getErrors () {
362+ return errors ;
363+ }
364+ }
365+ }
0 commit comments