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}