diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java index 5368638..c03bbfd 100644 --- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/HeuristicBeanResolutionEngine.java @@ -139,77 +139,9 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { return false; } - String targetVar = hasExplicitBeanTarget ? explicitTarget : chain.getContextMachineId(); - if (targetVar == null && chain.getTriggerPoint() != null) { - targetVar = chain.getTriggerPoint().getStateMachineId(); - } - - String simplifiedMachineName = currentMachineName != null ? currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1).toLowerCase() : ""; - - if (targetVar != null && !targetVar.isEmpty()) { - targetVar = targetVar.toLowerCase(); - if (targetVar.endsWith("statemachine")) { - String prefix = targetVar.substring(0, targetVar.length() - "statemachine".length()); - if (!prefix.isEmpty()) { - if (simplifiedMachineName.contains(prefix)) { - return true; // Explicit positive match - } else { - return false; // Explicit negative match - } - } - } else if (simplifiedMachineName.contains(targetVar)) { - return true; - } - } - - if (chain.getMethodChain() != null && !chain.getMethodChain().isEmpty() && currentMachineName != null) { - String smPackage = getPackageName(currentMachineName); - String smSimple = getSimpleClassName(currentMachineName); - String smPrefix = getFirstCamelCaseWord(smSimple); - - boolean hasPositiveMatch = false; - boolean hasStrongMismatch = false; - - if (chain.getTriggerPoint() != null && chain.getTriggerPoint().getClassName() != null) { - String chainClass = chain.getTriggerPoint().getClassName(); - String chainPackage = getPackageName(chainClass); - String chainSimple = getSimpleClassName(chainClass); - String chainPrefix = getFirstCamelCaseWord(chainSimple); - - if (smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix)) { - hasPositiveMatch = true; - } - - if (isDomainMatch(smPackage, chainPackage, smPrefix, chainPrefix)) { - hasPositiveMatch = true; - } - } - - for (String method : chain.getMethodChain()) { - String chainClass = getClassNameOnly(method); - String chainPackage = getPackageName(chainClass); - String chainSimple = getSimpleClassName(chainClass); - String chainPrefix = getFirstCamelCaseWord(chainSimple); - - if (smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix)) { - hasPositiveMatch = true; - } - - if (isDomainMatch(smPackage, chainPackage, smPrefix, chainPrefix)) { - hasPositiveMatch = true; - } - - if (isDomainMismatch(smPackage, chainPackage, smPrefix, chainPrefix, smSimple, chainSimple)) { - hasStrongMismatch = true; - } - } - - if (hasPositiveMatch) { - return true; - } - if (hasStrongMismatch) { - return false; - } + Boolean packageMatch = PackageNameRoutingHeuristics.matches(chain, currentMachineName, explicitTarget); + if (packageMatch != null) { + return packageMatch; } return context == null || isSingleStateMachine(context); } @@ -344,160 +276,6 @@ public class HeuristicBeanResolutionEngine implements BeanResolutionEngine { java.util.Set.of("StateMachine", "StateMachineFactory", "StateMachineModelFactory")).size(); } - private boolean isDomainMatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) { - if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) { - return false; - } - - if (smPackage.equals(chainPackage)) { - return true; - } - - if (!chainPackage.startsWith(smPackage + ".") && !smPackage.startsWith(chainPackage + ".")) { - return false; - } - - // Find deepest common package root - String[] p1 = smPackage.split("\\."); - String[] p2 = chainPackage.split("\\."); - int matchIdx = -1; - for (int i = 0; i < Math.min(p1.length, p2.length); i++) { - if (p1[i].equals(p2[i])) { - matchIdx = i; - } else { - break; - } - } - - // If the state machine's prefix is part of the shared common package, - // then the state machine represents the parent domain of the caller. - if (smPrefix != null && matchIdx >= 0) { - String lowerPrefix = smPrefix.toLowerCase(); - for (int i = 0; i <= matchIdx; i++) { - if (p1[i].toLowerCase().contains(lowerPrefix) || lowerPrefix.contains(p1[i].toLowerCase())) { - return true; - } - } - } - - return false; - } - - private boolean isDomainMismatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix, String smSimple, String chainSimple) { - if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) return false; - if (smPrefix == null || chainPrefix == null) return false; - - String smLower = smPrefix.toLowerCase(); - String chainLower = chainPrefix.toLowerCase(); - - // Ignore generic/technical class prefixes - if (smLower.equals("state") || smLower.equals("statemachine") || smLower.equals("config") || smLower.equals("configuration") || smLower.equals("adapter") || - smLower.equals("enterprise") || smLower.equals("app") || smLower.equals("global") || smLower.equals("core") || smLower.equals("project") || smLower.equals("main") || smLower.equals("base") || smLower.equals("common") || smLower.equals("shared") || - chainLower.equals("state") || chainLower.equals("statemachine") || chainLower.equals("config") || chainLower.equals("configuration") || chainLower.equals("adapter") || - chainLower.equals("enterprise") || chainLower.equals("app") || chainLower.equals("global") || chainLower.equals("core") || chainLower.equals("project") || chainLower.equals("main") || chainLower.equals("base") || chainLower.equals("common") || chainLower.equals("shared")) { - return false; - } - - // If class prefixes diverge, check if they are from different domains - if (!smLower.equals(chainLower)) { - // If one prefix is present as a domain/feature segment in the other package, they are related - if (chainPackage.toLowerCase().contains(smLower) || smPackage.toLowerCase().contains(chainLower)) { - return false; - } - - // Otherwise, if they diverge under a shared project package root, it is a mismatch - String[] p1 = smPackage.split("\\."); - String[] p2 = chainPackage.split("\\."); - int matchIdx = -1; - for (int i = 0; i < Math.min(p1.length, p2.length); i++) { - if (p1[i].equals(p2[i])) { - matchIdx = i; - } else { - break; - } - } - if (matchIdx >= 1) { - // If they diverge under a shared root, check if the divergence is only on generic package layers - boolean onlyGenericDivergence = true; - Set genericLayers = Set.of( - "config", "configuration", "service", "services", "impl", "api", "web", - "controller", "controllers", "messaging", "listener", "listeners", - "repository", "repositories", "db", "model", "models", "domain", - "constants", "utils", "helper", "helpers", "event", "events", - "state", "states", "transition", "transitions" - ); - for (int i = matchIdx + 1; i < p1.length; i++) { - if (!genericLayers.contains(p1[i].toLowerCase())) { - onlyGenericDivergence = false; - break; - } - } - if (onlyGenericDivergence) { - for (int i = matchIdx + 1; i < p2.length; i++) { - if (!genericLayers.contains(p2[i].toLowerCase())) { - onlyGenericDivergence = false; - break; - } - } - } - - if (!onlyGenericDivergence) { - return true; // Mismatch under a shared root - } - } - } - - return false; - } - - private Set extractDomainTerms(String pkg, String prefix) { - Set terms = new HashSet<>(); - if (pkg != null && !pkg.isEmpty()) { - String[] segments = pkg.split("\\."); - // Take segments from index 2 onwards if length > 2 - int start = segments.length > 2 ? 2 : 0; - for (int i = start; i < segments.length; i++) { - terms.add(segments[i].toLowerCase()); - } - } - if (prefix != null && !prefix.isEmpty()) { - terms.add(prefix.toLowerCase()); - } - return terms; - } - - private String getPackageName(String fqn) { - if (fqn == null || !fqn.contains(".")) return ""; - return fqn.substring(0, fqn.lastIndexOf('.')); - } - - private String getSimpleClassName(String fqn) { - if (fqn == null) return ""; - if (!fqn.contains(".")) return fqn; - return fqn.substring(fqn.lastIndexOf('.') + 1); - } - - private String getClassNameOnly(String methodFqn) { - if (methodFqn == null) return ""; - String clean = methodFqn; - if (clean.contains("(")) { - clean = clean.substring(0, clean.indexOf('(')); - } - if (clean.contains(".")) { - clean = clean.substring(0, clean.lastIndexOf('.')); - } - return clean; - } - - private String getFirstCamelCaseWord(String simpleName) { - if (simpleName == null || simpleName.isEmpty()) return null; - String[] words = simpleName.split("(? 0) { - return words[0]; - } - return null; - } - private String eraseGenerics(String type) { if (type == null) return null; int idx = type.indexOf('<'); diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/PackageNameRoutingHeuristics.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/PackageNameRoutingHeuristics.java new file mode 100644 index 0000000..c2d67e6 --- /dev/null +++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/routing/PackageNameRoutingHeuristics.java @@ -0,0 +1,256 @@ +package click.kamil.springstatemachineexporter.analysis.enricher.routing; + +import click.kamil.springstatemachineexporter.analysis.model.CallChain; + +import java.util.Set; + +/** + * Legacy package-prefix and domain-name matching for single-machine / null-context fallback routing. + */ +final class PackageNameRoutingHeuristics { + + private PackageNameRoutingHeuristics() { + } + + /** + * @return true/false if package heuristics decide, null if inconclusive + */ + static Boolean matches(CallChain chain, String currentMachineName, String explicitBeanTarget) { + if (chain == null) { + return null; + } + + boolean hasExplicitBeanTarget = explicitBeanTarget != null && !explicitBeanTarget.isEmpty(); + String targetVar = hasExplicitBeanTarget ? explicitBeanTarget : chain.getContextMachineId(); + if (targetVar == null && chain.getTriggerPoint() != null) { + targetVar = chain.getTriggerPoint().getStateMachineId(); + } + + String simplifiedMachineName = currentMachineName != null + ? currentMachineName.substring(currentMachineName.lastIndexOf('.') + 1).toLowerCase() + : ""; + + if (targetVar != null && !targetVar.isEmpty()) { + targetVar = targetVar.toLowerCase(); + if (targetVar.endsWith("statemachine")) { + String prefix = targetVar.substring(0, targetVar.length() - "statemachine".length()); + if (!prefix.isEmpty()) { + if (simplifiedMachineName.contains(prefix)) { + return true; + } else { + return false; + } + } + } else if (simplifiedMachineName.contains(targetVar)) { + return true; + } + } + + if (chain.getMethodChain() != null && !chain.getMethodChain().isEmpty() && currentMachineName != null) { + String smPackage = getPackageName(currentMachineName); + String smSimple = getSimpleClassName(currentMachineName); + String smPrefix = getFirstCamelCaseWord(smSimple); + + boolean hasPositiveMatch = false; + boolean hasStrongMismatch = false; + + if (chain.getTriggerPoint() != null && chain.getTriggerPoint().getClassName() != null) { + String chainClass = chain.getTriggerPoint().getClassName(); + String chainPackage = getPackageName(chainClass); + String chainSimple = getSimpleClassName(chainClass); + String chainPrefix = getFirstCamelCaseWord(chainSimple); + + if (smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix)) { + hasPositiveMatch = true; + } + + if (isDomainMatch(smPackage, chainPackage, smPrefix, chainPrefix)) { + hasPositiveMatch = true; + } + } + + for (String method : chain.getMethodChain()) { + String chainClass = getClassNameOnly(method); + String chainPackage = getPackageName(chainClass); + String chainSimple = getSimpleClassName(chainClass); + String chainPrefix = getFirstCamelCaseWord(chainSimple); + + if (smPrefix != null && chainPrefix != null && smPrefix.equals(chainPrefix)) { + hasPositiveMatch = true; + } + + if (isDomainMatch(smPackage, chainPackage, smPrefix, chainPrefix)) { + hasPositiveMatch = true; + } + + if (isDomainMismatch(smPackage, chainPackage, smPrefix, chainPrefix, smSimple, chainSimple)) { + hasStrongMismatch = true; + } + } + + if (hasPositiveMatch) { + return true; + } + if (hasStrongMismatch) { + return false; + } + } + return null; + } + + private static boolean isDomainMatch(String smPackage, String chainPackage, String smPrefix, String chainPrefix) { + if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) { + return false; + } + + if (smPackage.equals(chainPackage)) { + return true; + } + + if (!chainPackage.startsWith(smPackage + ".") && !smPackage.startsWith(chainPackage + ".")) { + return false; + } + + String[] p1 = smPackage.split("\\."); + String[] p2 = chainPackage.split("\\."); + int matchIdx = -1; + for (int i = 0; i < Math.min(p1.length, p2.length); i++) { + if (p1[i].equals(p2[i])) { + matchIdx = i; + } else { + break; + } + } + + if (smPrefix != null && matchIdx >= 0) { + String lowerPrefix = smPrefix.toLowerCase(); + for (int i = 0; i <= matchIdx; i++) { + if (p1[i].toLowerCase().contains(lowerPrefix) || lowerPrefix.contains(p1[i].toLowerCase())) { + return true; + } + } + } + + return false; + } + + private static boolean isDomainMismatch( + String smPackage, + String chainPackage, + String smPrefix, + String chainPrefix, + String smSimple, + String chainSimple) { + if (smPackage == null || chainPackage == null || smPackage.isEmpty() || chainPackage.isEmpty()) { + return false; + } + if (smPrefix == null || chainPrefix == null) { + return false; + } + + String smLower = smPrefix.toLowerCase(); + String chainLower = chainPrefix.toLowerCase(); + + if (smLower.equals("state") || smLower.equals("statemachine") || smLower.equals("config") + || smLower.equals("configuration") || smLower.equals("adapter") + || smLower.equals("enterprise") || smLower.equals("app") || smLower.equals("global") + || smLower.equals("core") || smLower.equals("project") || smLower.equals("main") + || smLower.equals("base") || smLower.equals("common") || smLower.equals("shared") + || chainLower.equals("state") || chainLower.equals("statemachine") || chainLower.equals("config") + || chainLower.equals("configuration") || chainLower.equals("adapter") + || chainLower.equals("enterprise") || chainLower.equals("app") || chainLower.equals("global") + || chainLower.equals("core") || chainLower.equals("project") || chainLower.equals("main") + || chainLower.equals("base") || chainLower.equals("common") || chainLower.equals("shared")) { + return false; + } + + if (!smLower.equals(chainLower)) { + if (chainPackage.toLowerCase().contains(smLower) || smPackage.toLowerCase().contains(chainLower)) { + return false; + } + + String[] p1 = smPackage.split("\\."); + String[] p2 = chainPackage.split("\\."); + int matchIdx = -1; + for (int i = 0; i < Math.min(p1.length, p2.length); i++) { + if (p1[i].equals(p2[i])) { + matchIdx = i; + } else { + break; + } + } + if (matchIdx >= 1) { + boolean onlyGenericDivergence = true; + Set genericLayers = Set.of( + "config", "configuration", "service", "services", "impl", "api", "web", + "controller", "controllers", "messaging", "listener", "listeners", + "repository", "repositories", "db", "model", "models", "domain", + "constants", "utils", "helper", "helpers", "event", "events", + "state", "states", "transition", "transitions" + ); + for (int i = matchIdx + 1; i < p1.length; i++) { + if (!genericLayers.contains(p1[i].toLowerCase())) { + onlyGenericDivergence = false; + break; + } + } + if (onlyGenericDivergence) { + for (int i = matchIdx + 1; i < p2.length; i++) { + if (!genericLayers.contains(p2[i].toLowerCase())) { + onlyGenericDivergence = false; + break; + } + } + } + + if (!onlyGenericDivergence) { + return true; + } + } + } + + return false; + } + + private static String getPackageName(String fqn) { + if (fqn == null || !fqn.contains(".")) { + return ""; + } + return fqn.substring(0, fqn.lastIndexOf('.')); + } + + private static String getSimpleClassName(String fqn) { + if (fqn == null) { + return ""; + } + if (!fqn.contains(".")) { + return fqn; + } + return fqn.substring(fqn.lastIndexOf('.') + 1); + } + + private static String getClassNameOnly(String methodFqn) { + if (methodFqn == null) { + return ""; + } + String clean = methodFqn; + if (clean.contains("(")) { + clean = clean.substring(0, clean.indexOf('(')); + } + if (clean.contains(".")) { + clean = clean.substring(0, clean.lastIndexOf('.')); + } + return clean; + } + + private static String getFirstCamelCaseWord(String simpleName) { + if (simpleName == null || simpleName.isEmpty()) { + return null; + } + String[] words = simpleName.split("(? 0) { + return words[0]; + } + return null; + } +}