001package com.streamconverter.command;
002
003import com.streamconverter.context.ExecutionContextHolder;
004import com.streamconverter.util.MeasuredInputStream;
005import com.streamconverter.util.MeasuredOutputStream;
006import java.io.IOException;
007import java.io.InputStream;
008import java.io.OutputStream;
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011
012/**
013 * Abstract class for stream commands.
014 *
015 * <p>This class provides a template for executing commands on input streams and writing the results
016 * to output streams.
017 */
018public abstract class AbstractStreamCommand implements IStreamCommand {
019  /** Logger instance for this command. Uses the actual subclass name for better traceability. */
020  protected final Logger log;
021
022  /**
023   * Default constructor.
024   *
025   * <p>Initializes the command with default settings and creates a logger using the actual command
026   * class name.
027   */
028  public AbstractStreamCommand() {
029    super();
030    this.log = LoggerFactory.getLogger(getClass());
031  }
032
033  /**
034   * Executes the command with ExecutionContext support.
035   *
036   * <p>This override ensures that MDC is synchronized with the latest shared context values before
037   * logging, enabling cross-thread MDC propagation in parallel execution environments.
038   *
039   * @param inputStream The input stream to read data from.
040   * @param outputStream The output stream to write data to.
041   * @param context The execution context for traceability and logging enhancement.
042   * @throws IOException If an I/O error occurs during the execution of the command.
043   */
044  @Override
045  public final void execute(
046      InputStream inputStream,
047      OutputStream outputStream,
048      com.streamconverter.context.ExecutionContext context)
049      throws IOException {
050    // Store context in ThreadLocal for TurboFilter to access on every log output
051    try {
052      ExecutionContextHolder.set(context);
053      // Delegate to the basic execute method
054      execute(inputStream, outputStream);
055    } finally {
056      // Clean up ThreadLocal to prevent memory leaks
057      ExecutionContextHolder.clear();
058    }
059  }
060
061  /**
062   * Executes the command on the provided input stream and writes the result to the output stream.
063   *
064   * <p>This method automatically logs execution details including: - Command name and execution
065   * time - Input/output data sizes - Exception details if execution fails - Performance metrics
066   *
067   * @param inputStream The input stream to read data from.
068   * @param outputStream The output stream to write data to.
069   * @throws IOException If an I/O error occurs during the execution of the command.
070   */
071  @Override
072  public final void execute(InputStream inputStream, OutputStream outputStream) throws IOException {
073    String commandName = this.getClass().getSimpleName();
074    long startTime = System.currentTimeMillis();
075    long startMemory = getUsedMemory();
076
077    // 実行開始ログ
078    if (log.isInfoEnabled()) {
079      log.info("Starting command execution: {}", commandName);
080    }
081    if (log.isDebugEnabled()) {
082      log.debug("Command details: {}", getCommandDetails());
083    }
084
085    // データサイズ測定用のストリームでラップ(try-with-resourcesでリソース管理)
086    long inputBytes = 0;
087    long outputBytes = 0;
088    try (MeasuredInputStream measuredInput = new MeasuredInputStream(inputStream);
089        MeasuredOutputStream measuredOutput = new MeasuredOutputStream(outputStream)) {
090
091      // 実際の処理実行
092      executeInternal(measuredInput, measuredOutput);
093
094      // データサイズ測定値を取得
095      inputBytes = measuredInput.getBytesRead();
096      outputBytes = measuredOutput.getBytesWritten();
097
098      // 成功時のログ出力
099      long duration = System.currentTimeMillis() - startTime;
100      long memoryUsed = getUsedMemory() - startMemory;
101
102      if (log.isInfoEnabled()) {
103        log.info(
104            "Command execution completed: {} ({}ms, input: {}bytes, output: {}bytes, memory: {}MB)",
105            commandName,
106            duration,
107            inputBytes,
108            outputBytes,
109            memoryUsed / 1024 / 1024);
110      }
111
112      // パフォーマンス警告
113      if (duration > 5000) { // 5秒以上
114        log.warn("Command {} took longer than expected: {}ms", commandName, duration);
115      }
116
117    } catch (Exception e) {
118      // 例外発生時のログ出力
119      long duration = System.currentTimeMillis() - startTime;
120      long memoryUsed = getUsedMemory() - startMemory;
121
122      if (log.isErrorEnabled()) {
123        log.error(
124            "Command execution failed: {} ({}ms, input: {}bytes, output: {}bytes, memory: {}MB) - {}",
125            commandName,
126            duration,
127            inputBytes,
128            outputBytes,
129            memoryUsed / 1024 / 1024,
130            e.getMessage(),
131            e);
132      }
133
134      // NullPointerExceptionをIOExceptionでラップ
135      if (e instanceof NullPointerException) {
136        throw new IOException("Invalid null stream parameter", e);
137      }
138
139      throw e;
140    }
141  }
142
143  /**
144   * Abstract method to be implemented by subclasses for executing the command.
145   *
146   * @param inputStream The input stream to read data from.
147   * @param outputStream The output stream to write data to.
148   * @throws IOException If an I/O error occurs during the execution of the command.
149   */
150  protected abstract void executeInternal(InputStream inputStream, OutputStream outputStream)
151      throws IOException;
152
153  /**
154   * コマンドの詳細情報を取得します。
155   *
156   * <p>サブクラスでオーバーライドして、コマンド固有の設定や状態を返すことができます。 この情報はデバッグログに出力され、問題の診断に役立ちます。
157   *
158   * @return コマンドの詳細情報
159   */
160  protected String getCommandDetails() {
161    return this.getClass().getSimpleName() + " (default implementation)";
162  }
163
164  /**
165   * 現在の使用メモリ量を取得します。
166   *
167   * @return 使用メモリ量(バイト)
168   */
169  private long getUsedMemory() {
170    Runtime runtime = Runtime.getRuntime();
171    return runtime.totalMemory() - runtime.freeMemory();
172  }
173}