Skip to content

Commit 4852200

Browse files
author
Michal Zelencik
committed
Merge branch 'feature/heuristic-mappings'
# Conflicts: # model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/activities/MappingsSuggestionRemoteServiceCallActivityRun.java
2 parents 98075f3 + 18ba4da commit 4852200

21 files changed

+1667
-192
lines changed

model/smart-api/src/main/java/com/evolveum/midpoint/smart/api/SmartIntegrationService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ MappingsSuggestionType suggestMappings(
203203
ResourceObjectTypeIdentification typeIdentification,
204204
SchemaMatchResultType schemaMatch,
205205
Boolean isInbound,
206+
Boolean useAiService,
206207
@Nullable List<ItemPath> targetPathsToIgnore,
207208
@Nullable CurrentActivityState<?> activityState,
208209
Task task,

model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/MappingSuggestionOperationFactory.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.evolveum.midpoint.schema.result.OperationResult;
88
import com.evolveum.midpoint.smart.api.ServiceClient;
99
import com.evolveum.midpoint.smart.impl.wellknownschemas.WellKnownSchemaService;
10+
import com.evolveum.midpoint.smart.impl.mappings.heuristics.HeuristicRuleMatcher;
1011
import com.evolveum.midpoint.smart.impl.scoring.MappingsQualityAssessor;
1112
import com.evolveum.midpoint.task.api.Task;
1213
import com.evolveum.midpoint.util.exception.CommunicationException;
@@ -22,21 +23,30 @@ public class MappingSuggestionOperationFactory {
2223
private final MappingsQualityAssessor mappingsQualityAssessor;
2324
private final OwnedShadowsProvider ownedShadowsProvider;
2425
private final WellKnownSchemaService wellKnownSchemaService;
26+
private final HeuristicRuleMatcher heuristicRuleMatcher;
2527

2628
public MappingSuggestionOperationFactory(MappingsQualityAssessor mappingsQualityAssessor,
2729
OwnedShadowsProvider ownedShadowsProvider,
28-
WellKnownSchemaService wellKnownSchemaService) {
30+
WellKnownSchemaService wellKnownSchemaService,
31+
HeuristicRuleMatcher heuristicRuleMatcher) {
2932
this.mappingsQualityAssessor = mappingsQualityAssessor;
3033
this.ownedShadowsProvider = ownedShadowsProvider;
3134
this.wellKnownSchemaService = wellKnownSchemaService;
35+
this.heuristicRuleMatcher = heuristicRuleMatcher;
3236
}
3337

3438
public MappingsSuggestionOperation create(ServiceClient client, String resourceOid,
3539
ResourceObjectTypeIdentification typeIdentification, CurrentActivityState<?> activityState,
36-
boolean isInbound, Task task, OperationResult parentResult)
40+
boolean isInbound, boolean useAiService, Task task, OperationResult parentResult)
3741
throws SchemaException, ExpressionEvaluationException, SecurityViolationException, CommunicationException,
3842
ConfigurationException, ObjectNotFoundException {
39-
return MappingsSuggestionOperation.init(client, resourceOid, typeIdentification, activityState,
40-
this.mappingsQualityAssessor, this.ownedShadowsProvider, this.wellKnownSchemaService, isInbound, task, parentResult);
43+
return MappingsSuggestionOperation.init(
44+
TypeOperationContext.init(client, resourceOid, typeIdentification, activityState, task, parentResult),
45+
this.mappingsQualityAssessor,
46+
this.ownedShadowsProvider,
47+
this.wellKnownSchemaService,
48+
this.heuristicRuleMatcher,
49+
isInbound,
50+
useAiService);
4151
}
4252
}

model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/MappingsSuggestionOperation.java

Lines changed: 182 additions & 71 deletions
Large diffs are not rendered by default.

model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/SmartIntegrationServiceImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ public MappingsSuggestionType suggestMappings(
576576
ResourceObjectTypeIdentification typeIdentification,
577577
SchemaMatchResultType schemaMatch,
578578
Boolean isInbound,
579+
Boolean useAiService,
579580
@Nullable List<ItemPath> targetPathsToIgnore,
580581
@Nullable CurrentActivityState<?> activityState,
581582
Task task,
@@ -589,7 +590,7 @@ public MappingsSuggestionType suggestMappings(
589590
.build();
590591
try (var serviceClient = this.clientFactory.getServiceClient(result)) {
591592
var mappings = this.mappingSuggestionOperationFactory.create(serviceClient, resourceOid,
592-
typeIdentification, activityState, isInbound, task, result)
593+
typeIdentification, activityState, isInbound, useAiService, task, result)
593594
.suggestMappings(result, schemaMatch, targetPathsToIgnore);
594595
LOGGER.debug("Suggested mappings:\n{}", mappings.debugDumpLazily(1));
595596
return mappings;

model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/activities/MappingsSuggestionRemoteServiceCallActivityRun.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ protected MappingsSuggestionRemoteServiceCallActivityRun(@NotNull ActivityRunIns
4444
var isInbound = getWorkDefinition().isInbound();
4545

4646
var suggestedMappings = SmartIntegrationBeans.get().smartIntegrationService.suggestMappings(
47-
resourceOid, typeDef, schemaMatch, isInbound, targetPathsToIgnore, state, task, result);
47+
resourceOid, typeDef, schemaMatch, isInbound, true, targetPathsToIgnore, state, task, result);
4848

4949
parentState.setWorkStateItemRealValues(MappingsSuggestionWorkStateType.F_RESULT, suggestedMappings);
5050
parentState.flushPendingTaskModifications(result);

model/smart-impl/src/main/java/com/evolveum/midpoint/smart/impl/mappings/ValuesPairSample.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public boolean isSourceDataMissing(float missingDataThreshold) {
7575
/**
7676
* Checks if all source values match their corresponding target values after type conversion.
7777
*/
78-
public boolean doAllConvertedSourcesMatchTargets(
78+
public boolean allSourcesMatchTargets(
7979
PrismObjectDefinition<?> focusTypeDefinition,
8080
ResourceObjectTypeDefinition objectTypeDefinition,
8181
Protector protector) {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) 2026 Evolveum and contributors
3+
*
4+
* Licensed under the EUPL-1.2 or later.
5+
*
6+
*/
7+
8+
package com.evolveum.midpoint.smart.impl.mappings.heuristics;
9+
10+
import com.evolveum.midpoint.smart.impl.mappings.ValuesPairSample;
11+
import com.evolveum.midpoint.xml.ns._public.common.common_3.ExpressionType;
12+
13+
import org.springframework.stereotype.Component;
14+
15+
/**
16+
* Heuristic that extracts the first word from the input.
17+
* Useful for extracting given names from full names or first part of compound identifiers.
18+
*/
19+
@Component
20+
public class FirstWordHeuristic implements HeuristicRule {
21+
22+
@Override
23+
public String getName() {
24+
return "firstWord";
25+
}
26+
27+
@Override
28+
public String getDescription() {
29+
return "Extract first word from input";
30+
}
31+
32+
/**
33+
* Only applicable if source values contain spaces (multi-word strings).
34+
*/
35+
@Override
36+
public boolean isApplicable(ValuesPairSample<?, ?> sample) {
37+
return sample.pairs().stream()
38+
.flatMap(pair -> pair.getSourceValues(sample.direction()).stream())
39+
.filter(value -> value instanceof String)
40+
.map(value -> (String) value)
41+
.anyMatch(str -> str != null && str.trim().contains(" "));
42+
}
43+
44+
@Override
45+
public ExpressionType inboundExpression(MappingExpressionFactory factory) {
46+
return factory.createScriptExpression(
47+
"input?.split('\\\\s+')[0]",
48+
"Extract first word");
49+
}
50+
51+
@Override
52+
public ExpressionType outboundExpression(String focusPropertyName, MappingExpressionFactory factory) {
53+
return factory.createScriptExpression(
54+
focusPropertyName + "?.split('\\\\s+')[0]",
55+
"Extract first word");
56+
}
57+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright (c) 2026 Evolveum and contributors
3+
*
4+
* Licensed under the EUPL-1.2 or later.
5+
*
6+
*/
7+
8+
package com.evolveum.midpoint.smart.impl.mappings.heuristics;
9+
10+
import com.evolveum.midpoint.xml.ns._public.common.common_3.ExpressionType;
11+
12+
/**
13+
* Result of attempting to apply a heuristic mapping.
14+
*/
15+
public record HeuristicResult(
16+
String heuristicName,
17+
ExpressionType expression,
18+
float quality) {
19+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright (c) 2026 Evolveum and contributors
3+
*
4+
* Licensed under the EUPL-1.2 or later.
5+
*
6+
*/
7+
8+
package com.evolveum.midpoint.smart.impl.mappings.heuristics;
9+
10+
import com.evolveum.midpoint.smart.impl.mappings.ValuesPairSample;
11+
import com.evolveum.midpoint.xml.ns._public.common.common_3.ExpressionType;
12+
13+
/**
14+
* Represents a simple transformation rule that can be applied to attribute mappings.
15+
* Rules are combined algorithmically by the heuristic manager to find the best fit.
16+
*
17+
* Rules are simple, fast transformations like toLowerCase(), trim(), etc.
18+
* that can be validated against sample data to determine if they produce correct mappings.
19+
*/
20+
public interface HeuristicRule {
21+
22+
/**
23+
* Returns a unique name for this rule (e.g., "toLowerCase", "trim").
24+
*/
25+
String getName();
26+
27+
/**
28+
* Returns a human-readable description of what this rule does.
29+
*/
30+
String getDescription();
31+
32+
/**
33+
* Checks if this rule is potentially applicable to the given sample data.
34+
* This is a quick pre-filter to avoid expensive quality assessment for obviously incompatible rules.
35+
*/
36+
boolean isApplicable(ValuesPairSample<?, ?> sample);
37+
38+
/**
39+
* Creates the expression for inbound mapping using the provided factory.
40+
* Uses 'input' as the variable name for the source shadow attribute.
41+
* Returns null if this rule represents "asIs" mapping (no transformation).
42+
*
43+
* @param factory the factory to create the expression
44+
* @return the expression or null for no transformation
45+
*/
46+
ExpressionType inboundExpression(MappingExpressionFactory factory);
47+
48+
/**
49+
* Creates the expression for outbound mapping using the provided factory.
50+
* Uses the actual focus property name as the variable (e.g., 'personalNumber', 'givenName').
51+
* Returns null if this rule represents "asIs" mapping (no transformation).
52+
*
53+
* @param focusPropertyName the name of the focus property
54+
* @param factory the factory to create the expression
55+
* @return the expression or null for no transformation
56+
*/
57+
ExpressionType outboundExpression(String focusPropertyName, MappingExpressionFactory factory);
58+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright (c) 2026 Evolveum and contributors
3+
*
4+
* Licensed under the EUPL-1.2 or later.
5+
*
6+
*/
7+
8+
package com.evolveum.midpoint.smart.impl.mappings.heuristics;
9+
10+
import java.util.List;
11+
import java.util.Optional;
12+
13+
import com.evolveum.midpoint.schema.result.OperationResult;
14+
import com.evolveum.midpoint.smart.impl.mappings.MappingDirection;
15+
import com.evolveum.midpoint.smart.impl.mappings.ValuesPairSample;
16+
import com.evolveum.midpoint.smart.impl.scoring.MappingsQualityAssessor;
17+
import com.evolveum.midpoint.task.api.Task;
18+
import com.evolveum.midpoint.util.exception.ExpressionEvaluationException;
19+
import com.evolveum.midpoint.util.exception.SecurityViolationException;
20+
import com.evolveum.midpoint.util.logging.Trace;
21+
import com.evolveum.midpoint.util.logging.TraceManager;
22+
import com.evolveum.midpoint.xml.ns._public.common.common_3.ExpressionType;
23+
import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectFactory;
24+
import com.evolveum.midpoint.xml.ns._public.common.common_3.ScriptExpressionEvaluatorType;
25+
26+
import org.springframework.stereotype.Component;
27+
28+
@Component
29+
public class HeuristicRuleMatcher {
30+
31+
private static final Trace LOGGER = TraceManager.getTrace(HeuristicRuleMatcher.class);
32+
33+
private final List<HeuristicRule> rules;
34+
private final MappingsQualityAssessor qualityAssessor;
35+
36+
public HeuristicRuleMatcher(List<HeuristicRule> rules, MappingsQualityAssessor qualityAssessor) {
37+
this.rules = rules;
38+
this.qualityAssessor = qualityAssessor;
39+
}
40+
41+
/**
42+
* Static factory method for creating script expressions.
43+
* Used as a method reference passed to heuristic rules.
44+
*/
45+
private static ExpressionType createScriptExpression(String groovyCode, String description) {
46+
return new ExpressionType()
47+
.description(description)
48+
.expressionEvaluator(
49+
new ObjectFactory().createScript(
50+
new ScriptExpressionEvaluatorType().code(groovyCode)));
51+
}
52+
53+
/**
54+
* Attempts to find the best heuristic rule.
55+
* All rules are evaluated, and the one with the highest quality (higher than zero) is returned.
56+
*/
57+
public Optional<HeuristicResult> findBestMatch(ValuesPairSample<?, ?> sample, Task task, OperationResult result) {
58+
HeuristicRule bestRule = null;
59+
ExpressionType bestExpression = null;
60+
float bestQuality = 0f;
61+
62+
for (HeuristicRule rule : rules) {
63+
if (!rule.isApplicable(sample)) {
64+
continue;
65+
}
66+
67+
LOGGER.trace("Evaluating rule: {}", rule.getName());
68+
ExpressionType expression = generateExpression(rule, sample);
69+
70+
try {
71+
var assessment = qualityAssessor.assessMappingsQuality(sample, expression, task, result);
72+
if (assessment == null || assessment.status() != MappingsQualityAssessor.AssessmentStatus.OK) {
73+
continue;
74+
}
75+
76+
float quality = assessment.quality();
77+
LOGGER.debug("Rule '{}' quality: {}", rule.getName(), quality);
78+
79+
if (quality > bestQuality) {
80+
LOGGER.debug("New best rule '{}' with quality {}", rule.getName(), quality);
81+
bestRule = rule;
82+
bestExpression = expression;
83+
bestQuality = quality;
84+
}
85+
} catch (ExpressionEvaluationException | SecurityViolationException e) {
86+
LOGGER.debug("Rule '{}' evaluation failed: {}", rule.getName(), e.getMessage());
87+
}
88+
}
89+
90+
if (bestRule != null) {
91+
LOGGER.info("Best rule '{}' with quality {}", bestRule.getName(), bestQuality);
92+
return Optional.of(new HeuristicResult(bestRule.getName(), bestExpression, bestQuality));
93+
}
94+
95+
return Optional.empty();
96+
}
97+
98+
99+
private ExpressionType generateExpression(HeuristicRule rule, ValuesPairSample<?, ?> sample) {
100+
if (sample.direction() == MappingDirection.INBOUND) {
101+
return rule.inboundExpression(HeuristicRuleMatcher::createScriptExpression);
102+
} else {
103+
String focusPropertyName = sample.focusPropertyPath().lastName().getLocalPart();
104+
return rule.outboundExpression(focusPropertyName, HeuristicRuleMatcher::createScriptExpression);
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)