|
1 | 1 | package com.jaipilot.cli.files; |
2 | 2 |
|
| 3 | +import com.jaipilot.cli.classpath.BuildToolClassResolutionService; |
| 4 | +import com.jaipilot.cli.classpath.ClassResolutionResult; |
| 5 | +import com.jaipilot.cli.classpath.LocationKind; |
| 6 | +import com.jaipilot.cli.classpath.ResolutionOptions; |
| 7 | +import com.jaipilot.cli.classpath.ResolvedSource; |
3 | 8 | import com.jaipilot.cli.process.BuildTool; |
4 | 9 | import com.jaipilot.cli.util.JavaSourceFormatter; |
5 | 10 | import java.io.IOException; |
@@ -55,22 +60,30 @@ public final class ProjectFileService { |
55 | 60 | ); |
56 | 61 |
|
57 | 62 | private final List<Path> dependencySourceSearchRoots; |
| 63 | + private final ContextSourceResolver contextSourceResolver; |
58 | 64 | private final Map<String, String> dependencySourceContentCache = new HashMap<>(); |
59 | 65 | private final Set<String> missingDependencySourcePaths = new HashSet<>(); |
60 | 66 | private List<Path> dependencySourceJars; |
61 | 67 |
|
62 | 68 | public ProjectFileService() { |
63 | | - this(defaultDependencySourceSearchRoots()); |
| 69 | + this(defaultDependencySourceSearchRoots(), defaultContextSourceResolver()); |
64 | 70 | } |
65 | 71 |
|
66 | 72 | ProjectFileService(List<Path> dependencySourceSearchRoots) { |
| 73 | + this(dependencySourceSearchRoots, defaultContextSourceResolver()); |
| 74 | + } |
| 75 | + |
| 76 | + ProjectFileService(List<Path> dependencySourceSearchRoots, ContextSourceResolver contextSourceResolver) { |
67 | 77 | this.dependencySourceSearchRoots = dependencySourceSearchRoots == null |
68 | 78 | ? List.of() |
69 | 79 | : dependencySourceSearchRoots.stream() |
70 | 80 | .filter(path -> path != null && !path.toString().isBlank()) |
71 | 81 | .map(Path::normalize) |
72 | 82 | .distinct() |
73 | 83 | .toList(); |
| 84 | + this.contextSourceResolver = contextSourceResolver == null |
| 85 | + ? defaultContextSourceResolver() |
| 86 | + : contextSourceResolver; |
74 | 87 | } |
75 | 88 |
|
76 | 89 | public Path resolvePath(Path projectRoot, Path path) { |
@@ -298,7 +311,130 @@ private String readRequestedContextSource(Path projectRoot, Path preferredSource |
298 | 311 | return dependencySource.get().content(); |
299 | 312 | } |
300 | 313 |
|
301 | | - throw new IllegalStateException("Unable to resolve requested context class path " + requestedPath); |
| 314 | + Optional<DependencySource> classpathResolvedSource = resolveDependencySourceViaClasspathIfPresent( |
| 315 | + projectRoot, |
| 316 | + preferredSourcePath, |
| 317 | + requestedPath |
| 318 | + ); |
| 319 | + if (classpathResolvedSource.isPresent()) { |
| 320 | + return classpathResolvedSource.get().content(); |
| 321 | + } |
| 322 | + |
| 323 | + throw new IllegalStateException( |
| 324 | + "Unable to resolve requested context class path " + requestedPath |
| 325 | + + ". Checked workspace sources and dependency sources. " |
| 326 | + + "Ensure the class is on the module test classpath (profiles/build args) and dependency sources are available." |
| 327 | + ); |
| 328 | + } |
| 329 | + |
| 330 | + private Optional<DependencySource> resolveDependencySourceViaClasspathIfPresent( |
| 331 | + Path projectRoot, |
| 332 | + Path preferredSourcePath, |
| 333 | + String requestedPath |
| 334 | + ) { |
| 335 | + Optional<String> requestedFqcn = normalizeRequestedFqcn(requestedPath); |
| 336 | + if (requestedFqcn.isEmpty()) { |
| 337 | + return Optional.empty(); |
| 338 | + } |
| 339 | + |
| 340 | + Path normalizedProjectRoot = projectRoot.toAbsolutePath().normalize(); |
| 341 | + Path moduleRoot = resolveContextModuleRoot(normalizedProjectRoot, preferredSourcePath); |
| 342 | + try { |
| 343 | + Optional<ResolvedContextSource> resolved = contextSourceResolver.resolve( |
| 344 | + normalizedProjectRoot, |
| 345 | + moduleRoot, |
| 346 | + requestedFqcn.get() |
| 347 | + ); |
| 348 | + if (resolved.isEmpty()) { |
| 349 | + return Optional.empty(); |
| 350 | + } |
| 351 | + String content = resolved.get().content(); |
| 352 | + String resolvedContextPath = normalizeContextPath(resolved.get().contextPath()); |
| 353 | + if (!resolvedContextPath.isBlank()) { |
| 354 | + dependencySourceContentCache.put(resolvedContextPath, content); |
| 355 | + missingDependencySourcePaths.remove(resolvedContextPath); |
| 356 | + } |
| 357 | + |
| 358 | + String requestedContextPath = normalizeContextPath(requestedPath); |
| 359 | + if (!requestedContextPath.isBlank()) { |
| 360 | + dependencySourceContentCache.put(requestedContextPath, content); |
| 361 | + missingDependencySourcePaths.remove(requestedContextPath); |
| 362 | + } |
| 363 | + |
| 364 | + String contextPath = resolvedContextPath.isBlank() |
| 365 | + ? contextPathFromFqcn(requestedFqcn.get()) |
| 366 | + : resolvedContextPath; |
| 367 | + return Optional.of(new DependencySource(contextPath, content)); |
| 368 | + } catch (RuntimeException ignored) { |
| 369 | + return Optional.empty(); |
| 370 | + } |
| 371 | + } |
| 372 | + |
| 373 | + private Path resolveContextModuleRoot(Path projectRoot, Path preferredSourcePath) { |
| 374 | + Path normalizedProjectRoot = projectRoot.toAbsolutePath().normalize(); |
| 375 | + if (preferredSourcePath != null) { |
| 376 | + Path moduleRoot = findNearestBuildProjectRoot(preferredSourcePath); |
| 377 | + if (moduleRoot != null) { |
| 378 | + Path normalizedModuleRoot = moduleRoot.toAbsolutePath().normalize(); |
| 379 | + if (normalizedModuleRoot.startsWith(normalizedProjectRoot)) { |
| 380 | + return normalizedModuleRoot; |
| 381 | + } |
| 382 | + } |
| 383 | + } |
| 384 | + return normalizedProjectRoot; |
| 385 | + } |
| 386 | + |
| 387 | + private Optional<String> normalizeRequestedFqcn(String requestedPath) { |
| 388 | + if (requestedPath == null || requestedPath.isBlank()) { |
| 389 | + return Optional.empty(); |
| 390 | + } |
| 391 | + String normalized = normalizeContextPath(requestedPath); |
| 392 | + if (normalized.isBlank()) { |
| 393 | + return Optional.empty(); |
| 394 | + } |
| 395 | + |
| 396 | + String candidate = normalized.trim(); |
| 397 | + if (candidate.startsWith("import ")) { |
| 398 | + candidate = candidate.substring("import ".length()).trim(); |
| 399 | + } |
| 400 | + if (candidate.startsWith("static ")) { |
| 401 | + candidate = candidate.substring("static ".length()).trim(); |
| 402 | + if (!candidate.endsWith(".*")) { |
| 403 | + int lastDot = candidate.lastIndexOf('.'); |
| 404 | + if (lastDot > 0) { |
| 405 | + candidate = candidate.substring(0, lastDot); |
| 406 | + } |
| 407 | + } |
| 408 | + } |
| 409 | + if (candidate.endsWith(";")) { |
| 410 | + candidate = candidate.substring(0, candidate.length() - 1).trim(); |
| 411 | + } |
| 412 | + if (candidate.endsWith(".class")) { |
| 413 | + candidate = candidate.substring(0, candidate.length() - ".class".length()); |
| 414 | + } |
| 415 | + if (candidate.endsWith(".*")) { |
| 416 | + return Optional.empty(); |
| 417 | + } |
| 418 | + if (candidate.endsWith(".java")) { |
| 419 | + candidate = candidate.substring(0, candidate.length() - ".java".length()); |
| 420 | + } |
| 421 | + candidate = candidate.replace('\\', '.').replace('/', '.'); |
| 422 | + while (candidate.startsWith(".")) { |
| 423 | + candidate = candidate.substring(1); |
| 424 | + } |
| 425 | + if (candidate.isBlank()) { |
| 426 | + return Optional.empty(); |
| 427 | + } |
| 428 | + return Optional.of(candidate); |
| 429 | + } |
| 430 | + |
| 431 | + private String contextPathFromFqcn(String fqcn) { |
| 432 | + String normalized = fqcn == null ? "" : fqcn.trim(); |
| 433 | + int firstDollar = normalized.indexOf('$'); |
| 434 | + if (firstDollar >= 0) { |
| 435 | + normalized = normalized.substring(0, firstDollar); |
| 436 | + } |
| 437 | + return normalized.replace('.', '/') + ".java"; |
302 | 438 | } |
303 | 439 |
|
304 | 440 | private Optional<Path> resolveRequestedContextPathIfPresent(Path projectRoot, Path preferredSourcePath, String requestedPath) { |
@@ -927,6 +1063,48 @@ private static String firstNonBlank(String... values) { |
927 | 1063 | return null; |
928 | 1064 | } |
929 | 1065 |
|
| 1066 | + @FunctionalInterface |
| 1067 | + interface ContextSourceResolver { |
| 1068 | + Optional<ResolvedContextSource> resolve(Path projectRoot, Path moduleRoot, String requestedFqcn); |
| 1069 | + } |
| 1070 | + |
| 1071 | + record ResolvedContextSource(String contextPath, String content) { |
| 1072 | + } |
| 1073 | + |
| 1074 | + private static ContextSourceResolver defaultContextSourceResolver() { |
| 1075 | + BuildToolClassResolutionService classResolutionService = new BuildToolClassResolutionService(); |
| 1076 | + return (projectRoot, moduleRoot, requestedFqcn) -> { |
| 1077 | + ResolutionOptions options = new ResolutionOptions(List.of(), false, true); |
| 1078 | + ClassResolutionResult classResult = classResolutionService.locate( |
| 1079 | + requestedFqcn, |
| 1080 | + projectRoot, |
| 1081 | + moduleRoot, |
| 1082 | + options |
| 1083 | + ); |
| 1084 | + if (classResult.kind() == LocationKind.NOT_FOUND) { |
| 1085 | + return Optional.empty(); |
| 1086 | + } |
| 1087 | + Optional<ResolvedSource> resolvedSource = classResolutionService.resolveSource( |
| 1088 | + classResult, |
| 1089 | + projectRoot, |
| 1090 | + moduleRoot, |
| 1091 | + options |
| 1092 | + ); |
| 1093 | + if (resolvedSource.isEmpty()) { |
| 1094 | + return Optional.empty(); |
| 1095 | + } |
| 1096 | + |
| 1097 | + ResolvedSource source = resolvedSource.get(); |
| 1098 | + String contextPath = source.fqcn(); |
| 1099 | + int firstDollar = contextPath.indexOf('$'); |
| 1100 | + if (firstDollar >= 0) { |
| 1101 | + contextPath = contextPath.substring(0, firstDollar); |
| 1102 | + } |
| 1103 | + contextPath = contextPath.replace('.', '/') + ".java"; |
| 1104 | + return Optional.of(new ResolvedContextSource(contextPath, source.sourceText())); |
| 1105 | + }; |
| 1106 | + } |
| 1107 | + |
930 | 1108 | private record DependencySource(String contextPath, String content) { |
931 | 1109 | } |
932 | 1110 | } |
0 commit comments