001package com.streamconverter.command; 002 003import java.io.IOException; 004import java.io.InputStream; 005import java.io.OutputStream; 006import org.slf4j.Logger; 007 008/** 009 * Unified interface for stream commands. 010 * 011 * <p>This interface defines methods for executing commands on input streams and writing results to 012 * output streams. 013 * 014 * <p>This is a functional interface and can be implemented using lambda expressions or method 015 * references for simple stream processing operations. 016 * 017 * <p>Usage examples: 018 * 019 * <pre>{@code 020 * // Simple copy operation (IOException is propagated from execute method signature) 021 * IStreamCommand copyCommand = (in, out) -> in.transferTo(out); 022 * 023 * // Buffered stream processing with custom logic 024 * IStreamCommand bufferCommand = (in, out) -> { 025 * byte[] buffer = new byte[8192]; 026 * int len; 027 * while ((len = in.read(buffer)) != -1) { 028 * out.write(buffer, 0, len); 029 * } 030 * }; 031 * 032 * // Using in StreamConverter pipeline 033 * StreamConverter converter = StreamConverter.create(copyCommand); 034 * converter.run(inputStream, outputStream); 035 * }</pre> 036 * 037 * <p><b>MDC への値の伝搬について:</b> {@code execute()} 内で {@code MDC.put()} を直接呼び出しても、 038 * 他コマンドのスレッドには伝搬されません。ストリームから抽出した値を全コマンドのログに反映させるには {@link 039 * com.streamconverter.command.rule.MdcPropagatingRule} を使用してください。子スレッドへの MDC 自動継承が必要な場合は {@link 040 * com.streamconverter.logging.MDCInitializer} を参照してください。 041 */ 042@FunctionalInterface 043public interface IStreamCommand { 044 045 /** 046 * Executes the command on the provided input stream and writes the result to the output stream. 047 * 048 * @param inputStream The input stream to read data from. 049 * @param outputStream The output stream to write data to. 050 * @throws IOException If an I/O error occurs during the execution of the command. 051 */ 052 void execute(InputStream inputStream, OutputStream outputStream) throws IOException; 053 054 /** 055 * Returns a best-effort human-readable name for this command. 056 * 057 * <p>Synthetic and anonymous implementations fall back to {@code IStreamCommand} so diagnostics 058 * remain stable even when the concrete class name is not meaningful. 059 * 060 * @return command label suitable for logging and error messages 061 */ 062 default String commandName() { 063 Class<?> implClass = this.getClass(); 064 if (implClass.isSynthetic()) { 065 return "IStreamCommand"; 066 } 067 String simpleName = implClass.getSimpleName(); 068 if (!simpleName.isEmpty()) { 069 return simpleName; 070 } 071 Class<?> superClass = implClass.getSuperclass(); 072 if (superClass != null && !Object.class.equals(superClass)) { 073 String superName = superClass.getSimpleName(); 074 if (!superName.isEmpty()) { 075 return superName; 076 } 077 } 078 return "IStreamCommand"; 079 } 080 081 /** 082 * Wraps this command with logging. The returned command logs start, completion, and failure using 083 * the provided logger. 084 * 085 * <p>The command name is derived by {@link #commandName()}. When a caller wants a different label 086 * in logs, prefer {@link #withLogging(Logger, String)}. 087 * 088 * @param logger the logger to write messages to 089 * @return a new {@link IStreamCommand} that delegates to this command and emits log records 090 */ 091 default IStreamCommand withLogging(Logger logger) { 092 return withLogging(logger, commandName()); 093 } 094 095 /** 096 * Wraps this command with logging using an explicit command name. 097 * 098 * <p>This overload is recommended when the command is implemented as a lambda expression or 099 * method reference, where the underlying class name might be synthetic and not meaningful in 100 * logs. 101 * 102 * @param logger the logger to write messages to 103 * @param commandName the human-readable name of this command to appear in log messages 104 * @return a new {@link IStreamCommand} that delegates to this command and emits log records 105 */ 106 default IStreamCommand withLogging(Logger logger, String commandName) { 107 return (in, out) -> { 108 logger.info("Starting command: {}", commandName); 109 long start = System.currentTimeMillis(); 110 try { 111 this.execute(in, out); 112 logger.info( 113 "Completed command: {} ({}ms)", commandName, System.currentTimeMillis() - start); 114 } catch (IOException e) { 115 logger.error("Failed command: {} - {}", commandName, e.getMessage(), e); 116 throw e; 117 } catch (RuntimeException e) { 118 logger.error("Failed command: {} - {}", commandName, e.getMessage(), e); 119 throw e; 120 } catch (Error e) { 121 logger.error("Fatal error in command: {} - {}", commandName, e.getMessage(), e); 122 throw e; 123 } 124 }; 125 } 126}