SquirrelJME/SquirrelJME

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

Summary

Maintainability
A
25 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.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import net.multiphasicapps.collections.UnmodifiableMap;

/**
 * Reads CSV files and writes to an output.
 *
 * @param <T> The type of value to read.
 * @since 2023/09/12
 */
public final class CsvReader<T>
    implements Closeable
{
    /** The deserializer for incoming lines. */
    protected final CsvDeserializer<T> deserializer;
    
    /** The input stream for CSV lines. */
    protected final CsvInputStream input;
    
    /** Temporary reading buffer. */
    private final StringBuilder _buffer =
        new StringBuilder();
    
    /** Working buffer. */
    private final StringBuilder _work =
        new StringBuilder();
    
    /** Working entries. */
    private final List<String> _workColumns =
        new ArrayList<>();
    
    /** Working input map. */
    private final Map<String, String> _workMap =
        new LinkedHashMap<>();
    
    /** Read only working input map. */
    private final Map<String, String> _workRoMap =
        UnmodifiableMap.of(this._workMap);
    
    /** The mapping of CSV headers. */
    private volatile List<String> _headerMap;
    
    /**
     * Initializes the reader.
     *
     * @param __deserializer The deserializer input.
     * @param __input The input to read from.
     * @throws NullPointerException On null arguments.
     * @since 2023/09/12
     */
    public CsvReader(CsvDeserializer<T> __deserializer,
        CsvInputStream __input)
        throws NullPointerException
    {
        if (__deserializer == null || __input == null)
            throw new NullPointerException("NARG");
        
        this.deserializer = __deserializer;
        this.input = __input;
    }
    
    /**
     * {@inheritDoc}
     * @since 2023/09/14
     */
    @Override
    public void close()
        throws IOException
    {
        this.input.close();
    }
    
    /**
     * Reads a single value.
     *
     * @return The single value.
     * @throws NoSuchElementException If there is nothing left.
     * @throws IOException On read errors.
     * @since 2023/09/12
     */
    public T read()
        throws NoSuchElementException, IOException
    {
        StringBuilder buffer = this._buffer;
        CsvInputStream input = this.input;
        List<String> headerMap = this._headerMap;
        
        // Read in line, loop to handle empty lines, etc.
        for (;;)
        {
            // Initialize buffer and read in data
            buffer.setLength(0);
            if (!input.next(buffer))
                throw new NoSuchElementException("NSEE");
            
            // Skip empty lines
            int bufLen = buffer.length();
            if (buffer.length() == 0)
                continue;
            
            // Re-initialize for future working
            List<String> workColumns = this._workColumns;
            StringBuilder work = this._work;
            
            // Clear before processing
            workColumns.clear();
            work.setLength(0);
            
            // Parse line
            boolean inQuote = false;
            boolean lastQuote = false;
            for (int i = 0; i < bufLen; i++)
            {
                // Get character for processing
                char c = buffer.charAt(i);
                
                // Quotes are special
                if (c == '"')
                {
                    // Previously read a quote?
                    if (lastQuote)
                    {
                        // We wanted an actual quote here
                        work.append('"');
                        
                        // Clear last quote state
                        lastQuote = false;
                    }
                    
                    // Reading fresh quote
                    else
                        lastQuote = true;
                    
                    // Flip being in quotes because:
                    // "" would result in not being quoted
                    // while """ would
                    inQuote = !inQuote;
                }
                
                // End column
                else if (c == ',' && !inQuote)
                {
                    // Add from column
                    workColumns.add(work.toString());
                    work.setLength(0);
                    
                    // Reset state
                    lastQuote = false;
                }
                
                // Normal character
                else
                {
                    work.append(c);
                    
                    // Was not a quote
                    lastQuote = false;
                }
            }
            
            // If there is anything left over, treat as last column
            if (work.length() > 0)
            {
                workColumns.add(work.toString());
                work.setLength(0);
            }
            
            // For later cleanup...
            try
            {
                // If there are no headers read yet, use them
                if (headerMap == null)
                {
                    headerMap = new ArrayList<>(workColumns);
                    this._headerMap = headerMap;
                    
                    // Run loop again
                    continue;
                }
                
                // Load into a map
                else
                {
                    // Fill in columns for the entire map, default to empty
                    // string if there is no value there
                    int max = workColumns.size();
                    Map<String, String> workMap = this._workMap;
                    for (int i = 0, n = headerMap.size(); i < n; i++)
                        workMap.put(headerMap.get(i),
                            (i < max ? workColumns.get(i) : ""));
                    
                    // Process with deserializer
                    return this.deserializer.deserialize(this._workRoMap);
                }
            }
            
            // Cleanup after a processing run
            finally
            {
                workColumns.clear();
            }
        }
    }
    
    /**
     * Reads all entries into a list.
     *
     * @return The resultant list.
     * @throws IOException On read errors.
     * @since 2023/09/12
     */
    public List<T> readAll()
        throws IOException
    {
        return this.readAll(new ArrayList<T>());
    }
    
    /**
     * Reads all the values into the given collection.
     *
     * @param <C> The collection type.
     * @param __into The collection to write into.
     * @return {@code __into}.
     * @throws IOException On read errors.
     * @throws NullPointerException On null arguments.
     * @since 2023/09/12
     */
    public <C extends Collection<? super T>> C readAll(C __into)
        throws IOException, NullPointerException
    {
        for (;;)
            try
            {
                __into.add(this.read());
            }
            catch (NoSuchElementException __ignored)
            {
                break;
            }
        
        // Return the target
        return __into;
    }
}