1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package dev.metaschema.core.model.constraint;
7   
8   import org.eclipse.jdt.annotation.Owning;
9   
10  import java.util.List;
11  import java.util.Objects;
12  import java.util.concurrent.ExecutorService;
13  import java.util.concurrent.ForkJoinPool;
14  import java.util.concurrent.TimeUnit;
15  
16  import edu.umd.cs.findbugs.annotations.NonNull;
17  import edu.umd.cs.findbugs.annotations.Nullable;
18  
19  /**
20   * Configuration for constraint validation.
21   * <p>
22   * This class supports parallel execution and optional event-based
23   * instrumentation. Two execution modes are available:
24   * <ul>
25   * <li>Internal thread pool: Created via {@link #withThreads(int)}, shut down by
26   * {@link #close()}. Uses {@link ForkJoinPool} internally to handle nested
27   * parallelism without deadlock.</li>
28   * <li>External executor: Provided via {@link #withExecutor(ExecutorService)},
29   * NOT shut down by {@link #close()}</li>
30   * </ul>
31   * <p>
32   * An optional {@link ValidationEventListener} can be configured via
33   * {@link #withListener(ValidationEventListener)} to receive callbacks during
34   * validation. By default, a {@link NoOpValidationEventListener} is used,
35   * ensuring zero overhead when instrumentation is not needed.
36   * <p>
37   * Instances should be used with try-with-resources or explicitly closed after
38   * validation.
39   */
40  public final class ValidationConfig implements AutoCloseable {
41  
42    /**
43     * Single-threaded sequential execution (default, current behavior).
44     * <p>
45     * This instance does not need to be closed.
46     */
47    @SuppressWarnings("resource")
48    @NonNull
49    public static final ValidationConfig SEQUENTIAL = new ValidationConfig(null, 1, false,
50        NoOpValidationEventListener.INSTANCE);
51  
52    /**
53     * The executor service, lazily initialized if using internal pool.
54     * <p>
55     * Volatile is required for thread-safe lazy initialization.
56     */
57    @SuppressWarnings("PMD.AvoidUsingVolatile") // Required for thread-safe lazy initialization
58    @Nullable
59    private volatile ExecutorService executor;
60    private final int threadCount;
61    private final boolean ownsExecutor;
62    @NonNull
63    private final ValidationEventListener listener;
64  
65    private ValidationConfig(
66        @Nullable ExecutorService executor,
67        int threadCount,
68        boolean ownsExecutor,
69        @NonNull ValidationEventListener listener) {
70      this.executor = executor;
71      this.threadCount = threadCount;
72      this.ownsExecutor = ownsExecutor;
73      this.listener = listener;
74    }
75  
76    /**
77     * Create configuration using an application-provided executor.
78     * <p>
79     * The executor is NOT shut down by {@link #close()}; the caller retains
80     * ownership.
81     * <p>
82     * The caller owns the returned configuration and is responsible for closing it.
83     *
84     * @param executor
85     *          the executor service to use for parallel tasks
86     * @return configuration using the provided executor
87     * @throws NullPointerException
88     *           if executor is null
89     */
90    @NonNull
91    @Owning
92    public static ValidationConfig withExecutor(@NonNull ExecutorService executor) {
93      Objects.requireNonNull(executor, "executor must not be null");
94      return new ValidationConfig(executor, 0, false, NoOpValidationEventListener.INSTANCE);
95    }
96  
97    /**
98     * Create configuration that creates an internal thread pool.
99     * <p>
100    * The internal pool is shut down when {@link #close()} is called.
101    * <p>
102    * The caller owns the returned configuration and is responsible for closing it.
103    *
104    * @param threadCount
105    *          number of threads (must be &gt;= 1)
106    * @return configuration with internal thread pool
107    * @throws IllegalArgumentException
108    *           if threadCount &lt; 1
109    */
110   @NonNull
111   @Owning
112   public static ValidationConfig withThreads(int threadCount) {
113     if (threadCount < 1) {
114       throw new IllegalArgumentException("threadCount must be at least 1, got: " + threadCount);
115     }
116     if (threadCount == 1) {
117       return SEQUENTIAL;
118     }
119     return new ValidationConfig(null, threadCount, true, NoOpValidationEventListener.INSTANCE);
120   }
121 
122   /**
123    * Create a new configuration with the specified event listener, preserving the
124    * current parallel execution settings.
125    * <p>
126    * If this config owns its executor, the derived config will lazily create its
127    * own independent pool to avoid unsafe sharing of owned executors.
128    *
129    * @param listener
130    *          the event listener to use for validation instrumentation
131    * @return a new configuration with the given listener
132    * @throws NullPointerException
133    *           if listener is null
134    */
135   @NonNull
136   @Owning
137   public ValidationConfig withListener(@NonNull ValidationEventListener listener) {
138     Objects.requireNonNull(listener, "listener must not be null");
139     if (this.ownsExecutor) {
140       // Don't share owned executor; derived config will lazily create its own
141       return new ValidationConfig(null, this.threadCount, true, listener);
142     }
143     return new ValidationConfig(this.executor, this.threadCount, false, listener);
144   }
145 
146   /**
147    * Create a new configuration that adds an additional event listener, preserving
148    * the current parallel execution settings and any existing listener.
149    * <p>
150    * If the current listener is a {@link NoOpValidationEventListener}, the new
151    * listener replaces it directly. Otherwise, a
152    * {@link CompositeValidationEventListener} is created to deliver events to both
153    * the existing and new listeners.
154    *
155    * @param additionalListener
156    *          the additional event listener to add
157    * @return a new configuration with the additional listener
158    * @throws NullPointerException
159    *           if additionalListener is null
160    */
161   @NonNull
162   @Owning
163   public ValidationConfig addListener(@NonNull ValidationEventListener additionalListener) {
164     Objects.requireNonNull(additionalListener, "additionalListener must not be null");
165     ValidationEventListener current = this.listener;
166     if (current instanceof NoOpValidationEventListener) {
167       return withListener(additionalListener);
168     }
169     return withListener(
170         new CompositeValidationEventListener(List.of(current, additionalListener)));
171   }
172 
173   /**
174    * Check if parallel execution is enabled.
175    *
176    * @return true if using more than one thread
177    */
178   public boolean isParallel() {
179     return executor != null || threadCount > 1;
180   }
181 
182   /**
183    * Get the configured validation event listener.
184    *
185    * @return the event listener, never null
186    */
187   @NonNull
188   public ValidationEventListener getListener() {
189     return listener;
190   }
191 
192   /**
193    * Get the executor service, creating an internal pool if needed.
194    * <p>
195    * For internal pools, the executor is created lazily on first call.
196    *
197    * @return the executor service
198    * @throws IllegalStateException
199    *           if called on SEQUENTIAL config
200    */
201   @NonNull
202   public ExecutorService getExecutor() {
203     if (!isParallel()) {
204       throw new IllegalStateException("Cannot get executor for sequential configuration");
205     }
206     ExecutorService result = executor;
207     if (result == null) {
208       synchronized (this) {
209         result = executor;
210         if (result == null) {
211           // Use ForkJoinPool to avoid deadlock with nested parallelism.
212           // Fixed thread pools deadlock when all threads wait for children.
213           result = new ForkJoinPool(threadCount);
214           executor = result;
215         }
216       }
217     }
218     return Objects.requireNonNull(result, "Executor should not be null after initialization");
219   }
220 
221   /**
222    * Shut down internal executor if one was created.
223    * <p>
224    * Does nothing if using an external executor or if no executor was created.
225    */
226   @Override
227   public void close() {
228     ExecutorService exec = executor;
229     if (ownsExecutor && exec != null) {
230       exec.shutdown();
231       try {
232         if (!exec.awaitTermination(60, TimeUnit.SECONDS)) {
233           exec.shutdownNow();
234         }
235       } catch (@SuppressWarnings("unused") InterruptedException ex) {
236         exec.shutdownNow();
237         Thread.currentThread().interrupt();
238       }
239     }
240   }
241 }