1+ package com .redis .om .spring .indexing ;
2+
3+ import java .util .regex .Matcher ;
4+ import java .util .regex .Pattern ;
5+
6+ import org .springframework .context .ApplicationContext ;
7+ import org .springframework .expression .Expression ;
8+ import org .springframework .expression .ExpressionParser ;
9+ import org .springframework .expression .spel .standard .SpelExpressionParser ;
10+ import org .springframework .expression .spel .support .StandardEvaluationContext ;
11+
12+ import com .redis .om .spring .annotations .IndexingOptions ;
13+
14+ /**
15+ * Default implementation of {@link IndexResolver} that supports
16+ * SpEL expressions and context-based resolution.
17+ *
18+ * <p>This implementation evaluates SpEL expressions found in
19+ * {@link IndexingOptions} annotations and incorporates context
20+ * information from {@link RedisIndexContext} when available.
21+ *
22+ * @since 1.0.0
23+ */
24+ public class DefaultIndexResolver implements IndexResolver {
25+
26+ private final ApplicationContext applicationContext ;
27+ private final ExpressionParser parser = new SpelExpressionParser ();
28+ private static final Pattern SPEL_TEMPLATE_PATTERN = Pattern .compile ("#\\ {([^}]+)\\ }" );
29+
30+ /**
31+ * Creates a new DefaultIndexResolver.
32+ *
33+ * @param applicationContext the Spring application context
34+ */
35+ public DefaultIndexResolver (ApplicationContext applicationContext ) {
36+ this .applicationContext = applicationContext ;
37+ }
38+
39+ @ Override
40+ public String resolveIndexName (Class <?> entityClass , RedisIndexContext context ) {
41+ IndexingOptions indexingOptions = entityClass .getAnnotation (IndexingOptions .class );
42+
43+ if (indexingOptions != null && !indexingOptions .indexName ().isEmpty ()) {
44+ String indexName = indexingOptions .indexName ();
45+
46+ // Check if the index name contains SpEL expressions
47+ if (containsSpelExpression (indexName )) {
48+ String evaluated = evaluateSpelExpression (indexName , entityClass , context );
49+ if (evaluated != null ) {
50+ return evaluated ;
51+ }
52+ // If SpEL evaluation failed, fall back to default
53+ } else {
54+ // Plain string annotation - always return as is, regardless of context
55+ return indexName ;
56+ }
57+ }
58+
59+ // Default naming convention
60+ String baseName = getDefaultIndexName (entityClass );
61+
62+ // Apply context if available
63+ if (context != null ) {
64+ StringBuilder sb = new StringBuilder (baseName );
65+
66+ // Remove the _idx suffix if present
67+ if (baseName .endsWith ("_idx" )) {
68+ sb .setLength (sb .length () - 4 );
69+ } else if (baseName .endsWith ("Idx" )) {
70+ sb .setLength (sb .length () - 3 );
71+ }
72+
73+ if (context .getTenantId () != null ) {
74+ sb .append ("_" ).append (context .getTenantId ());
75+ }
76+
77+ if (context .getEnvironment () != null ) {
78+ sb .append ("_" ).append (context .getEnvironment ());
79+ }
80+
81+ // Add version attribute if present
82+ Object version = context .getAttribute ("version" );
83+ if (version != null ) {
84+ sb .append ("_" ).append (version );
85+ }
86+
87+ sb .append ("_idx" );
88+ return sb .toString ();
89+ }
90+
91+ return baseName ;
92+ }
93+
94+ @ Override
95+ public String resolveKeyPrefix (Class <?> entityClass , RedisIndexContext context ) {
96+ IndexingOptions indexingOptions = entityClass .getAnnotation (IndexingOptions .class );
97+
98+ if (indexingOptions != null && !indexingOptions .keyPrefix ().isEmpty ()) {
99+ String keyPrefix = indexingOptions .keyPrefix ();
100+
101+ // Check if the prefix contains SpEL expressions
102+ if (containsSpelExpression (keyPrefix )) {
103+ String evaluated = evaluateSpelExpression (keyPrefix , entityClass , context );
104+ if (evaluated != null ) {
105+ return evaluated ;
106+ }
107+ // If SpEL evaluation failed, fall back to default
108+ } else {
109+ // Plain string annotation - always return as is, regardless of context
110+ return keyPrefix ;
111+ }
112+ }
113+
114+ // Default key prefix convention
115+ String basePrefix = getDefaultKeyPrefix (entityClass );
116+
117+ // Apply context if available
118+ if (context != null && context .getTenantId () != null ) {
119+ // Prepend tenant ID to the prefix
120+ return context .getTenantId () + ":" + basePrefix ;
121+ }
122+
123+ return basePrefix ;
124+ }
125+
126+ /**
127+ * Gets the tenant ID from the context.
128+ * Subclasses can override this for custom tenant resolution.
129+ *
130+ * @param context the current context
131+ * @return the tenant ID, or null if not available
132+ */
133+ protected String getTenantId (RedisIndexContext context ) {
134+ if (context != null ) {
135+ return context .getTenantId ();
136+ }
137+ return null ;
138+ }
139+
140+ private boolean containsSpelExpression (String value ) {
141+ return value .contains ("#{" );
142+ }
143+
144+ private String evaluateSpelExpression (String expression , Class <?> entityClass , RedisIndexContext context ) {
145+ try {
146+ // Create evaluation context
147+ StandardEvaluationContext evalContext = new StandardEvaluationContext ();
148+
149+ // Add context as a variable
150+ if (context != null ) {
151+ evalContext .setVariable ("context" , context );
152+ } else {
153+ // Create empty context to avoid null reference errors
154+ evalContext .setVariable ("context" , RedisIndexContext .builder ().build ());
155+ }
156+
157+ // Add application context beans
158+ if (applicationContext != null ) {
159+ evalContext .setBeanResolver ((ctx , beanName ) -> {
160+ // Special handling for environment bean
161+ if ("environment" .equals (beanName )) {
162+ return applicationContext .getEnvironment ();
163+ }
164+ return applicationContext .getBean (beanName );
165+ });
166+
167+ // Also add environment as a variable for direct access
168+ if (applicationContext .getEnvironment () != null ) {
169+ evalContext .setVariable ("environment" , applicationContext .getEnvironment ());
170+ }
171+ }
172+
173+ // Process template expressions - replace #{...} with evaluated values
174+ StringBuffer result = new StringBuffer ();
175+ Matcher matcher = SPEL_TEMPLATE_PATTERN .matcher (expression );
176+ boolean hasFailedExpressions = false ;
177+
178+ while (matcher .find ()) {
179+ String spelExpression = matcher .group (1 );
180+ try {
181+ Expression exp = parser .parseExpression (spelExpression );
182+ Object evalResult = exp .getValue (evalContext );
183+ if (evalResult != null ) {
184+ matcher .appendReplacement (result , Matcher .quoteReplacement (evalResult .toString ()));
185+ } else {
186+ hasFailedExpressions = true ;
187+ break ;
188+ }
189+ } catch (Exception e ) {
190+ // Expression evaluation failed
191+ hasFailedExpressions = true ;
192+ break ;
193+ }
194+ }
195+
196+ if (hasFailedExpressions ) {
197+ return null ;
198+ }
199+
200+ matcher .appendTail (result );
201+ return result .toString ();
202+
203+ } catch (Exception e ) {
204+ // Fall back to default if SpEL evaluation fails
205+ return null ;
206+ }
207+ }
208+
209+ private String getDefaultIndexName (Class <?> entityClass ) {
210+ // Use the simple name converted to snake_case
211+ String className = entityClass .getSimpleName ();
212+ StringBuilder sb = new StringBuilder ();
213+
214+ for (int i = 0 ; i < className .length (); i ++) {
215+ char c = className .charAt (i );
216+ if (Character .isUpperCase (c ) && i > 0 ) {
217+ sb .append ("_" );
218+ }
219+ sb .append (Character .toLowerCase (c ));
220+ }
221+
222+ sb .append ("_idx" );
223+ return sb .toString ();
224+ }
225+
226+ private String getDefaultKeyPrefix (Class <?> entityClass ) {
227+ // Use the simple name converted to lowercase with colons
228+ String className = entityClass .getSimpleName ();
229+ StringBuilder sb = new StringBuilder ();
230+
231+ for (int i = 0 ; i < className .length (); i ++) {
232+ char c = className .charAt (i );
233+ if (Character .isUpperCase (c ) && i > 0 ) {
234+ sb .append (":" );
235+ }
236+ sb .append (Character .toLowerCase (c ));
237+ }
238+
239+ sb .append (":" );
240+ return sb .toString ();
241+ }
242+ }
0 commit comments