AbstractInternalCsvCallbackHandler.java

package de.siegmar.fastcsv.reader;

import java.util.Objects;

import de.siegmar.fastcsv.util.Preconditions;

/// Abstract base class for [CsvCallbackHandler] implementations.
///
/// @param <T> the type of the resulting records
public abstract sealed class AbstractInternalCsvCallbackHandler<T> extends CsvCallbackHandler<T>
    permits CsvRecordHandler, NamedCsvRecordHandler, StringArrayHandler {

    private static final int DEFAULT_INITIAL_FIELDS_SIZE = 32;

    /// The maximum number of fields a single record may have.
    protected final int maxFields;

    /// The maximum number of characters a single field may have.
    protected final int maxFieldSize;

    /// The maximum number of characters a single record may have.
    protected final int maxRecordSize;

    /// The field modifier.
    protected final FieldModifier fieldModifier;

    /// The starting line number of the current record.
    ///
    /// See [#beginRecord(long)].
    protected long startingLineNumber;

    /// The internal fields array.
    protected String[] fields;

    /// The total size (sum of all characters) of the current record.
    protected int recordSize;

    /// The current index in the internal fields array.
    protected int fieldIdx;

    /// The type of the current record.
    protected RecordType recordType = RecordType.DATA;

    /// Constructs a new instance with the given configuration.
    ///
    /// @param maxFields     the maximum number of fields; must be > 0
    /// @param maxFieldSize  the maximum field size; must be > 0
    /// @param maxRecordSize the maximum record size; must be > 0 and >= `maxFieldSize`
    /// @param fieldModifier the field modifier; must not be `null`
    /// @throws IllegalArgumentException if the arguments are invalid
    /// @throws NullPointerException     if `null` is passed
    protected AbstractInternalCsvCallbackHandler(final int maxFields,
                                                 final int maxFieldSize,
                                                 final int maxRecordSize,
                                                 final FieldModifier fieldModifier) {

        Preconditions.checkArgument(maxRecordSize >= maxFieldSize, "maxRecordSize must be >= maxFieldSize");

        this.maxFields = maxFields;
        this.maxFieldSize = maxFieldSize;
        this.maxRecordSize = maxRecordSize;
        this.fieldModifier = Objects.requireNonNull(fieldModifier, "fieldModifier must not be null");
        fields = new String[Math.min(DEFAULT_INITIAL_FIELDS_SIZE, maxFields)];
    }

    @Override
    public RecordType getRecordType() {
        return recordType;
    }

    @Override
    protected int getFieldCount() {
        return fieldIdx;
    }

    /// {@inheritDoc}
    /// Resets the internal state of this handler.
    @SuppressWarnings("checkstyle:HiddenField")
    @Override
    protected void beginRecord(final long startingLineNumber) {
        this.startingLineNumber = startingLineNumber;
        fieldIdx = 0;
        recordSize = 0;
        recordType = RecordType.DATA;
    }

    /// {@inheritDoc}
    /// Materializes the field value, apply field modifier, checks constraints and adds the field to the record.
    ///
    /// @throws CsvParseException if the addition exceeds the limit of record size or maximum fields count.
    @Override
    protected void addField(final char[] buf, final int offset, final int len, final boolean quoted) {
        final String modifiedField = modifyField(new String(buf, offset, len), quoted);
        final int modifiedFieldLength = modifiedField.length();

        if (maxFieldSize < modifiedFieldLength) {
            throw new CsvParseException(maxFieldSizeExceededMessage());
        }
        if (maxRecordSize < recordSize + modifiedFieldLength) {
            throw new CsvParseException(maxRecordSizeExceededMessage());
        }

        if (fieldIdx == fields.length) {
            extendCapacity();
        }

        fields[fieldIdx++] = modifiedField;
        recordSize += modifiedFieldLength;
    }

    /// Modifies field value.
    ///
    /// @param value  the field value
    /// @param quoted `true` if the field was quoted
    /// @return the modified field value
    protected String modifyField(final String value, final boolean quoted) {
        return fieldModifier.modify(startingLineNumber, fieldIdx, quoted, value);
    }

    private String maxFieldSizeExceededMessage() {
        return "Field at index %d in record starting at line %d exceeds the max field size of %d characters"
            .formatted(fieldIdx, startingLineNumber, maxFieldSize);
    }

    private String maxRecordSizeExceededMessage() {
        return "Field at index %d in record starting at line %d exceeds the max record size of %d characters"
            .formatted(fieldIdx, startingLineNumber, maxRecordSize);
    }

    /// {@inheritDoc}
    /// Materializes the comment value, apply field modifier, checks constraints and adds the field to the record.
    ///
    /// @throws CsvParseException if the addition exceeds the limit of record size.
    @Override
    protected void setComment(final char[] buf, final int offset, final int len) {
        recordType = RecordType.COMMENT;

        final String modifiedComment = modifyComment(new String(buf, offset, len));
        final int modifiedCommentLength = modifiedComment.length();
        if (maxFieldSize < modifiedCommentLength) {
            throw new CsvParseException(maxFieldSizeExceededMessage());
        }

        // No need to check maxRecordSize here since maxRecordSize >= maxFieldSize and
        // comments are one-field records

        recordSize += modifiedCommentLength;
        fields[0] = modifiedComment;
        fieldIdx = 1;
    }

    /// Modifies comment value.
    ///
    /// @param field the comment value
    /// @return the modified comment value
    protected String modifyComment(final String field) {
        return fieldModifier.modifyComment(startingLineNumber, field);
    }

    @Override
    protected void setEmpty() {
        recordType = RecordType.EMPTY;
        fields[0] = "";
        fieldIdx = 1;
    }

    private void extendCapacity() {
        if (fields.length == maxFields) {
            throw new CsvParseException("Record starting at line %d has surpassed the maximum limit of %d fields"
                .formatted(startingLineNumber, maxFields));
        }
        final String[] newFields = new String[Math.min(maxFields, fields.length * 2)];
        System.arraycopy(fields, 0, newFields, 0, fieldIdx);
        fields = newFields;
    }

    /// Builds a compact fields array (a copy of the internal fields array with the length of the current record).
    ///
    /// In contrast to the class property [#fields], the returned array does only contain the fields of the
    /// current record.
    ///
    /// @return the compact fields array
    protected String[] compactFields() {
        final String[] ret = new String[fieldIdx];
        System.arraycopy(fields, 0, ret, 0, fieldIdx);
        return ret;
    }

    /// Abstract builder for [AbstractInternalCsvCallbackHandler] subclasses.
    ///
    /// This class is for **internal use only** and should not be used directly. It will be sealed in a future release.
    ///
    /// @param <T> the type of the actual builder
    @SuppressWarnings("PMD.AvoidFieldNameMatchingMethodName")
    public abstract static class AbstractInternalCsvCallbackHandlerBuilder
        <T extends AbstractInternalCsvCallbackHandlerBuilder<?>> {

        private static final int DEFAULT_MAX_FIELDS = 16 * 1024;
        private static final int DEFAULT_MAX_FIELD_SIZE = 16 * 1024 * 1024;
        private static final int DEFAULT_MAX_RECORD_SIZE = 4 * DEFAULT_MAX_FIELD_SIZE;

        /// The maximum number of fields a single record may have.
        /// The default value is {@value %,2d #DEFAULT_MAX_FIELDS}.
        protected int maxFields = DEFAULT_MAX_FIELDS;

        /// The maximum number of characters a single field may have.
        /// The default value is {@value %,2d #DEFAULT_MAX_FIELD_SIZE}.
        protected int maxFieldSize = DEFAULT_MAX_FIELD_SIZE;

        /// The maximum number of characters a single record may have.
        /// The default value is {@value %,2d #DEFAULT_MAX_RECORD_SIZE}.
        protected int maxRecordSize = DEFAULT_MAX_RECORD_SIZE;

        /// The field modifier.
        /// The default value is [FieldModifiers#NOP].
        protected FieldModifier fieldModifier = FieldModifiers.NOP;

        /// Constructs a new default instance.
        protected AbstractInternalCsvCallbackHandlerBuilder() {
        }

        /// Method to be implemented by subclasses to return the correct type.
        ///
        /// @return This object of subclass type.
        protected abstract T self();

        /// Defines the maximum number of fields a single record may have.
        ///
        /// This constraint is enforced for all fields, including the header.
        ///
        /// @param maxFields the maximum fields a record may have; must be > 0
        ///                  (default: {@value %,2d #DEFAULT_MAX_FIELDS})
        /// @return This updated object, allowing additional method calls to be chained together.
        /// @throws IllegalArgumentException if the argument is less than 1
        @SuppressWarnings("checkstyle:HiddenField")
        public T maxFields(final int maxFields) {
            Preconditions.checkArgument(maxFields > 0, "maxFields must be > 0");
            this.maxFields = maxFields;
            return self();
        }

        /// Defines the maximum number of characters a single field may have.
        ///
        /// This constraint is enforced for all fields, including the header and comments.
        /// The size of the field is determined **after** field modifiers are applied.
        ///
        /// In contrast to [de.siegmar.fastcsv.reader.CsvReader.CsvReaderBuilder#maxBufferSize(int)] which enforces
        /// the maximum field size **before** the field modifier is applied, this constraint allows more precise control
        /// over the field size as field modifiers may have a significant impact on the field size.
        ///
        /// @param maxFieldSize the maximum field size; must be > 0
        ///                     (default: {@value %,2d #DEFAULT_MAX_FIELD_SIZE})
        /// @return This updated object, allowing additional method calls to be chained together.
        /// @throws IllegalArgumentException if the argument is less than 1
        /// @see de.siegmar.fastcsv.reader.CsvReader.CsvReaderBuilder#maxBufferSize(int)
        @SuppressWarnings("checkstyle:HiddenField")
        public T maxFieldSize(final int maxFieldSize) {
            Preconditions.checkArgument(maxFieldSize > 0, "maxFieldSize must be > 0");
            this.maxFieldSize = maxFieldSize;
            return self();
        }

        /// Defines the maximum number of characters a single record may have.
        ///
        /// This constraint is enforced for all fields, including the header and comments.
        /// The size of the record is the sum of the sizes of all fields.
        /// The size of each field is determined **after** field modifiers are applied.
        ///
        /// Make sure that [#maxRecordSize] is >= [#maxFieldSize].
        ///
        /// @param maxRecordSize the maximum record size; must be > 0
        ///                      (default: {@value %,2d #DEFAULT_MAX_RECORD_SIZE})
        /// @return This updated object, allowing additional method calls to be chained together.
        /// @throws IllegalArgumentException if the argument is less than 1
        /// @see #maxFieldSize(int)
        @SuppressWarnings("checkstyle:HiddenField")
        public T maxRecordSize(final int maxRecordSize) {
            Preconditions.checkArgument(maxRecordSize > 0, "maxRecordSize must be > 0");
            this.maxRecordSize = maxRecordSize;
            return self();
        }

        /// Sets the field modifier.
        ///
        /// @param fieldModifier the field modifier; must not be `null` (default: [FieldModifiers#NOP])
        /// @return This updated object, allowing additional method calls to be chained together.
        /// @throws NullPointerException if `null` is passed
        @SuppressWarnings("checkstyle:HiddenField")
        public T fieldModifier(final FieldModifier fieldModifier) {
            Objects.requireNonNull(fieldModifier, "fieldModifier must not be null");
            this.fieldModifier = fieldModifier;
            return self();
        }

    }

}