NamedCsvRecordHandler.java

package de.siegmar.fastcsv.reader;

import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;

import de.siegmar.fastcsv.util.Nullable;

/// A callback handler that returns a [NamedCsvRecord] for each record.
///
/// Example:
/// ```
/// NamedCsvRecordHandler handler = NamedCsvRecordHandler.builder()
///     .fieldModifier(FieldModifiers.TRIM)
///     .header("foo", "bar")
///     .build();
/// ```
///
/// This implementation is stateful and must not be reused.
public final class NamedCsvRecordHandler extends AbstractInternalCsvCallbackHandler<NamedCsvRecord> {

    private static final String[] EMPTY_HEADER = new String[0];
    private final boolean allowDuplicateHeaderFields;
    private final boolean returnHeader;

    @Nullable
    private String[] header;

    private NamedCsvRecordHandler(final int maxFields, final int maxFieldSize, final int maxRecordSize,
                                  final FieldModifier fieldModifier,
                                  final boolean allowDuplicateHeaderFields,
                                  final boolean returnHeader,
                                  @Nullable final List<String> header) {
        super(maxFields, maxFieldSize, maxRecordSize, fieldModifier);
        this.allowDuplicateHeaderFields = allowDuplicateHeaderFields;
        this.returnHeader = returnHeader;
        if (header != null) {
            this.header = validateHeader(header.toArray(new String[0]));
        }
    }

    /// Constructs a new builder instance for this class.
    ///
    /// @return the builder
    /// @see #of(Consumer)
    public static NamedCsvRecordHandlerBuilder builder() {
        return new NamedCsvRecordHandlerBuilder();
    }

    /// Constructs a new instance of this class with default settings.
    ///
    /// @return the new instance
    /// @see NamedCsvRecordHandlerBuilder#build()
    public static NamedCsvRecordHandler of() {
        return builder().build();
    }

    /// Constructs a new instance of this class with the given configuration.
    ///
    /// This is an alternative to the builder pattern for convenience.
    ///
    /// @param configurer the configuration, must not be `null`
    /// @return the new instance
    /// @throws NullPointerException     if `null` is passed
    /// @throws IllegalArgumentException if argument constraints are violated
    /// @see #builder()
    public static NamedCsvRecordHandler of(final Consumer<NamedCsvRecordHandlerBuilder> configurer) {
        Objects.requireNonNull(configurer, "configurer must not be null");
        final NamedCsvRecordHandlerBuilder builder = builder();
        configurer.accept(builder);
        return builder.build();
    }

    @SuppressWarnings("PMD.UseVarargs")
    private String[] validateHeader(final String[] fields) {
        Objects.requireNonNull(fields, "header must not be null");
        for (final String h : fields) {
            Objects.requireNonNull(h, "header element must not be null");
        }

        if (!allowDuplicateHeaderFields) {
            checkForDuplicates(fields);
        }

        return fields;
    }

    @SuppressWarnings("PMD.UseVarargs")
    private static void checkForDuplicates(final String[] header) {
        final var duplicateHeaders = new LinkedHashSet<String>();
        final var seen = new HashSet<String>();
        for (final String h : header) {
            if (!seen.add(h)) {
                duplicateHeaders.add(h);
            }
        }

        if (!duplicateHeaders.isEmpty()) {
            throw new IllegalArgumentException("Header contains duplicate fields: "
                + duplicateHeaders);
        }
    }

    @Nullable
    @Override
    protected NamedCsvRecord buildRecord() {
        final String[] compactFields = compactFields();

        if (recordType == RecordType.COMMENT) {
            return new NamedCsvRecord(startingLineNumber, compactFields, true, EMPTY_HEADER);
        }

        if (header == null) {
            header = validateHeader(compactFields);
            if (!returnHeader) {
                return null;
            }
        }

        return new NamedCsvRecord(startingLineNumber, compactFields, false, header);
    }

    /// A builder for [NamedCsvRecordHandler].
    @SuppressWarnings({"checkstyle:HiddenField", "PMD.AvoidFieldNameMatchingMethodName"})
    public static final class NamedCsvRecordHandlerBuilder
        extends AbstractInternalCsvCallbackHandlerBuilder<NamedCsvRecordHandlerBuilder> {

        private boolean allowDuplicateHeaderFields;
        private boolean returnHeader;

        @Nullable
        private List<String> header;

        private NamedCsvRecordHandlerBuilder() {
        }

        /// Sets whether duplicate header fields are allowed.
        ///
        /// When set to `false`, an [IllegalArgumentException] is thrown if the header contains duplicate fields.
        /// When set to `true`, duplicate fields are allowed. See [NamedCsvRecord] for details on how duplicate
        /// headers are handled.
        ///
        /// @param allowDuplicateHeaderFields whether duplicate header fields are allowed (default: `false`)
        /// @return This updated object, allowing additional method calls to be chained together.
        public NamedCsvRecordHandlerBuilder allowDuplicateHeaderFields(final boolean allowDuplicateHeaderFields) {
            this.allowDuplicateHeaderFields = allowDuplicateHeaderFields;
            return this;
        }

        /// Sets a predefined header.
        ///
        /// When not set, the header is taken from the first record (that is not a comment).
        ///
        /// @param header the header, must not be `null`
        /// @return This updated object, allowing additional method calls to be chained together.
        /// @throws NullPointerException if `null` is passed
        /// @see #header(List)
        @SuppressWarnings("checkstyle:HiddenField")
        public NamedCsvRecordHandlerBuilder header(final String... header) {
            Objects.requireNonNull(header, "header must not be null");
            this.header = List.of(header);
            return this;
        }

        /// Sets the header.
        ///
        /// When not set, the header is taken from the first record (that is not a comment).
        ///
        /// @param header the header, must not be `null`
        /// @return This updated object, allowing additional method calls to be chained together.
        /// @throws NullPointerException if `null` is passed
        /// @see #header(String...)
        @SuppressWarnings("checkstyle:HiddenField")
        public NamedCsvRecordHandlerBuilder header(final List<String> header) {
            Objects.requireNonNull(header, "header must not be null");
            this.header = List.copyOf(header);
            return this;
        }

        /// Sets whether the header itself should be returned as the first record.
        ///
        /// When enabled, the first record returned will be a [NamedCsvRecord] with the header fields as its fields
        /// and as the header.
        ///
        /// @param returnHeader whether the header should be returned as the first record (default: `false`)
        /// @return This updated object, allowing additional method calls to be chained together.
        public NamedCsvRecordHandlerBuilder returnHeader(final boolean returnHeader) {
            this.returnHeader = returnHeader;
            return this;
        }

        @Override
        protected NamedCsvRecordHandlerBuilder self() {
            return this;
        }

        /// Builds the [NamedCsvRecordHandler] instance.
        ///
        /// @return the new instance
        /// @throws IllegalArgumentException if argument constraints are violated
        ///     (see [AbstractInternalCsvCallbackHandler])
        public NamedCsvRecordHandler build() {
            if (returnHeader && header != null) {
                throw new IllegalArgumentException("Predefined headers cannot be used with returnHeader=true");
            }
            return new NamedCsvRecordHandler(maxFields, maxFieldSize, maxRecordSize, fieldModifier,
                allowDuplicateHeaderFields, returnHeader, header);
        }

    }

}