简体   繁体   中英

Java opencsv library: remove quotations from empty values(null values)

I use this library for exporting to CSV file

<dependency>
        <groupId>com.opencsv</groupId>
        <artifactId>opencsv</artifactId>
        <version>5.3</version>
    </dependency>

I created Builder:

writer = new StatefulBeanToCsvBuilder<T>(printWriter)
                .withQuotechar(CSVWriter.DEFAULT_QUOTE_CHARACTER)
                .withSeparator(CSVWriter.DEFAULT_SEPARATOR)
                .withOrderedResults(false)
                .withMappingStrategy(mappingStrategy)
                .build();

It Is my POJO:

@Data
public class ReportCsvDto {

    @CsvBindByName(column = "NAME")
    @CsvBindByPosition(position = 0)
    private String name;

    @CsvBindByName(column = "ID")
    @CsvBindByPosition(position = 1)
    private String id;

    @CsvBindByName(column = "GENDER")
    @CsvBindByPosition(position = 3)
    private String gender;
}

How can I remove quotations from empty values?

I have this: "Bill","","male" I want this: "Bill",,"male"

I want to remove quotations only from empty values

I have looked through the code of opencsv library. And most simple decision which I can come up with now it is just override transmuteBean method in MappingStrategy and passing this new stategy to the builder. For example for ColumnPositionMappingStrategy :

public class CustomColumnPositionMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
    @Override
    public String[] transmuteBean(T bean) throws CsvFieldAssignmentException, CsvChainedException {
        int numColumns = headerIndex.findMaxIndex()+1;
        BeanField<T, Integer> firstBeanField, subsequentBeanField;
        Integer firstIndex, subsequentIndex;
        List<String> contents = new ArrayList<>(Math.max(numColumns, 0));

        // Create a map of types to instances of subordinate beans
        Map<Class<?>, Object> instanceMap;
        try {
            instanceMap = indexBean(bean);
        }
        catch(IllegalAccessException | InvocationTargetException e) {
            // Our testing indicates these exceptions probably can't be thrown,
            // but they're declared, so we have to deal with them. It's an
            // alibi catch block.
            CsvBeanIntrospectionException csve = new CsvBeanIntrospectionException(
                    ResourceBundle.getBundle(
                            ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
                            .getString("error.introspecting.beans"));
            csve.initCause(e);
            throw csve;
        }

        CsvChainedException chainedException = null;
        for(int i = 0; i < numColumns;) {

            // Determine the first value
            firstBeanField = findField(i);
            firstIndex = chooseMultivaluedFieldIndexFromHeaderIndex(i);
            String[] fields = ArrayUtils.EMPTY_STRING_ARRAY;
            if(firstBeanField != null) {
                try {
                    fields = firstBeanField.write(instanceMap.get(firstBeanField.getType()), firstIndex);
                }
                catch(CsvDataTypeMismatchException | CsvRequiredFieldEmptyException e) {
                    if(chainedException != null) {
                        chainedException.add(e);
                    }
                    else {
                        chainedException = new CsvChainedException(e);
                    }
                }
            }

            if(fields.length == 0) {

                // Write the only value
                contents.add(null);
                i++; // Advance the index
            }
            else {

                // Multiple values. Write the first.
                contents.add(fields[0]);

                // Now write the rest.
                // We must make certain that we don't write more fields
                // than we have columns of the correct type to cover them.
                int j = 1;
                int displacedIndex = i+j;
                subsequentBeanField = findField(displacedIndex);
                subsequentIndex = chooseMultivaluedFieldIndexFromHeaderIndex(displacedIndex);
                while(j < fields.length
                        && displacedIndex < numColumns
                        && Objects.equals(firstBeanField, subsequentBeanField)
                        && Objects.equals(firstIndex, subsequentIndex)) {
                    // This field still has a header, so add it
                    contents.add(fields[j]);

                    // Prepare for the next loop through
                    displacedIndex = i + (++j);
                    subsequentBeanField = findField(displacedIndex);
                    subsequentIndex = chooseMultivaluedFieldIndexFromHeaderIndex(displacedIndex);
                }

                i = displacedIndex; // Advance the index

                // And here's where we fill in any fields that are missing to
                // cover the number of columns of the same type
                if(i < numColumns) {
                    subsequentBeanField = findField(i);
                    subsequentIndex = chooseMultivaluedFieldIndexFromHeaderIndex(i);
                    while(Objects.equals(firstBeanField, subsequentBeanField)
                            && Objects.equals(firstIndex, subsequentIndex)
                            && i < numColumns) {
                        contents.add(null);
                        subsequentBeanField = findField(++i);
                        subsequentIndex = chooseMultivaluedFieldIndexFromHeaderIndex(i);
                    }
                }
            }
        }

        // If there were exceptions, throw them
        if(chainedException != null) {
            if (chainedException.hasOnlyOneException()) {
                throw chainedException.getFirstException();
            }
            throw chainedException;
        }

        return contents.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
    }
}

And for your example it will produce the following output:

"Bill",,"male"

This overridden method is a simple copy of the original method. But instead of writing empty string on null value it writes null value. And CSVWriter.writeNext method then skips the output of the quotes for null value. This decision can be extended to handle blank lines in the original data too.

As an option you can implement MappingStrategy entirely of course. But I think this is not what you need.

Or you can just implement ICSVWriter for your case or redefine writeNext method for existing subclass. And then you need to pass this CSVWriter to builder. For example CSVWriter.writeNext :

public class CustomCSVWriter extends CSVWriter {
    public CustomCSVWriter(Writer writer) {
        super(writer);
    }

    public CustomCSVWriter(Writer writer, char separator, char quotechar, char escapechar, String lineEnd) {
        super(writer, separator, quotechar, escapechar, lineEnd);
    }

    @Override
    protected void writeNext(String[] nextLine, boolean applyQuotesToAll, Appendable appendable) throws IOException {
        if (nextLine == null) {
            return;
        }

        for (int i = 0; i < nextLine.length; i++) {

            if (i != 0) {
                appendable.append(separator);
            }

            String nextElement = nextLine[i];

            if (StringUtils.isEmpty(nextElement)) {
                continue;
            }

            Boolean stringContainsSpecialCharacters = stringContainsSpecialCharacters(nextElement);

            appendQuoteCharacterIfNeeded(applyQuotesToAll, appendable, stringContainsSpecialCharacters);

            if (stringContainsSpecialCharacters) {
                processLine(nextElement, appendable);
            } else {
                appendable.append(nextElement);
            }

            appendQuoteCharacterIfNeeded(applyQuotesToAll, appendable, stringContainsSpecialCharacters);
        }

        appendable.append(lineEnd);
        writer.write(appendable.toString());
    }

    private void appendQuoteCharacterIfNeeded(boolean applyQuotesToAll, Appendable appendable, Boolean stringContainsSpecialCharacters) throws IOException {
        if ((applyQuotesToAll || stringContainsSpecialCharacters) && quotechar != NO_QUOTE_CHARACTER) {
            appendable.append(quotechar);
        }
    }
}

Overridden method is a simple copy of the original method again. But it skips processing of empty strings (StringUtils.isEmpty(nextElement) check instead of checking for null).

And, of course, you can redefine this behavior in the following way:

public class CustomCSVWriter extends CSVWriter {
    public CustomCSVWriter(Writer writer) {
        super(writer);
    }

    public CustomCSVWriter(Writer writer, char separator, char quotechar, char escapechar, String lineEnd) {
        super(writer, separator, quotechar, escapechar, lineEnd);
    }

    @Override
    protected void writeNext(String[] nextLine, boolean applyQuotesToAll, Appendable appendable) throws IOException {
        if (nextLine != null) {
            for (int i = 0; i < nextLine.length; i++) {
                if (StringUtils.isEmpty(nextLine[i])) {
                    nextLine[i] = null;
                }
            }
        }
        super.writeNext(nextLine, applyQuotesToAll, appendable);
    }
}

Here empty strings are simply replaced with null values. And for me, this method would be more preferable if you do not need to separate empty strings and null values from the original data. Otherwise, the first option (with redefining MappingStrategy) is the only one possible.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM