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 * @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 * @Override 046 * public String getInputDataType() { return "CSV"; } 047 * 048 * @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 * @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}