001/* 002 * Copyright 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet; 018 019import com.soklet.Soklet.MockHttpServer; 020import com.soklet.Soklet.MockSseServer; 021import com.soklet.converter.ValueConverterRegistry; 022import org.jspecify.annotations.NonNull; 023import org.jspecify.annotations.Nullable; 024 025import javax.annotation.concurrent.NotThreadSafe; 026import javax.annotation.concurrent.ThreadSafe; 027import java.util.Collection; 028import java.util.concurrent.atomic.AtomicBoolean; 029import java.util.function.Consumer; 030 031import static com.soklet.internal.ObjectIdentity.sameInstance; 032import static java.util.Objects.requireNonNull; 033 034/** 035 * Opaque, single-use execution configuration consumed by one 036 * {@link SokletSimulator} run attempt. 037 * <p> 038 * Each {@link #builder()} and 039 * {@link #withSokletConfig(SokletConfig)} owns a fresh set of off-network 040 * transports. A successful {@link Builder#build()} seals that transport graph, and 041 * {@link SokletSimulator#run(SimulatorConfig, SokletSimulator.Simulation)} 042 * atomically claims the completed configuration before lifecycle work begins. 043 * Sequential or concurrent reuse is rejected. Create a new configuration for 044 * each simulation run. 045 * <p> 046 * Instances are thread-safe, but deliberately stateful: claiming a run is an 047 * irreversible operation. The configuration intentionally exposes no public 048 * accessors; configured transports are available from the run's 049 * {@link Simulator}. 050 */ 051@ThreadSafe 052public final class SimulatorConfig { 053 @NonNull 054 private final SokletConfig sokletConfig; 055 @NonNull 056 private final SimulatorOptions simulatorOptions; 057 @NonNull 058 private final ConfigurationGraph configurationGraph; 059 @NonNull 060 private final AtomicBoolean runClaimed; 061 062 private SimulatorConfig(@NonNull Builder builder) { 063 Builder exactBuilder = requireNonNull(builder); 064 this.sokletConfig = exactBuilder.sokletConfigBuilder.build(); 065 this.simulatorOptions = exactBuilder.simulatorOptions == null 066 ? SimulatorOptions.defaultInstance() 067 : exactBuilder.simulatorOptions; 068 this.configurationGraph = exactBuilder.configurationGraph; 069 this.runClaimed = new AtomicBoolean(); 070 } 071 072 /** 073 * Creates a builder backed by a fresh off-network transport graph. 074 * 075 * @return a fresh simulator-configuration builder 076 */ 077 @NonNull 078 public static Builder builder() { 079 return new Builder(); 080 } 081 082 /** 083 * Creates a builder by importing the application settings and transport 084 * shape of an existing Soklet configuration. 085 * <p> 086 * The returned builder owns a fresh off-network counterpart for each HTTP, 087 * SSE, or MCP transport present in the source; it never reuses or changes the 088 * source configuration's transport instances. 089 * Every transport present in the source remains present in the imported 090 * shape. Use {@link #builder()} instead when a test must omit one. 091 * Explicitly configured application collaborators are reused by identity, 092 * while unset defaults that depend on the completed configuration are 093 * derived again for the simulator configuration. MCP collaborators such as 094 * admission controllers, interceptors, localizers, rate limiters, the task 095 * manager, and its task-event publisher are also reused by identity; stateful 096 * ones can be replaced through 097 * {@link Builder#configureMcpServer(Consumer)}. Later calls on the returned 098 * builder override imported settings. 099 * <p> 100 * Derivation is transport-isolated, not a deep copy of the application object 101 * graph. A dependency-injection provider or other collaborator that captured 102 * a source transport is not inspected or rebound and continues to reference 103 * that source object. Supply test-scoped collaborators explicitly when that 104 * distinction matters. 105 * <p> 106 * An imported MCP server is reconstructed from its original build settings. 107 * Runtime control-plane changes made after that server was built, such as key 108 * rotation or localization invalidation, are deliberately not copied. 109 * HTTP and SSE streaming lifecycle capacity, callback concurrency, and cleanup 110 * timeout are copied from Soklet's built-in servers. The SSE connection queue 111 * capacity is also copied. Custom transports use the simulator's defaults. 112 * 113 * @param sokletConfig source application configuration 114 * @return a fresh, transport-isolated simulator-configuration builder 115 */ 116 @NonNull 117 public static Builder withSokletConfig( 118 @NonNull SokletConfig sokletConfig) { 119 return new Builder(requireNonNull(sokletConfig)); 120 } 121 122 /** 123 * Creates a completed, single-use simulator configuration from an existing 124 * Soklet configuration. 125 * <p> 126 * This is equivalent to {@code withSokletConfig(sokletConfig).build()}. 127 * Each invocation creates a fresh off-network transport graph, so the source 128 * configuration may be used to derive any number of transport-isolated 129 * simulator configurations. Application and MCP collaborators are still 130 * shared by identity as described by 131 * {@link #withSokletConfig(SokletConfig)}. 132 * 133 * @param sokletConfig source application configuration 134 * @return a fresh, completed, transport-isolated simulator configuration 135 */ 136 @NonNull 137 public static SimulatorConfig fromSokletConfig( 138 @NonNull SokletConfig sokletConfig) { 139 return withSokletConfig(sokletConfig).build(); 140 } 141 142 @NonNull 143 SokletConfig getSokletConfig() { 144 return this.sokletConfig; 145 } 146 147 @NonNull 148 SimulatorOptions getSimulatorOptions() { 149 return this.simulatorOptions; 150 } 151 152 void claimForRun() { 153 if (!this.runClaimed.compareAndSet(false, true)) 154 throw new IllegalStateException( 155 "The simulator configuration has already been claimed by a run"); 156 } 157 158 @NonNull 159 Object configurationIdentity() { 160 return this.configurationGraph.configurationIdentity(); 161 } 162 163 boolean belongsTo(@NonNull Object configurationIdentity) { 164 return sameInstance(this.configurationGraph.configurationIdentity(), 165 requireNonNull(configurationIdentity)); 166 } 167 168 @NonNull 169 MockHttpServer simulatedHttpServer() { 170 return this.configurationGraph.httpServer(); 171 } 172 173 @NonNull 174 MockSseServer simulatedSseServer() { 175 return this.configurationGraph.sseServer(); 176 } 177 178 @Nullable 179 DefaultMcpServer simulatedMcpServer() { 180 return this.configurationGraph.mcpServer(); 181 } 182 183 /** 184 * Builds a single-use configuration against fresh off-network transports. 185 * <p> 186 * A builder is sealed after a successful {@link #build()}. Builders are not 187 * thread-safe and must not be retained after building. 188 */ 189 @NotThreadSafe 190 public static final class Builder { 191 @NonNull 192 private final ConfigurationGraph configurationGraph; 193 private final SokletConfig.@NonNull Builder sokletConfigBuilder; 194 @Nullable 195 private SimulatorOptions simulatorOptions; 196 @Nullable 197 private DefaultMcpServer importedMcpServer; 198 private boolean built; 199 private int activeTransportConfigurers; 200 201 private Builder() { 202 this.configurationGraph = new ConfigurationGraph(null, null); 203 this.sokletConfigBuilder = new SokletConfig.Builder(); 204 } 205 206 private Builder(@NonNull SokletConfig sokletConfig) { 207 SokletConfig exactConfig = requireNonNull(sokletConfig); 208 this.configurationGraph = new ConfigurationGraph(exactConfig.getHttpServer().orElse(null), 209 exactConfig.getSseServer().orElse(null)); 210 this.sokletConfigBuilder = new SokletConfig.Builder(); 211 exactConfig.applyApplicationSettingsTo(this.sokletConfigBuilder); 212 if (exactConfig.getHttpServer().isPresent()) 213 httpServer(); 214 if (exactConfig.getSseServer().isPresent()) 215 sseServer(); 216 this.importedMcpServer = exactConfig.getMcpServer() 217 .map(server -> (DefaultMcpServer) server) 218 .orElse(null); 219 } 220 221 /** 222 * Adds this run's fresh simulated HTTP server. 223 * 224 * @return this builder 225 */ 226 @NonNull 227 public Builder httpServer() { 228 requireMutable(); 229 this.sokletConfigBuilder.httpServer( 230 this.configurationGraph.httpServer()); 231 return this; 232 } 233 234 /** 235 * Adds this run's fresh simulated HTTP server and supplies that exact 236 * server for configuration-time application dependency wiring. For access 237 * from the simulation body, prefer {@link Simulator#getHttpServer()}. 238 * The consumer runs synchronously during this call and must not retain the 239 * server for another configuration. 240 * 241 * @param httpServerConsumer receives this run's HTTP server 242 * @return this builder 243 */ 244 @NonNull 245 public Builder httpServer( 246 @NonNull Consumer<@NonNull HttpServer> httpServerConsumer) { 247 Consumer<@NonNull HttpServer> exactConsumer = 248 requireNonNull(httpServerConsumer); 249 requireMutable(); 250 beginTransportConfigurer(); 251 try { 252 exactConsumer.accept(this.configurationGraph.httpServer()); 253 } finally { 254 endTransportConfigurer(); 255 } 256 requireMutable(); 257 this.sokletConfigBuilder.httpServer( 258 this.configurationGraph.httpServer()); 259 return this; 260 } 261 262 /** 263 * Adds this run's fresh simulated Server-Sent Events server. 264 * 265 * @return this builder 266 */ 267 @NonNull 268 public Builder sseServer() { 269 requireMutable(); 270 this.sokletConfigBuilder.sseServer( 271 this.configurationGraph.sseServer()); 272 return this; 273 } 274 275 /** 276 * Adds this run's fresh simulated Server-Sent Events server and supplies 277 * that exact server for configuration-time application dependency wiring. 278 * For access from the simulation body, prefer 279 * {@link Simulator#getSseServer()}. The consumer runs synchronously during 280 * this call and must not retain the server for another configuration. 281 * 282 * @param sseServerConsumer receives this run's SSE server 283 * @return this builder 284 */ 285 @NonNull 286 public Builder sseServer( 287 @NonNull Consumer<@NonNull SseServer> sseServerConsumer) { 288 Consumer<@NonNull SseServer> exactConsumer = 289 requireNonNull(sseServerConsumer); 290 requireMutable(); 291 beginTransportConfigurer(); 292 try { 293 exactConsumer.accept(this.configurationGraph.sseServer()); 294 } finally { 295 endTransportConfigurer(); 296 } 297 requireMutable(); 298 this.sokletConfigBuilder.sseServer( 299 this.configurationGraph.sseServer()); 300 return this; 301 } 302 303 /** 304 * Configures, builds, and adds one fresh simulated MCP server. 305 * If this builder was created by 306 * {@link SimulatorConfig#withSokletConfig(SokletConfig)} and the source 307 * configuration contains an MCP server, configuration begins with that 308 * server's construction settings. Otherwise, configuration begins with 309 * {@link McpServer#withPort(Integer)} defaults and logical port {@code 0}. 310 * The logical port may be changed with 311 * {@link McpServer.Builder#port(Integer)}. 312 * <p> 313 * The consumer runs synchronously and must only customize the supplied 314 * builder. This outer builder owns the call to 315 * {@link McpServer.Builder#build()}; calling it from the consumer or 316 * retaining the builder for later use is rejected. 317 * 318 * @param mcpServerConfigurer customizes this configuration's MCP builder 319 * @return this builder 320 */ 321 @NonNull 322 public Builder configureMcpServer( 323 @NonNull Consumer<McpServer.@NonNull Builder> 324 mcpServerConfigurer) { 325 Consumer<McpServer.@NonNull Builder> exactConfigurer = 326 requireNonNull(mcpServerConfigurer); 327 requireMutable(); 328 DefaultMcpServer exactImportedMcpServer = this.importedMcpServer; 329 McpBuilderLease lease = exactImportedMcpServer == null 330 ? this.configurationGraph.openMcpBuilder(0) 331 : this.configurationGraph.openMcpBuilder( 332 exactImportedMcpServer); 333 return configureAndBuildMcpServer(lease, exactConfigurer); 334 } 335 336 @NonNull 337 private Builder configureAndBuildMcpServer( 338 @NonNull McpBuilderLease lease, 339 @NonNull Consumer<McpServer.@NonNull Builder> configurer) { 340 requireMutable(); 341 try { 342 McpServer.Builder mcpServerBuilder = lease.builder(); 343 beginTransportConfigurer(); 344 try { 345 requireNonNull(configurer).accept(mcpServerBuilder); 346 } finally { 347 endTransportConfigurer(); 348 } 349 requireMutable(); 350 lease.finishConfiguration(); 351 McpServer mcpServer = mcpServerBuilder.build(); 352 requireMutable(); 353 this.sokletConfigBuilder.mcpServer(mcpServer); 354 this.importedMcpServer = null; 355 return this; 356 } finally { 357 lease.close(); 358 } 359 } 360 361 /** 362 * Sets the startup and shutdown deadline policy shared by every configured 363 * lifecycle component. Passing {@code null} restores the built-in default. 364 * 365 * @param lifecyclePolicy lifecycle policy, or {@code null} to use the default 366 * @return this builder 367 */ 368 @NonNull 369 public Builder lifecyclePolicy( 370 @Nullable LifecyclePolicy lifecyclePolicy) { 371 requireMutable(); 372 this.sokletConfigBuilder.lifecyclePolicy(lifecyclePolicy); 373 return this; 374 } 375 376 @NonNull 377 Builder internalLifecyclePolicy( 378 @NonNull InternalLifecyclePolicy internalLifecyclePolicy) { 379 requireMutable(); 380 this.sokletConfigBuilder.internalLifecyclePolicy( 381 requireNonNull(internalLifecyclePolicy)); 382 return this; 383 } 384 385 /** 386 * Sets how Soklet creates application instances for HTTP, SSE, and MCP 387 * handlers and application parameter values. The provider may be called 388 * concurrently. 389 * 390 * @param instanceProvider instance provider, or {@code null} for the default 391 * @return this builder 392 */ 393 @NonNull 394 public Builder instanceProvider( 395 @Nullable InstanceProvider instanceProvider) { 396 requireMutable(); 397 this.sokletConfigBuilder.instanceProvider(instanceProvider); 398 return this; 399 } 400 401 /** 402 * Sets the registry used for Java value conversions. 403 * 404 * @param valueConverterRegistry registry, or {@code null} for the default 405 * @return this builder 406 */ 407 @NonNull 408 public Builder valueConverterRegistry( 409 @Nullable ValueConverterRegistry valueConverterRegistry) { 410 requireMutable(); 411 this.sokletConfigBuilder.valueConverterRegistry(valueConverterRegistry); 412 return this; 413 } 414 415 /** 416 * Sets how request bodies are converted to application values. 417 * 418 * @param requestBodyMarshaler marshaler, or {@code null} to derive one 419 * @return this builder 420 */ 421 @NonNull 422 public Builder requestBodyMarshaler( 423 @Nullable RequestBodyMarshaler requestBodyMarshaler) { 424 requireMutable(); 425 this.sokletConfigBuilder.requestBodyMarshaler(requestBodyMarshaler); 426 return this; 427 } 428 429 /** 430 * Sets how Soklet discovers and resolves Resource Methods. 431 * 432 * @param resourceMethodResolver resolver, or {@code null} for the default 433 * @return this builder 434 */ 435 @NonNull 436 public Builder resourceMethodResolver( 437 @Nullable ResourceMethodResolver resourceMethodResolver) { 438 requireMutable(); 439 this.sokletConfigBuilder.resourceMethodResolver(resourceMethodResolver); 440 return this; 441 } 442 443 /** 444 * Sets how parameters are supplied for Resource Method invocation. 445 * 446 * @param resourceMethodParameterProvider provider, or {@code null} for the 447 * default 448 * @return this builder 449 */ 450 @NonNull 451 public Builder resourceMethodParameterProvider( 452 @Nullable ResourceMethodParameterProvider 453 resourceMethodParameterProvider) { 454 requireMutable(); 455 this.sokletConfigBuilder.resourceMethodParameterProvider( 456 resourceMethodParameterProvider); 457 return this; 458 } 459 460 /** 461 * Sets how application response values are converted for transmission. 462 * 463 * @param responseMarshaler marshaler, or {@code null} for the default 464 * @return this builder 465 */ 466 @NonNull 467 public Builder responseMarshaler( 468 @Nullable ResponseMarshaler responseMarshaler) { 469 requireMutable(); 470 this.sokletConfigBuilder.responseMarshaler(responseMarshaler); 471 return this; 472 } 473 474 /** 475 * Sets the interceptor around application request handling. 476 * 477 * @param requestInterceptor interceptor, or {@code null} for the default 478 * @return this builder 479 */ 480 @NonNull 481 public Builder requestInterceptor( 482 @Nullable RequestInterceptor requestInterceptor) { 483 requireMutable(); 484 this.sokletConfigBuilder.requestInterceptor(requestInterceptor); 485 return this; 486 } 487 488 /** 489 * Replaces the configured lifecycle observers with one observer. 490 * 491 * @param lifecycleObserver observer, or {@code null} for no observers 492 * @return this builder 493 */ 494 @NonNull 495 public Builder lifecycleObserver( 496 @Nullable LifecycleObserver lifecycleObserver) { 497 requireMutable(); 498 this.sokletConfigBuilder.lifecycleObserver(lifecycleObserver); 499 return this; 500 } 501 502 /** 503 * Replaces the configured lifecycle observers in iteration order. 504 * 505 * @param lifecycleObservers non-null observers, or {@code null} for no 506 * observers; elements must be non-null 507 * @return this builder 508 */ 509 @NonNull 510 public Builder lifecycleObservers( 511 @Nullable Collection<? extends @NonNull LifecycleObserver> 512 lifecycleObservers) { 513 requireMutable(); 514 this.sokletConfigBuilder.lifecycleObservers(lifecycleObservers); 515 return this; 516 } 517 518 /** 519 * Sets how Soklet records operational metrics. 520 * 521 * @param metricsCollector collector, or {@code null} for the default 522 * @return this builder 523 */ 524 @NonNull 525 public Builder metricsCollector( 526 @Nullable MetricsCollector metricsCollector) { 527 requireMutable(); 528 this.sokletConfigBuilder.metricsCollector(metricsCollector); 529 return this; 530 } 531 532 /** 533 * Sets the authorizer for ordinary HTTP and SSE CORS processing. 534 * 535 * @param corsAuthorizer authorizer, or {@code null} to reject all 536 * cross-origin requests 537 * @return this builder 538 */ 539 @NonNull 540 public Builder corsAuthorizer( 541 @Nullable CorsAuthorizer corsAuthorizer) { 542 requireMutable(); 543 this.sokletConfigBuilder.corsAuthorizer(corsAuthorizer); 544 return this; 545 } 546 547 /** 548 * Sets simulator request and response behavior options. 549 * 550 * @param simulatorOptions options, or {@code null} for defaults 551 * @return this builder 552 */ 553 @NonNull 554 public Builder simulatorOptions( 555 @Nullable SimulatorOptions simulatorOptions) { 556 requireMutable(); 557 this.simulatorOptions = simulatorOptions; 558 return this; 559 } 560 561 /** 562 * Builds the single-use configuration for one simulator run attempt. 563 * 564 * @return completed simulator configuration 565 * @throws IllegalStateException if the builder is sealed, was already built, 566 * or has no simulated server 567 */ 568 @NonNull 569 public SimulatorConfig build() { 570 requireMutable(); 571 if (this.activeTransportConfigurers != 0) 572 throw new IllegalStateException( 573 "A simulator configuration cannot be built from a transport configurer"); 574 if (this.importedMcpServer != null) 575 configureMcpServer(ignored -> { 576 }); 577 SimulatorConfig config = new SimulatorConfig(this); 578 this.built = true; 579 this.configurationGraph.seal(); 580 return config; 581 } 582 583 private void requireMutable() { 584 if (this.built) 585 throw new IllegalStateException( 586 "The simulator configuration has already been built"); 587 this.configurationGraph.requireOpen(); 588 } 589 590 private void beginTransportConfigurer() { 591 this.activeTransportConfigurers++; 592 } 593 594 private void endTransportConfigurer() { 595 this.activeTransportConfigurers--; 596 } 597 } 598 599 @ThreadSafe 600 private static final class ConfigurationGraph { 601 @NonNull 602 private final Object configurationIdentity; 603 @NonNull 604 private final MockHttpServer httpServer; 605 @NonNull 606 private final MockSseServer sseServer; 607 private boolean open; 608 private @Nullable DefaultMcpServer mcpServer; 609 private @Nullable McpBuilderLease activeMcpBuilderLease; 610 611 private ConfigurationGraph(@Nullable HttpServer sourceHttpServer, @Nullable SseServer sourceSseServer) { 612 this.configurationIdentity = new Object(); 613 this.httpServer = new MockHttpServer(sourceHttpServer); 614 this.sseServer = new MockSseServer(sourceSseServer); 615 this.open = true; 616 } 617 618 @NonNull 619 private Object configurationIdentity() { 620 return this.configurationIdentity; 621 } 622 623 @NonNull 624 private MockHttpServer httpServer() { 625 return this.httpServer; 626 } 627 628 @NonNull 629 private MockSseServer sseServer() { 630 return this.sseServer; 631 } 632 633 @Nullable 634 private synchronized DefaultMcpServer mcpServer() { 635 return this.mcpServer; 636 } 637 638 @NonNull 639 private synchronized McpBuilderLease openMcpBuilder( 640 @NonNull Integer port) { 641 return openMcpBuilder(new McpBuilderLease(this, 642 requireNonNull(port))); 643 } 644 645 @NonNull 646 private synchronized McpBuilderLease openMcpBuilder( 647 @NonNull DefaultMcpServer sourceMcpServer) { 648 return openMcpBuilder(new McpBuilderLease(this, 649 requireNonNull(sourceMcpServer))); 650 } 651 652 @NonNull 653 private McpBuilderLease openMcpBuilder( 654 @NonNull McpBuilderLease lease) { 655 requireOpen(); 656 if (this.mcpServer != null) 657 throw new IllegalStateException( 658 "A simulator configuration may build at most one MCP server"); 659 if (this.activeMcpBuilderLease != null) 660 throw new IllegalStateException( 661 "A simulator MCP builder is already active"); 662 this.activeMcpBuilderLease = requireNonNull(lease); 663 return this.activeMcpBuilderLease; 664 } 665 666 private synchronized void verifyBuildAllowed( 667 @NonNull McpBuilderLease lease) { 668 if (this.activeMcpBuilderLease != requireNonNull(lease)) 669 throw new IllegalStateException( 670 "The simulator MCP builder is no longer active"); 671 requireOpen(); 672 if (!lease.buildAllowed()) 673 throw new IllegalStateException( 674 "Only SimulatorConfig.Builder may build the simulator MCP server"); 675 if (this.mcpServer != null) 676 throw new IllegalStateException( 677 "A simulator configuration may build at most one MCP server"); 678 } 679 680 private synchronized void register(@NonNull McpBuilderLease lease, 681 @NonNull DefaultMcpServer server) { 682 verifyBuildAllowed(lease); 683 DefaultMcpServer registered = requireNonNull(server); 684 registered.claimSimulatorScope(this); 685 this.mcpServer = registered; 686 } 687 688 private synchronized void closeLease(@NonNull McpBuilderLease lease) { 689 if (this.activeMcpBuilderLease == requireNonNull(lease)) 690 this.activeMcpBuilderLease = null; 691 } 692 693 private synchronized void seal() { 694 this.open = false; 695 } 696 697 private synchronized void requireOpen() { 698 if (!this.open) 699 throw new IllegalStateException( 700 "The simulator configuration has been sealed"); 701 } 702 } 703 704 @ThreadSafe 705 private static final class McpBuilderLease 706 implements SimulatorMcpBuildRegistrar, AutoCloseable { 707 @NonNull 708 private final ConfigurationGraph configurationGraph; 709 private final McpServer.@NonNull Builder builder; 710 private volatile @Nullable Thread buildThread; 711 712 private McpBuilderLease(@NonNull ConfigurationGraph configurationGraph, 713 @NonNull Integer port) { 714 this.configurationGraph = requireNonNull(configurationGraph); 715 this.builder = McpServer.withPort(requireNonNull(port)) 716 .simulatorBuildRegistrar(this); 717 } 718 719 private McpBuilderLease(@NonNull ConfigurationGraph configurationGraph, 720 @NonNull DefaultMcpServer sourceMcpServer) { 721 this.configurationGraph = requireNonNull(configurationGraph); 722 this.builder = requireNonNull(sourceMcpServer) 723 .copyBuilderForSimulator(this); 724 } 725 726 private McpServer.@NonNull Builder builder() { 727 return this.builder; 728 } 729 730 private void finishConfiguration() { 731 this.buildThread = Thread.currentThread(); 732 } 733 734 private boolean buildAllowed() { 735 return sameInstance(this.buildThread, Thread.currentThread()); 736 } 737 738 @Override 739 public void verifyBuildAllowed() { 740 this.configurationGraph.verifyBuildAllowed(this); 741 } 742 743 @Override 744 public void register(@NonNull DefaultMcpServer server) { 745 this.configurationGraph.register(this, requireNonNull(server)); 746 } 747 748 @Override 749 public void close() { 750 this.buildThread = null; 751 this.configurationGraph.closeLease(this); 752 } 753 } 754}