|
21 | 21 | */ |
22 | 22 |
|
23 | 23 | import build.base.foundation.Lazy; |
| 24 | +import build.base.graph.Graph; |
| 25 | +import build.base.graph.Graphs; |
24 | 26 | import build.codemodel.foundation.usage.GenericTypeUsage; |
25 | 27 | import build.codemodel.foundation.usage.NamedTypeUsage; |
26 | 28 | import build.codemodel.foundation.usage.TypeUsage; |
@@ -422,6 +424,122 @@ public <T> T create(final Dependency dependency) |
422 | 424 | .orElseThrow(() -> new UnsatisfiedDependencyException(dependency)); |
423 | 425 | } |
424 | 426 |
|
| 427 | + @Override |
| 428 | + public Context validate() { |
| 429 | + // Build a directed dependency graph over all ClassBindings. |
| 430 | + // Edge: binding.dependency() → each of its injected dependencies. |
| 431 | + final var graphBuilder = Graph.<Dependency>directed(); |
| 432 | + |
| 433 | + this.bindingsByDependency.values().stream() |
| 434 | + .filter(ClassBinding.class::isInstance) |
| 435 | + .map(b -> (ClassBinding<?>) b) |
| 436 | + .forEach(classBinding -> { |
| 437 | + final var bindingDep = classBinding.dependency(); |
| 438 | + graphBuilder.addVertex(bindingDep); |
| 439 | + this.injectionFramework.getInjectableDescriptor(classBinding.concreteClass()) |
| 440 | + .injectionPoints() |
| 441 | + .flatMap(InjectionPoint::dependencies) |
| 442 | + .forEach(dep -> graphBuilder.addEdge(bindingDep, dep)); |
| 443 | + }); |
| 444 | + |
| 445 | + final var graph = graphBuilder.build(); |
| 446 | + |
| 447 | + // 1. Cycle detection |
| 448 | + Graphs.findCycle(graph) |
| 449 | + .ifPresent(cycle -> { |
| 450 | + throw new CyclicDependencyException(cycle); |
| 451 | + }); |
| 452 | + |
| 453 | + // 2. Unsatisfied dependency and 3. Scope violation detection |
| 454 | + final var problems = new ArrayList<String>(); |
| 455 | + |
| 456 | + graph.edges().forEach(edge -> { |
| 457 | + final var dep = edge.to(); |
| 458 | + |
| 459 | + // Unsatisfied: no explicit binding and not auto-resolvable |
| 460 | + if (!isResolvable(dep)) { |
| 461 | + problems.add("Unsatisfied dependency: [" + dep + "]" |
| 462 | + + " required by [" + edge.from() + "]"); |
| 463 | + } |
| 464 | + |
| 465 | + // Scope violation: @Singleton depends on NonSingletonClassBinding (prototype) |
| 466 | + final var fromBinding = this.bindingsByDependency.get(edge.from()); |
| 467 | + final var toBinding = this.bindingsByDependency.get(dep); |
| 468 | + if (fromBinding instanceof LazySingletonClassBinding |
| 469 | + && toBinding instanceof NonSingletonClassBinding) { |
| 470 | + problems.add("Scope violation: singleton [" + edge.from() |
| 471 | + + "] depends on prototype-scoped [" + dep + "]"); |
| 472 | + } |
| 473 | + }); |
| 474 | + |
| 475 | + if (!problems.isEmpty()) { |
| 476 | + throw new ValidationException(problems); |
| 477 | + } |
| 478 | + |
| 479 | + return this; |
| 480 | + } |
| 481 | + |
| 482 | + /** |
| 483 | + * Returns {@code true} if the given {@link Dependency} can be satisfied — either by an explicit or |
| 484 | + * multibinding already registered, or by an auto-bindable {@link jakarta.inject.Singleton} class. |
| 485 | + */ |
| 486 | + private boolean isResolvable(final Dependency dependency) { |
| 487 | + if (resolver().resolve(dependency).isPresent()) { |
| 488 | + return true; |
| 489 | + } |
| 490 | + // auto-singleton: a @Singleton class that can be bound on-demand |
| 491 | + if (dependency.typeUsage() instanceof NamedTypeUsage namedTypeUsage) { |
| 492 | + try { |
| 493 | + final var clazz = Class.forName(namedTypeUsage.typeName().canonicalName()); |
| 494 | + return this.injectionFramework.codeModel() |
| 495 | + .getJDKTypeDescriptor(clazz) |
| 496 | + .map(this.injectionFramework::isSingleton) |
| 497 | + .orElse(false); |
| 498 | + } catch (final ClassNotFoundException ignored) { |
| 499 | + return false; |
| 500 | + } |
| 501 | + } |
| 502 | + return false; |
| 503 | + } |
| 504 | + |
| 505 | + @Override |
| 506 | + @SuppressWarnings("unchecked") |
| 507 | + public Context initializeEagerSingletons() { |
| 508 | + // Collect all explicitly-bound singleton class bindings |
| 509 | + final var singletonBindings = this.bindingsByDependency.values().stream() |
| 510 | + .filter(LazySingletonClassBinding.class::isInstance) |
| 511 | + .map(b -> (LazySingletonClassBinding<Object>) b) |
| 512 | + .collect(Collectors.toList()); |
| 513 | + |
| 514 | + if (singletonBindings.isEmpty()) { |
| 515 | + return this; |
| 516 | + } |
| 517 | + |
| 518 | + // Build a dependency graph limited to singleton vertices so we can find initialization order |
| 519 | + final var singletonDeps = singletonBindings.stream() |
| 520 | + .map(Binding::dependency) |
| 521 | + .collect(Collectors.toSet()); |
| 522 | + |
| 523 | + final var graphBuilder = Graph.<Dependency>directed(); |
| 524 | + singletonBindings.forEach(b -> { |
| 525 | + final var bindingDep = b.dependency(); |
| 526 | + graphBuilder.addVertex(bindingDep); |
| 527 | + this.injectionFramework.getInjectableDescriptor(b.concreteClass()) |
| 528 | + .injectionPoints() |
| 529 | + .flatMap(InjectionPoint::dependencies) |
| 530 | + .filter(singletonDeps::contains) |
| 531 | + .forEach(dep -> graphBuilder.addEdge(bindingDep, dep)); |
| 532 | + }); |
| 533 | + |
| 534 | + final var graph = graphBuilder.build(); |
| 535 | + |
| 536 | + // Initialize each parallelizable layer; within a layer, initialize concurrently |
| 537 | + Graphs.parallelizableGroups(graph).forEach(layer -> |
| 538 | + layer.parallelStream().forEach(dep -> create(dep))); |
| 539 | + |
| 540 | + return this; |
| 541 | + } |
| 542 | + |
425 | 543 | @Override |
426 | 544 | public Context newContext() { |
427 | 545 | return this.injectionFramework.newContext(this.resolver()); |
|
0 commit comments