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