001package com.streamconverter.context;
002
003import java.time.Instant;
004import java.util.Collections;
005import java.util.HashMap;
006import java.util.Map;
007import java.util.Objects;
008import java.util.UUID;
009import java.util.concurrent.ConcurrentHashMap;
010import java.util.concurrent.atomic.AtomicBoolean;
011import java.util.concurrent.atomic.AtomicInteger;
012import org.slf4j.MDC;
013
014/**
015 * 実行コンテキストを管理するクラス
016 *
017 * <p>StreamConverterの実行全体を通じて一意の識別子とコンテキスト情報を提供し、 マルチスレッド環境でのMDC伝播を支援します。
018 */
019public class ExecutionContext {
020
021  private final String executionId;
022  private final Instant startTime;
023  private final AtomicInteger commandSequence;
024  private final Map<String, String> globalContext;
025  private final Map<String, String> userContext;
026  private final ConcurrentHashMap<String, String> sharedContext;
027  private final AtomicBoolean sharedContextDirty;
028
029  // 標準的なコンテキストキー
030  /** 実行ID用のMDCキー */
031  public static final String EXECUTION_ID_KEY = "executionId";
032
033  /** 開始時刻用のMDCキー */
034  public static final String START_TIME_KEY = "startTime";
035
036  /** コマンドシーケンス用のMDCキー */
037  public static final String COMMAND_SEQUENCE_KEY = "commandSequence";
038
039  /** スレッド名用のMDCキー */
040  public static final String THREAD_NAME_KEY = "threadName";
041
042  /** ステージ用のMDCキー */
043  public static final String STAGE_KEY = "stage";
044
045  private ExecutionContext(Builder builder) {
046    this.executionId = builder.executionId;
047    this.startTime = builder.startTime;
048    this.commandSequence = new AtomicInteger(0);
049    this.globalContext = Collections.unmodifiableMap(new HashMap<>(builder.globalContext));
050    this.userContext = new HashMap<>(builder.userContext);
051    this.sharedContext = new ConcurrentHashMap<>();
052    this.sharedContextDirty = new AtomicBoolean(false);
053  }
054
055  /**
056   * 新しい実行コンテキストを生成
057   *
058   * @return 新しいExecutionContextインスタンス
059   */
060  public static ExecutionContext create() {
061    return new Builder().build();
062  }
063
064  /**
065   * カスタム設定でExecutionContextを作成するためのビルダーを取得
066   *
067   * @return Builderインスタンス
068   */
069  public static Builder builder() {
070    return new Builder();
071  }
072
073  /**
074   * 実行ID(一意識別子)を取得
075   *
076   * @return 実行ID
077   */
078  public String getExecutionId() {
079    return executionId;
080  }
081
082  /**
083   * 実行開始時刻を取得
084   *
085   * @return 開始時刻
086   */
087  public Instant getStartTime() {
088    return startTime;
089  }
090
091  /**
092   * 次のコマンドシーケンス番号を取得(自動インクリメント)
093   *
094   * @return シーケンス番号
095   */
096  public int getNextCommandSequence() {
097    return commandSequence.incrementAndGet();
098  }
099
100  /**
101   * 現在のコマンドシーケンス番号を取得
102   *
103   * @return 現在のシーケンス番号
104   */
105  public int getCurrentCommandSequence() {
106    return commandSequence.get();
107  }
108
109  /**
110   * グローバルコンテキスト値を取得
111   *
112   * @param key キー
113   * @return 値(存在しない場合はnull)
114   */
115  public String getGlobalContext(String key) {
116    return globalContext.get(key);
117  }
118
119  /**
120   * 全てのグローバルコンテキストを取得
121   *
122   * @return 読み取り専用のコンテキストマップ
123   */
124  public Map<String, String> getAllGlobalContext() {
125    return globalContext;
126  }
127
128  /**
129   * ユーザーコンテキスト値を取得
130   *
131   * @param key キー
132   * @return 値(存在しない場合はnull)
133   */
134  public String getUserContext(String key) {
135    return userContext.get(key);
136  }
137
138  /**
139   * ユーザーコンテキスト値を設定
140   *
141   * @param key キー
142   * @param value 値
143   */
144  public void setUserContext(String key, String value) {
145    if (value == null) {
146      userContext.remove(key);
147    } else {
148      userContext.put(key, value);
149    }
150  }
151
152  /**
153   * 全てのユーザーコンテキストを取得
154   *
155   * @return ユーザーコンテキストマップ
156   */
157  public Map<String, String> getAllUserContext() {
158    return new HashMap<>(userContext);
159  }
160
161  /**
162   * 共有コンテキスト値を取得
163   *
164   * <p>共有コンテキストはスレッド間で共有されるコンテキスト情報です。 ConcurrentHashMapで実装されているため、スレッドセーフです。
165   *
166   * @param key キー
167   * @return 値(存在しない場合はnull)
168   */
169  public String getSharedContext(String key) {
170    return sharedContext.get(key);
171  }
172
173  /**
174   * 共有コンテキスト値を設定
175   *
176   * <p>共有コンテキストはスレッド間で共有されるコンテキスト情報です。 実行中のスレッドからいつでも設定・取得が可能で、他のスレッドから即座にアクセスできます。
177   *
178   * @param key キー
179   * @param value 値(nullの場合は削除)
180   */
181  public void setSharedContext(String key, String value) {
182    if (value == null) {
183      sharedContext.remove(key);
184    } else {
185      sharedContext.put(key, value);
186    }
187    sharedContextDirty.set(true);
188  }
189
190  /**
191   * 全ての共有コンテキストを取得
192   *
193   * @return 共有コンテキストマップのコピー
194   */
195  public Map<String, String> getAllSharedContext() {
196    return new HashMap<>(sharedContext);
197  }
198
199  /**
200   * 現在のコンテキストをMDCに設定
201   *
202   * <p>このメソッドを呼び出すことで、実行コンテキストの情報が 現在のスレッドのMDCに設定されます。 共有コンテキストも含めて全てのコンテキスト情報がMDCに反映されます。
203   *
204   * <p>共有コンテキストはDirty Flag最適化により、変更があった場合のみMDCに同期されます。 これにより、頻繁なapplyToMDC()呼び出しでもパフォーマンスが維持されます。
205   */
206  public void applyToMDC() {
207    // 基本的なコンテキスト情報をMDCに設定
208    MDC.put(EXECUTION_ID_KEY, executionId);
209    MDC.put(START_TIME_KEY, startTime.toString());
210    MDC.put(COMMAND_SEQUENCE_KEY, String.valueOf(getCurrentCommandSequence()));
211    MDC.put(THREAD_NAME_KEY, Thread.currentThread().getName());
212
213    // グローバルコンテキストをMDCに設定
214    globalContext.forEach(MDC::put);
215
216    // ユーザーコンテキストをMDCに設定
217    userContext.forEach(MDC::put);
218
219    // 共有コンテキストをMDCに設定
220    // マルチスレッド環境では各スレッドが独自のMDCを持つため、常に同期が必要
221    sharedContext.forEach(MDC::put);
222  }
223
224  /**
225   * 特定のステージでのMDC設定
226   *
227   * @param stageName ステージ名
228   */
229  public void applyToMDCWithStage(String stageName) {
230    applyToMDC();
231    MDC.put(STAGE_KEY, stageName);
232  }
233
234  /**
235   * コンテキストのコピーを作成 ユーザーコンテキストは複製されますが、グローバルコンテキストは共有されます。 コピー時はコマンドシーケンスは0にリセットされます。
236   *
237   * @return コンテキストのコピー
238   */
239  public ExecutionContext copy() {
240    return new Builder()
241        .executionId(this.executionId)
242        .startTime(this.startTime)
243        .globalContext(this.globalContext)
244        .userContext(this.userContext)
245        .build();
246    // コマンドシーケンスは新しいコピーとして0から開始
247  }
248
249  /** ExecutionContext作成用のBuilderクラス */
250  public static class Builder {
251    private String executionId = generateExecutionId();
252    private Instant startTime = Instant.now();
253    private Map<String, String> globalContext = new HashMap<>();
254    private Map<String, String> userContext = new HashMap<>();
255
256    /** Creates a new builder. */
257    public Builder() {}
258
259    /**
260     * 実行IDを設定
261     *
262     * @param executionId 実行ID
263     * @return Builderインスタンス
264     */
265    public Builder executionId(String executionId) {
266      this.executionId = Objects.requireNonNull(executionId, "executionId cannot be null");
267      return this;
268    }
269
270    /**
271     * 開始時刻を設定
272     *
273     * @param startTime 開始時刻
274     * @return Builderインスタンス
275     */
276    public Builder startTime(Instant startTime) {
277      this.startTime = Objects.requireNonNull(startTime, "startTime cannot be null");
278      return this;
279    }
280
281    /**
282     * グローバルコンテキスト値を設定
283     *
284     * @param key キー
285     * @param value 値
286     * @return Builderインスタンス
287     */
288    public Builder globalContext(String key, String value) {
289      Objects.requireNonNull(key, "key cannot be null");
290      if (value == null) {
291        this.globalContext.remove(key);
292      } else {
293        this.globalContext.put(key, value);
294      }
295      return this;
296    }
297
298    /**
299     * グローバルコンテキストを一括設定
300     *
301     * @param context コンテキストマップ
302     * @return Builderインスタンス
303     */
304    public Builder globalContext(Map<String, String> context) {
305      if (context != null) {
306        this.globalContext.putAll(context);
307      }
308      return this;
309    }
310
311    /**
312     * ユーザーコンテキスト値を設定
313     *
314     * @param key キー
315     * @param value 値
316     * @return Builderインスタンス
317     */
318    public Builder userContext(String key, String value) {
319      Objects.requireNonNull(key, "key cannot be null");
320      if (value == null) {
321        this.userContext.remove(key);
322      } else {
323        this.userContext.put(key, value);
324      }
325      return this;
326    }
327
328    /**
329     * ユーザーコンテキストを一括設定
330     *
331     * @param context コンテキストマップ
332     * @return Builderインスタンス
333     */
334    public Builder userContext(Map<String, String> context) {
335      if (context != null) {
336        this.userContext.putAll(context);
337      }
338      return this;
339    }
340
341    /**
342     * ExecutionContextインスタンスを生成
343     *
344     * @return ExecutionContextインスタンス
345     */
346    public ExecutionContext build() {
347      return new ExecutionContext(this);
348    }
349
350    private static String generateExecutionId() {
351      return "EXEC-"
352          + UUID.randomUUID().toString().substring(0, 8)
353          + "-"
354          + System.currentTimeMillis();
355    }
356  }
357
358  @Override
359  public String toString() {
360    return String.format(
361        "ExecutionContext{executionId='%s', startTime=%s, commandSequence=%d}",
362        executionId, startTime, getCurrentCommandSequence());
363  }
364
365  @Override
366  public boolean equals(Object obj) {
367    if (this == obj) return true;
368    if (obj == null || getClass() != obj.getClass()) return false;
369    ExecutionContext that = (ExecutionContext) obj;
370    return Objects.equals(executionId, that.executionId);
371  }
372
373  @Override
374  public int hashCode() {
375    return Objects.hash(executionId);
376  }
377}