SquirrelJME/SquirrelJME

View on GitHub
modules/csv/src/main/java/cc/squirreljme/csv/CsvWriter.java

Summary

Maintainability
A
40 mins
Test Coverage
// -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
// ---------------------------------------------------------------------------
// Multi-Phasic Applications: SquirrelJME
//     Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
// ---------------------------------------------------------------------------
// SquirrelJME is under the Mozilla Public License Version 2.0.
// See license.mkd for licensing and copyright information.
// ---------------------------------------------------------------------------

package cc.squirreljme.csv;

import cc.squirreljme.runtime.cldc.debug.Debugging;
import java.io.Closeable;
import java.io.IOException;
import java.util.Arrays;

/**
 * Writes CSV data to an output file.
 *
 * @param <T> The type to write.
 * @since 2023/09/12
 */
public final class CsvWriter<T>
    implements Closeable
{
    /** The serializer used. */
    protected final CsvSerializer<T> serializer;
    
    /** The output target. */
    protected final Appendable out;
    
    /** The working buffer. */
    private final StringBuilder _buffer =
        new StringBuilder();
    
    /** The internal result. */
    private final CsvSerializerResult _result =
        new CsvSerializerResult();
    
    /** Were the headers written? */
    private volatile boolean _wroteHeaders;
    
    /**
     * Initializes the CSV writer.
     *
     * @param __serializer The serializer for encoding data.
     * @param __out The output source to write rows to.
     * @throws NullPointerException On null arguments.
     * @since 2023/09/12
     */
    public CsvWriter(CsvSerializer<T> __serializer, Appendable __out)
        throws NullPointerException
    {
        if (__serializer == null || __out == null)
            throw new NullPointerException("NARG");
        
        this.serializer = __serializer;
        this.out = __out;
        
        // Always determine target headers to use
        __serializer.serializeHeaders(this._result);
    }
    
    /**
     * {@inheritDoc}
     * @since 2023/09/12
     */
    @Override
    public void close()
        throws IOException
    {
        // Write the headers?
        IOException suppress = null;
        try
        {
            if (!this._wroteHeaders)
            {
                this.__writeRow(this._result._headers);
                this._wroteHeaders = true;
            }
        }
        catch (IOException __e)
        {
            suppress = __e;
        }
        
        // Forward close to the target appendable
        Appendable out = this.out;
        if (out instanceof Closeable)
            ((Closeable)out).close();
        else if (out instanceof AutoCloseable)
            try
            {
                ((AutoCloseable)out).close();
            }
            catch (Exception __e)
            {
                if (suppress != null)
                    __e.addSuppressed(suppress);
                
                if (__e instanceof RuntimeException)
                    throw (RuntimeException)__e;
                else if (__e instanceof IOException)
                    throw (IOException)__e;
                else
                    throw new IOException("WRAP", __e);
            }
            
        // Suppressed?
        if (suppress != null)
            throw suppress;
    }
    
    /**
     * Writes the given value.
     *
     * @param __value The value to write.
     * @throws IOException On write errors.
     * @throws NullPointerException On null arguments.
     * @since 2023/09/12
     */
    public void write(T __value)
        throws IOException, NullPointerException
    {
        if (__value == null)
            throw new NullPointerException("NARG");
        
        // Write the headers?
        CsvSerializerResult result = this._result;
        if (!this._wroteHeaders)
        {
            this.__writeRow(result._headers);
            this._wroteHeaders = true;
        }
        
        // Determine values to write
        try
        {
            // Serialize value
            result.__reset();
            this.serializer.serialize(__value, result);
            
            /* {@squirreljme.error CS05 End of row not called.} */
            if (!result._endRow)
                throw new IllegalStateException("CS05");
            
            // Write row values
            this.__writeRow(result._values);
        }
        
        // Cleanup always
        finally
        {
            result.__reset();
        }
    }
    
    /**
     * Writes all the given values.
     *
     * @param __values The values to write.
     * @throws IOException On write errors.
     * @throws NullPointerException On null arguments.
     * @since 2023/09/12
     */
    @SuppressWarnings("unchecked")
    public void writeAll(T... __values)
        throws IOException, NullPointerException
    {
        if (__values == null)
            throw new NullPointerException("NARG");
        
        this.writeAll(Arrays.asList(__values));
    }
    
    /**
     * Writes all the given values.
     *
     * @param __values The values to write.
     * @throws IOException On write errors.
     * @throws NullPointerException On null arguments.
     * @since 2023/09/12
     */
    public void writeAll(Iterable<? extends T> __values)
        throws IOException, NullPointerException
    {
        if (__values == null)
            throw new NullPointerException("NARG");
        
        for (T value : __values)
            this.write(value);
    }
    
    /**
     * Writes a row to the output.
     *
     * @param __row The row to write.
     * @throws IOException On write errors.
     * @since 2023/09/14
     */
    private void __writeRow(String[] __row)
        throws IOException, NullPointerException
    {
        if (__row == null)
            throw new NullPointerException("NARG");
        
        Appendable out = this.out;
        
        // Handle each column
        for (int i = 0, n = __row.length; i < n; i++)
        {
            // Preface with comma for new columns
            if (i > 0)
                out.append(',');
            
            // Skip nulls and blanks
            String input = __row[i];
            if (input == null || input.isEmpty())
                continue;
            
            // Can directly output with no quoting needed
            if (!CsvWriter.__needQuote(input))
                out.append(input);
            
            // Need to properly encode values
            else
            {
                // Starting quote
                out.append('"');
                
                // Write every character, escaping strings accordingly
                for (int j = 0, p = input.length(); j < p; j++)
                {
                    char c = input.charAt(j);
                    
                    // Escape double quotes
                    if (c == '"')
                        out.append('"');
                    
                    out.append(c);
                }
                
                // Ending quote
                out.append('"');
            }
        }
        
        // RFC4180 says to end with CRLF, do so
        out.append('\r');
        out.append('\n');
    }
    
    /**
     * Does the given string need quoting?
     *
     * @param __input The input string.
     * @return If quoting is needed.
     * @throws IllegalArgumentException If the input character is not valid.
     * @throws NullPointerException On null arguments.
     * @since 2023/09/17
     */
    private static boolean __needQuote(String __input)
        throws IllegalArgumentException, NullPointerException
    {
        if (__input == null)
            throw new NullPointerException("NARG");
        
        // Check each character
        for (int i = 0, n = __input.length(); i < n; i++)
        {
            char c = __input.charAt(i);
            
            /* {@squirreljme.error CS03 Invalid CSV character.} */
            if (c == '\0' || c == '\r' || c == '\n')
                throw new IllegalArgumentException("CS03");
            
            // Commas, quotes, and start/end whitespace needs quotes
            if (c == ',' || c == '"' ||
                (c <= ' ' && i == 0) || (c <= ' ' && i == (n - 1)))
                return true;
        }
        
        // Not needed
        return false;
    }
}