001package com.streamconverter.command.impl; 002 003import com.streamconverter.command.AbstractStreamCommand; 004import java.io.IOException; 005import java.io.InputStream; 006import java.io.InputStreamReader; 007import java.io.OutputStream; 008import java.io.OutputStreamWriter; 009import java.io.Reader; 010import java.io.Writer; 011import java.nio.charset.StandardCharsets; 012import java.util.Objects; 013import org.slf4j.Logger; 014import org.slf4j.LoggerFactory; 015 016/** 017 * Line ending normalization command. 018 * 019 * <p>This command normalizes line endings in text streams to a specified format. It can convert 020 * between different line ending styles (Unix LF, Windows CRLF, classic Mac CR) or preserve the 021 * input format. 022 * 023 * <p>The command processes data efficiently while preserving the exact structure of the input, 024 * including whether the input ends with a line terminator. 025 */ 026public class LineEndingNormalizeCommand extends AbstractStreamCommand { 027 private static final Logger logger = LoggerFactory.getLogger(LineEndingNormalizeCommand.class); 028 029 /** Supported line ending types for normalization. */ 030 public enum LineEndingType { 031 /** Unix/Linux style line endings (LF only) */ 032 UNIX("\n"), 033 034 /** Windows style line endings (CRLF) */ 035 WINDOWS("\r\n"), 036 037 /** Classic Mac style line endings (CR only) - rarely used in modern systems */ 038 MAC_CLASSIC("\r"), 039 040 /** Preserve input line endings as-is */ 041 PRESERVE_INPUT(null), 042 043 /** Use system default line separator */ 044 SYSTEM_DEFAULT(System.lineSeparator()); 045 046 private final String separator; 047 048 LineEndingType(String separator) { 049 this.separator = separator; 050 } 051 052 /** 053 * Gets the line separator string for this type. 054 * 055 * @return the line separator string, or null for PRESERVE_INPUT 056 */ 057 public String getSeparator() { 058 return separator; 059 } 060 } 061 062 private final LineEndingType targetType; 063 064 /** 065 * Creates a new line ending normalization command. 066 * 067 * @param targetType the target line ending type to convert to 068 * @throws NullPointerException if targetType is null 069 */ 070 public LineEndingNormalizeCommand(LineEndingType targetType) { 071 this.targetType = Objects.requireNonNull(targetType, "Target type cannot be null"); 072 } 073 074 @Override 075 protected void executeInternal(InputStream inputStream, OutputStream outputStream) 076 throws IOException { 077 logger.debug("Starting line ending normalization to: {}", targetType); 078 079 // Stream processing for memory efficiency 080 try (Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); 081 Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) { 082 083 if (targetType == LineEndingType.PRESERVE_INPUT) { 084 // For PRESERVE_INPUT, copy directly without modification 085 char[] buffer = new char[8192]; 086 int bytesRead; 087 while ((bytesRead = reader.read(buffer)) != -1) { 088 writer.write(buffer, 0, bytesRead); 089 } 090 } else { 091 // Process character by character for line ending normalization 092 String targetSeparator = targetType.getSeparator(); 093 int current; 094 095 while ((current = reader.read()) != -1) { 096 if (current == '\r') { 097 // Handle CR - could be CR, CRLF, or standalone CR 098 int next = reader.read(); 099 if (next == '\n') { 100 // CRLF -> convert to target 101 writer.write(targetSeparator); 102 } else { 103 // Standalone CR -> convert to target 104 writer.write(targetSeparator); 105 // Write the next character that wasn't part of line ending 106 if (next != -1) { 107 writer.write(next); 108 } 109 } 110 } else if (current == '\n') { 111 // LF -> convert to target (handles Unix style) 112 writer.write(targetSeparator); 113 } else { 114 // Regular character 115 writer.write(current); 116 } 117 } 118 } 119 120 writer.flush(); 121 } 122 123 logger.debug("Line ending normalization completed successfully"); 124 } 125 126 @Override 127 protected String getCommandDetails() { 128 return String.format( 129 "LineEndingNormalizeCommand(target=%s, separator='%s')", 130 targetType, 131 targetType.getSeparator() != null 132 ? escapeLineSeparator(targetType.getSeparator()) 133 : "preserve"); 134 } 135 136 /** 137 * Escapes line separator characters for display purposes. 138 * 139 * @param separator the line separator to escape 140 * @return escaped string representation 141 */ 142 private String escapeLineSeparator(String separator) { 143 if (separator == null) return "null"; 144 return separator.replace("\r", "\\r").replace("\n", "\\n"); 145 } 146}