Summary
Extract the ~400-line FioriRecommendationHandler into an orchestrator + focused helpers. Replace the unbounded negative-result cache with a bounded Caffeine cache.
Problem
FioriRecommendationHandler (~400 lines) has 10 distinct responsibilities:
- Entity eligibility checks (draft-enabled, single row, active entity)
- Prediction element discovery (ValueList annotations)
- Context column analysis (type filtering)
- Context query construction (SELECT with ordering/limiting)
- Predict row building (null replacement with
[PREDICT])
- Synthetic key computation (composite key flattening)
- AI inference orchestration (client resolution + invocation)
- Prediction value type coercion (String → Integer/BigDecimal/etc.)
- Text path resolution (annotation introspection for descriptions)
- Final recommendations map assembly
In cds-services, handlers delegate complex logic to focused helper classes (e.g., RemoteODataHandler → UriGenerator, ODataDataUtils).
Additionally, entitiesWithoutPredictions uses ConcurrentHashMap.newKeySet() which grows unboundedly — a memory risk in multi-tenant/extensibility scenarios.
Plan
Step 1: Extract RecommendationContextBuilder
Handles responsibilities 2-6:
class RecommendationContextBuilder {
private final CdsStructuredType target;
private final CdsStructuredType rowType;
private final int contextRowLimit;
RecommendationContextBuilder(CdsStructuredType target, CdsStructuredType rowType, int limit) { ... }
List<String> predictionElementNames() { ... } // from lines 113-125
List<String> contextColumns() { ... } // from lines 127-140
CqnSelect buildContextQuery() { ... } // from lines 142-176
CdsData buildPredictRow(CdsData row) { ... } // from lines 225-236
String computeSyntheticKey(Map<String, Object> row) { ... } // from lines 238-252
String indexColumn() { ... }
boolean syntheticKeyNeeded() { ... }
}
Move SUPPORTED_CONTEXT_TYPES EnumSet into this class.
Step 2: Extract RecommendationResultParser
Handles responsibilities 8-10:
class RecommendationResultParser {
Map<String, Object> buildRecommendations(
PersistenceService db, CdsData prediction,
List<String> predictionElementNames,
CdsReadEventContext context, CdsStructuredType rowType) { ... }
// Private helpers:
// - parseValue(Object, CdsBaseType)
// - resolveTextPaths(...)
// - buildFkToAssociationMap(...)
// - resolveDescriptionsBatch(...)
// - getTextPath(...)
}
Step 3: Simplify Handler to Orchestrator
FioriRecommendationHandler.afterRead() becomes ~80-100 lines:
@After(entity = "*")
public void afterRead(CdsReadEventContext context, List<CdsData> dataList) {
// Quick bail-outs (null, cache, size, draft, active) ...
var builder = new RecommendationContextBuilder(target, rowType, limit);
if (builder.predictionElementNames().isEmpty()) {
entitiesWithoutPredictions.put(entityName, Boolean.TRUE);
return;
}
List<CdsData> contextRows = db.run(builder.buildContextQuery()).list();
if (contextRows.size() < 2) return;
CdsData predictRow = builder.buildPredictRow(row);
if (predictRow == null) return;
List<CdsData> allRows = builder.assembleRows(contextRows, predictRow, row);
RecommendationClient client = clientResolver.resolve(aiCoreService, tenantId);
List<CdsData> predictions = client.predict(allRows, builder.predictionElementNames(), builder.indexColumn());
// ... validate predictions ...
var parser = new RecommendationResultParser();
row.put("SAP_Recommendations", parser.buildRecommendations(db, predictions.get(0), missingCols, context, rowType));
}
Step 4: Bound the Negative Cache
// Before
private final Set<String> entitiesWithoutPredictions = ConcurrentHashMap.newKeySet();
// After
private final Cache<String, Boolean> entitiesWithoutPredictions =
Caffeine.newBuilder().maximumSize(10_000).build();
Update usages:
contains() → getIfPresent() != null
add() → put(key, Boolean.TRUE)
Files
- New:
RecommendationContextBuilder.java
- New:
RecommendationResultParser.java
- Modified:
FioriRecommendationHandler.java (reduced to ~80-100 lines)
Verification
All existing tests for the recommendation module should pass unchanged. The refactoring is purely structural (extract method → extract class).
Summary
Extract the ~400-line
FioriRecommendationHandlerinto an orchestrator + focused helpers. Replace the unbounded negative-result cache with a bounded Caffeine cache.Problem
FioriRecommendationHandler(~400 lines) has 10 distinct responsibilities:[PREDICT])In
cds-services, handlers delegate complex logic to focused helper classes (e.g.,RemoteODataHandler→UriGenerator,ODataDataUtils).Additionally,
entitiesWithoutPredictionsusesConcurrentHashMap.newKeySet()which grows unboundedly — a memory risk in multi-tenant/extensibility scenarios.Plan
Step 1: Extract
RecommendationContextBuilderHandles responsibilities 2-6:
Move
SUPPORTED_CONTEXT_TYPESEnumSet into this class.Step 2: Extract
RecommendationResultParserHandles responsibilities 8-10:
Step 3: Simplify Handler to Orchestrator
FioriRecommendationHandler.afterRead()becomes ~80-100 lines:Step 4: Bound the Negative Cache
Update usages:
contains()→getIfPresent() != nulladd()→put(key, Boolean.TRUE)Files
RecommendationContextBuilder.javaRecommendationResultParser.javaFioriRecommendationHandler.java(reduced to ~80-100 lines)Verification
All existing tests for the recommendation module should pass unchanged. The refactoring is purely structural (extract method → extract class).