001package com.streamconverter.controller;
002
003import com.streamconverter.CommandResult;
004import com.streamconverter.StreamConverter;
005import com.streamconverter.command.CommandConfig;
006import com.streamconverter.command.IStreamCommand;
007import java.io.IOException;
008import java.io.InputStream;
009import java.io.OutputStream;
010import java.util.Arrays;
011import java.util.List;
012import java.util.Objects;
013import org.slf4j.Logger;
014import org.slf4j.LoggerFactory;
015
016/**
017 * Abstract base class for stream controllers.
018 *
019 * <p>This class provides common functionality for controllers including:
020 *
021 * <ul>
022 *   <li>Command configuration and creation using CommandFactory
023 *   <li>StreamConverter lifecycle management
024 *   <li>Error handling and logging
025 *   <li>Input/output validation
026 * </ul>
027 *
028 * <p>Subclasses need to implement the command configuration logic by overriding {@link
029 * #configureCommands()} method. This allows each controller to define its specific processing
030 * pipeline while inheriting common functionality.
031 *
032 * <p>Example usage:
033 *
034 * <pre>
035 * public class CsvToJsonController extends AbstractStreamController {
036 *
037 *     &#64;Override
038 *     protected CommandConfig[] configureCommands() {
039 *         return new CommandConfig[] {
040 *             new CommandConfig(CsvNavigateCommand.class, "Extract CSV data"),
041 *             new CommandConfig(JsonFormatCommand.class, "Format as JSON")
042 *         };
043 *     }
044 *
045 *     &#64;Override
046 *     public String getInputDataType() { return "CSV"; }
047 *
048 *     &#64;Override
049 *     public String getOutputDataType() { return "JSON"; }
050 * }
051 * </pre>
052 *
053 * @author StreamConverter Team
054 * @version 1.0
055 * @since 1.0
056 */
057public abstract class AbstractStreamController implements IStreamController {
058
059  private static final Logger log = LoggerFactory.getLogger(AbstractStreamController.class);
060
061  /** Cached command pipeline configuration */
062  private CommandConfig[] commandConfigs;
063
064  /** Cached StreamConverter instance */
065  private StreamConverter streamConverter;
066
067  /** Flag indicating if the controller has been properly configured */
068  private boolean configured = false;
069
070  /**
071   * Default constructor that initializes the controller.
072   *
073   * <p>The controller will be configured lazily on first use to allow subclasses to complete their
074   * initialization.
075   */
076  protected AbstractStreamController() {
077    // Configuration will be done lazily to allow subclass initialization
078  }
079
080  /**
081   * Processes data from input stream to output stream using the configured command pipeline.
082   *
083   * <p>This method ensures the controller is configured before processing and handles all error
084   * scenarios appropriately.
085   *
086   * @param inputStream the input stream to read data from
087   * @param outputStream the output stream to write results to
088   * @return list of command execution results
089   * @throws IOException if an I/O error occurs during processing
090   * @throws IllegalArgumentException if input parameters are null
091   * @throws IllegalStateException if the controller cannot be configured
092   */
093  @Override
094  public final List<CommandResult> process(InputStream inputStream, OutputStream outputStream)
095      throws IOException {
096    Objects.requireNonNull(inputStream, "Input stream cannot be null");
097    Objects.requireNonNull(outputStream, "Output stream cannot be null");
098
099    // Ensure controller is configured
100    ensureConfigured();
101
102    String controllerName = getClass().getSimpleName();
103    log.info("Starting stream processing with controller: {}", controllerName);
104    log.debug("Controller configuration: {}", getConfigurationDescription());
105
106    try {
107      // Execute the processing pipeline
108      List<CommandResult> results = streamConverter.run(inputStream, outputStream);
109
110      log.info("Stream processing completed successfully with controller: {}", controllerName);
111      return results;
112
113    } catch (IOException e) {
114      log.error(
115          "Stream processing failed with controller: {} - {}", controllerName, e.getMessage(), e);
116      throw e;
117    } catch (Exception e) {
118      log.error(
119          "Unexpected error during stream processing with controller: {} - {}",
120          controllerName,
121          e.getMessage(),
122          e);
123      throw new IOException("Stream processing failed: " + e.getMessage(), e);
124    }
125  }
126
127  /**
128   * Checks if the controller is properly configured.
129   *
130   * @return true if configured, false otherwise
131   */
132  @Override
133  public final boolean isConfigured() {
134    return configured && streamConverter != null;
135  }
136
137  /**
138   * Gets a description of the controller's current configuration.
139   *
140   * @return configuration description
141   */
142  @Override
143  public String getConfigurationDescription() {
144    if (!configured) {
145      return getClass().getSimpleName() + " (not configured)";
146    }
147
148    StringBuilder desc = new StringBuilder();
149    desc.append(getClass().getSimpleName())
150        .append(" - Input: ")
151        .append(getInputDataType())
152        .append(", Output: ")
153        .append(getOutputDataType())
154        .append(", Commands: ")
155        .append(commandConfigs.length);
156
157    if (log.isDebugEnabled()) {
158      desc.append(" [");
159      for (int i = 0; i < commandConfigs.length; i++) {
160        if (i > 0) desc.append(", ");
161        desc.append(commandConfigs[i].getCommandClass().getSimpleName());
162      }
163      desc.append("]");
164    }
165
166    return desc.toString();
167  }
168
169  /**
170   * Configures the command pipeline for this controller.
171   *
172   * <p>Subclasses must implement this method to define their specific processing pipeline. The
173   * returned CommandConfig array will be used to create the actual command objects using
174   * EnhancedCommandFactory.
175   *
176   * <p>Example implementation:
177   *
178   * <pre>
179   * &#64;Override
180   * protected CommandConfig[] configureCommands() {
181   *     return new CommandConfig[] {
182   *         new CommandConfig(CsvNavigateCommand.class, "data", "Extract data column"),
183   *         new CommandConfig(CsvValidateCommand.class, "Validate output", requiredColumns)
184   *     };
185   * }
186   * </pre>
187   *
188   * @return array of command configurations defining the processing pipeline
189   * @throws IllegalStateException if the configuration is invalid
190   */
191  protected abstract CommandConfig[] configureCommands();
192
193  /**
194   * Ensures the controller is properly configured, performing lazy initialization if needed.
195   *
196   * @throws IllegalStateException if configuration fails
197   */
198  private void ensureConfigured() {
199    if (configured && streamConverter != null) {
200      return; // Already configured
201    }
202
203    try {
204      log.debug("Configuring controller: {}", getClass().getSimpleName());
205
206      // Get command configuration from subclass
207      commandConfigs = configureCommands();
208
209      if (commandConfigs == null || commandConfigs.length == 0) {
210        throw new IllegalStateException("Controller must configure at least one command");
211      }
212
213      // Create command pipeline using direct instantiation
214      IStreamCommand[] commands = createCommandsFromConfigs(commandConfigs);
215
216      // Create StreamConverter with the configured commands
217      streamConverter = StreamConverter.create(Arrays.asList(commands));
218
219      configured = true;
220
221      log.info(
222          "Controller configured successfully: {} with {} commands",
223          getClass().getSimpleName(),
224          commands.length);
225
226    } catch (Exception e) {
227      log.error(
228          "Failed to configure controller: {} - {}", getClass().getSimpleName(), e.getMessage(), e);
229      throw new IllegalStateException("Controller configuration failed: " + e.getMessage(), e);
230    }
231  }
232
233  /**
234   * Create commands from CommandConfig array using direct instantiation
235   *
236   * @param configs command configurations
237   * @return array of instantiated commands
238   */
239  private IStreamCommand[] createCommandsFromConfigs(CommandConfig[] configs) {
240    IStreamCommand[] commands = new IStreamCommand[configs.length];
241    for (int i = 0; i < configs.length; i++) {
242      try {
243        // For simplicity, use reflection to create instances
244        // In a real refactor, we'd replace this with direct factory methods
245        Class<? extends IStreamCommand> clazz = configs[i].getCommandClass();
246        Object[] args = configs[i].getArgs();
247
248        if (args.length == 0) {
249          commands[i] = clazz.getDeclaredConstructor().newInstance();
250        } else {
251          // This is a simplified version - in practice, we'd need proper constructor matching
252          commands[i] = clazz.getDeclaredConstructor(getArgTypes(args)).newInstance(args);
253        }
254
255        log.debug("Created command: {}", clazz.getSimpleName());
256      } catch (Exception e) {
257        throw new RuntimeException(
258            "Failed to create command: " + configs[i].getCommandClass().getSimpleName(), e);
259      }
260    }
261    return commands;
262  }
263
264  /** Get argument types for constructor matching */
265  private Class<?>[] getArgTypes(Object[] args) {
266    Class<?>[] types = new Class<?>[args.length];
267    for (int i = 0; i < args.length; i++) {
268      types[i] = args[i].getClass();
269    }
270    return types;
271  }
272
273  /**
274   * Gets the configured command configurations.
275   *
276   * <p>This method is protected to allow subclasses access to the configuration for advanced
277   * scenarios while keeping it internal to the controller hierarchy.
278   *
279   * @return array of command configurations, or null if not yet configured
280   */
281  protected final CommandConfig[] getCommandConfigs() {
282    return commandConfigs == null ? null : commandConfigs.clone();
283  }
284
285  /**
286   * Gets the configured StreamConverter instance.
287   *
288   * <p>This method is protected to allow subclasses access to the StreamConverter for advanced
289   * scenarios while keeping it internal to the controller hierarchy.
290   *
291   * @return the StreamConverter instance, or null if not yet configured
292   */
293  protected final StreamConverter getStreamConverter() {
294    return streamConverter;
295  }
296
297  /**
298   * Allows subclasses to perform additional validation after configuration.
299   *
300   * <p>This method is called after the command pipeline is configured but before the controller is
301   * marked as ready. Subclasses can override this to add custom validation logic.
302   *
303   * @throws IllegalStateException if validation fails
304   */
305  protected void validateConfiguration() {
306    // Default implementation does nothing
307    // Subclasses can override for custom validation
308  }
309}