web-dev-qa-db-fra.com

OpenCSV: Comment créer un fichier CSV à partir de POJO avec des en-têtes de colonne personnalisés et des positions de colonne personnalisées?

J'ai créé une classe MappingsBean dans laquelle toutes les colonnes du fichier CSV sont spécifiées. Ensuite, j'analyse des fichiers XML et crée une liste de mappingbeans. Ensuite, j'écris ces données dans un fichier CSV en tant que rapport.

J'utilise les annotations suivantes:

public class MappingsBean {

    @CsvBindByName(column = "TradeID")
    @CsvBindByPosition(position = 0)
    private String tradeId;

    @CsvBindByName(column = "GWML GUID", required = true)
    @CsvBindByPosition(position = 1)
    private String gwmlGUID;

    @CsvBindByName(column = "MXML GUID", required = true)
    @CsvBindByPosition(position = 2)
    private String mxmlGUID;

    @CsvBindByName(column = "GWML File")
    @CsvBindByPosition(position = 3)
    private String gwmlFile;

    @CsvBindByName(column = "MxML File")
    @CsvBindByPosition(position = 4)
    private String mxmlFile;

    @CsvBindByName(column = "MxML Counterparty")
    @CsvBindByPosition(position = 5)
    private String mxmlCounterParty;

    @CsvBindByName(column = "GWML Counterparty")
    @CsvBindByPosition(position = 6)
    private String gwmlCounterParty;
}

Et puis j'utilise la classe StatefulBeanToCsv pour écrire dans un fichier CSV:

File reportFile = new File(reportOutputDir + "/" + REPORT_FILENAME);
Writer writer = new PrintWriter(reportFile);
StatefulBeanToCsv<MappingsBean> beanToCsv = new 
                              StatefulBeanToCsvBuilder(writer).build();
beanToCsv.write(makeFinalMappingBeanList());
writer.close();

Le problème avec cette approche est que si j'utilise @CsvBindByPosition(position = 0) pour contrôler la position , Je ne peux pas générer de noms de colonnes. Si j'utilise @CsvBindByName(column = "TradeID"), je ne peux pas définir la position des colonnes.

Existe-t-il un moyen d'utiliser les deux annotations pour créer des fichiers CSV avec des en-têtes de colonne et contrôler la position de la colonne? 

Cordialement, Vikram Pathania

13
Vikram Pathania

J'ai eu le même problème. Autant que je sache, OpenCSV ne contient aucune fonctionnalité permettant d’écrire un bean au format CSV avec des noms de colonne personnalisés et ordering. 

Il existe deux principaux MappingStrategyies disponibles dans OpenCSV:

  • HeaderColumnNameMappingStrategy: cela permet de mapper les colonnes de fichier CVS à des champs de haricots basés sur un nom personnalisé; lors de l'écriture de bean au format CSV, cela permet de changer le nom de l'en-tête de la colonne mais nous n'avons aucun contrôle sur l'ordre des colonnes
  • ColumnPositionMappingStrategy: permet de mapper les colonnes de fichiers CSV aux champs de beans en fonction de l'ordre des colonnes; lors de l'écriture de bean au format CSV, nous pouvons contrôler l'ordre des colonnes, mais nous obtenons un en-tête vide (l'implémentation renvoie new String[0] en tant qu'en-tête)

Le seul moyen que j’ai trouvé d’obtenir des noms de colonne et un ordre personnalisés est d’écrire votre MappingStrategy personnalisé.

Première solution: rapide et facile mais codé en dur

Créer une MappingStrategy personnalisée:

class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
    private static final String[] HEADER = new String[]{"TradeID", "GWML GUID", "MXML GUID", "GWML File", "MxML File", "MxML Counterparty", "GWML Counterparty"};

    @Override
    public String[] generateHeader() {
        return HEADER;
    }
}

Et utilisez-le dans StatefulBeanToCsvBuilder:

final CustomMappingStrategy<MappingsBean> mappingStrategy = new CustomMappingStrategy<>();
mappingStrategy.setType(MappingsBean.class);

final StatefulBeanToCsv<MappingsBean> beanToCsv = new StatefulBeanToCsvBuilder<MappingsBean>(writer)
    .withMappingStrategy(mappingStrategy)
    .build();
beanToCsv.write(makeFinalMappingBeanList());
writer.close()

Dans la classe MappingsBean, nous avons laissé des annotations CsvBindByPosition - pour contrôler le classement (dans cette solution, les annotations CsvBindByName ne sont pas nécessaires). Grâce à la stratégie de mappage personnalisé, les noms des colonnes d'en-tête sont inclus dans le fichier CSV résultant.

L'inconvénient de cette solution est que, lorsque nous modifions l'ordre des colonnes via l'annotation CsvBindByPosition, nous devons également modifier manuellement la constante HEADER dans notre stratégie de mappage personnalisé.

Deuxième solution: plus flexible

La première solution fonctionne, mais ce n'était pas bon pour moi. Basé sur les implémentations intégrées de MappingStrategy, je suis venu avec une autre implémentation:

class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
    @Override
    public String[] generateHeader() {
        final int numColumns = findMaxFieldIndex();
        if (!isAnnotationDriven() || numColumns == -1) {
            return super.generateHeader();
        }

        header = new String[numColumns + 1];

        BeanField beanField;
        for (int i = 0; i <= numColumns; i++) {
            beanField = findField(i);
            String columnHeaderName = extractHeaderName(beanField);
            header[i] = columnHeaderName;
        }
        return header;
    }

    private String extractHeaderName(final BeanField beanField) {
        if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class).length == 0) {
            return StringUtils.EMPTY;
        }

        final CsvBindByName bindByNameAnnotation = beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class)[0];
        return bindByNameAnnotation.column();
    }
}

Vous pouvez utiliser cette stratégie personnalisée dans StatefulBeanToCsvBuilder exactement de la même façon que dans la première solution (n'oubliez pas d'appeler mappingStrategy.setType(MappingsBean.class);, sinon cette solution ne fonctionnera pas).

Actuellement, notre MappingsBean doit contenir à la fois les annotations CsvBindByName et CsvBindByPosition. Le premier à donner le nom de la colonne d'en-tête et le second à créer un ordre des colonnes dans l'en-tête CSV de sortie. Maintenant, si nous changeons (en utilisant des annotations) le nom de la colonne ou l'ordre dans la classe MappingsBean - cette modification sera reflétée dans le fichier CSV de sortie.

18
sebast26

Correction de la réponse ci-dessus pour correspondre à la nouvelle version.

package csvpojo;

import org.Apache.commons.lang3.StringUtils;

import com.opencsv.bean.BeanField;
import com.opencsv.bean.ColumnPositionMappingStrategy;
import com.opencsv.bean.CsvBindByName;
import com.opencsv.exceptions.CsvRequiredFieldEmptyException;

class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
    @Override
    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {

super.setColumnMapping(new String[ FieldUtils.getAllFields(bean.getClass()).length]);
        final int numColumns = findMaxFieldIndex();
        if (!isAnnotationDriven() || numColumns == -1) {
            return super.generateHeader(bean);
        }

        String[] header = new String[numColumns + 1];

        BeanField<T> beanField;
        for (int i = 0; i <= numColumns; i++) {
            beanField = findField(i);
            String columnHeaderName = extractHeaderName(beanField);
            header[i] = columnHeaderName;
        }
        return header;
    }

    private String extractHeaderName(final BeanField<T> beanField) {
        if (beanField == null || beanField.getField() == null
                || beanField.getField().getDeclaredAnnotationsByType(CsvBindByName.class).length == 0) {
            return StringUtils.EMPTY;
        }

        final CsvBindByName bindByNameAnnotation = beanField.getField()
                .getDeclaredAnnotationsByType(CsvBindByName.class)[0];
        return bindByNameAnnotation.column();
    }
}

Appelez ensuite ceci pour générer du CSV. J'ai utilisé les visiteurs comme mon POJO pour peupler, mettre à jour si nécessaire.

        CustomMappingStrategy<Visitors> mappingStrategy = new CustomMappingStrategy<>();
        mappingStrategy.setType(Visitors.class);
        // writing sample
        List<Visitors> beans2 = new ArrayList<Visitors>();

        Visitors v = new Visitors();
        v.set_1_firstName(" test1");
        v.set_2_lastName("lastname1");
        v.set_3_visitsToWebsite("876");
        beans2.add(v);

        v = new Visitors();
        v.set_1_firstName(" firstsample2");
        v.set_2_lastName("lastname2");
        v.set_3_visitsToWebsite("777");
        beans2.add(v);

        Writer writer = new FileWriter("G://output.csv");
        StatefulBeanToCsv<Visitors> beanToCsv = new StatefulBeanToCsvBuilder<Visitors>(writer)
                .withMappingStrategy(mappingStrategy).withSeparator(',').withApplyQuotesToAll(false).build();
        beanToCsv.write(beans2);
        writer.close();

Mes annotations de haricots ressemblent à ceci

 @CsvBindByName (column = "First Name", required = true)
 @CsvBindByPosition(position=1)
 private String firstName;


 @CsvBindByName (column = "Last Name", required = true)
 @CsvBindByPosition(position=0)
 private String lastName;
7
Lalji Gajera

merci pour ce fil, cela m’a été vraiment utile ... j’ai un peu amélioré la solution fournie afin d’accepter aussi POJO où certains champs ne sont pas annotés (ne sont pas destinés à être lus/écrits):

public class ColumnAndNameMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {

@Override
public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {

    super.setColumnMapping(new String[ getAnnotatedFields(bean)]);
    final int numColumns = getAnnotatedFields(bean);
    final int totalFieldNum = findMaxFieldIndex();
    if (!isAnnotationDriven() || numColumns == -1) {
        return super.generateHeader(bean);
    }

    String[] header = new String[numColumns];

    BeanField<T> beanField;
    for (int i = 0; i <= totalFieldNum; i++) {
        beanField = findField(i);
        if (isFieldAnnotated(beanField.getField())) {
            String columnHeaderName = extractHeaderName(beanField);
            header[i] = columnHeaderName;
        }
    }
    return header;
}

private int getAnnotatedFields(T bean) {
    return (int) Arrays.stream(FieldUtils.getAllFields(bean.getClass()))
            .filter(this::isFieldAnnotated)
            .count();
}

private boolean isFieldAnnotated(Field f) {
    return f.isAnnotationPresent(CsvBindByName.class) || f.isAnnotationPresent(CsvCustomBindByName.class);
}

private String extractHeaderName(final BeanField beanField) {
    if (beanField == null || beanField.getField() == null) {
        return StringUtils.EMPTY;
    }

    Field field = beanField.getField();

    if (field.getDeclaredAnnotationsByType(CsvBindByName.class).length != 0) {
        final CsvBindByName bindByNameAnnotation = field.getDeclaredAnnotationsByType(CsvBindByName.class)[0];
        return bindByNameAnnotation.column();
    }

    if (field.getDeclaredAnnotationsByType(CsvCustomBindByName.class).length != 0) {
        final CsvCustomBindByName bindByNameAnnotation = field.getDeclaredAnnotationsByType(CsvCustomBindByName.class)[0];
        return bindByNameAnnotation.column();
    }

    return StringUtils.EMPTY;
}

}

1
Rodrigo Broggi

Essayez quelque chose comme ci-dessous:

private static class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {

    String[] header;

    public CustomMappingStrategy(String[] cols) {
        header = cols;
    }

    @Override
    public String[] generateHeader(T bean) throws CsvRequiredFieldEmptyException {
        return header;
    }
}

Puis utilisez-le comme suit:

String[] columns = new String[]{"Name", "Age", "Company", "Salary"};
        CustomMappingStrategy<Employee> mappingStrategy = new CustomMappingStrategy<Employee>(columns);

Où les colonnes sont les colonnes de votre haricot et Employee est votre haricot

0
Kalya Elisha

Si vous n'avez pas de méthode getDeclaredAnnotationsByType, mais avez besoin du nom de votre nom de champ d'origine:

beanField.getField (). getName ()

public class CustomMappingStrategy<T> extends ColumnPositionMappingStrategy<T> {
@Override
public String[] generateHeader() {
    final int numColumns = findMaxFieldIndex();
    if (!isAnnotationDriven() || numColumns == -1) {
        return super.generateHeader();
    }

    header = new String[numColumns + 1];

    BeanField beanField;
    for (int i = 0; i <= numColumns; i++) {
        beanField = findField(i);
        String columnHeaderName = extractHeaderName(beanField);
        header[i] = columnHeaderName;
    }
    return header;
}

private String extractHeaderName(final BeanField beanField) {
    if (beanField == null || beanField.getField() == null || beanField.getField().getDeclaredAnnotations().length == 0) {
        return StringUtils.EMPTY;
    }
    return beanField.getField().getName();
}

}

0
akasha

Je souhaitais obtenir les mêmes résultats que le sujet, mais je voulais aussi pouvoir importer le fichier CSV généré dans POJO. Aucune solution présentée ne m'a aidé à y parvenir.

Pour obtenir mes résultats, je devais refuser d'utiliser @CsvBindByPosition, car dans ce cas, ColumnPositionMappingStrategy était sélectionné automatiquement. Par documents: cette stratégie nécessite que le fichier ne comporte PAS d'en-tête .

Mais si nous écrivons avec openCSV avec une liaison par nom de colonne, un en-tête est ajouté automatiquement (nous pouvons le supprimer, mais je le trouve utile).

Ce que j'ai utilisé pour atteindre l'objectif:

HeaderColumnNameMappingStrategy
mappingStrategy.setColumnOrderOnWrite(Comparator<String> writeOrder)

CsvUtils pour lire/écrire en csv

import com.opencsv.CSVWriter;
import com.opencsv.bean.*;
import org.springframework.web.multipart.MultipartFile;

import Java.io.*;
import Java.util.List;

public class CsvUtils {
    private CsvUtils() {
    }

    public static <T> String convertToCsv(List<T> entitiesList, MappingStrategy<T> mappingStrategy) throws Exception {
        try (Writer writer = new StringWriter()) {
            StatefulBeanToCsv<T> beanToCsv = new StatefulBeanToCsvBuilder<T>(writer)
                    .withMappingStrategy(mappingStrategy)
                    .withQuotechar(CSVWriter.NO_QUOTE_CHARACTER)
                    .build();
            beanToCsv.write(entitiesList);
            return writer.toString();
        }
    }

    @SuppressWarnings("unchecked")
    public static <T> List<T> convertFromCsv(MultipartFile file, Class clazz) throws IOException {
        try (Reader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) {
            CsvToBean<T> csvToBean = new CsvToBeanBuilder<T>(reader).withType(clazz).build();
            return csvToBean.parse();
        }
    }
}

POJO pour import/export

public class LocalBusinessTrainingPairDTO {
    //this is used for CSV columns ordering on exporting LocalBusinessTrainingPairs
    public static final String[] FIELDS_ORDER = {"leftId", "leftName", "rightId", "rightName"};

    @CsvBindByName(column = "leftId")
    private int leftId;

    @CsvBindByName(column = "leftName")
    private String leftName;

    @CsvBindByName(column = "rightId")
    private int rightId;

    @CsvBindByName(column = "rightName")
    private String rightName;
    // getters/setters omitted, do not forget to add them
}

Comparateur personnalisé pour un ordre de chaîne prédéfini:

public class OrderedComparatorIgnoringCase implements Comparator<String> {
    private List<String> predefinedOrder;

    public OrderedComparatorIgnoringCase(String[] predefinedOrder) {
        this.predefinedOrder = new ArrayList<>();
        for (String item : predefinedOrder) {
            this.predefinedOrder.add(item.toLowerCase());
        }
    }

    @Override
    public int compare(String o1, String o2) {
        return predefinedOrder.indexOf(o1.toLowerCase()) - predefinedOrder.indexOf(o2.toLowerCase());
    }
}

Commande écrite pour POJO (réponse à la question initiale) 

public static void main(String[] args) throws Exception {
     List<LocalBusinessTrainingPairDTO> localBusinessTrainingPairsDTO = new ArrayList<>();
     LocalBusinessTrainingPairDTO localBusinessTrainingPairDTO = new LocalBusinessTrainingPairDTO();
     localBusinessTrainingPairDTO.setLeftId(1);
     localBusinessTrainingPairDTO.setLeftName("leftName");
     localBusinessTrainingPairDTO.setRightId(2);
     localBusinessTrainingPairDTO.setRightName("rightName");

     localBusinessTrainingPairsDTO.add(localBusinessTrainingPairDTO);

     //Creating HeaderColumnNameMappingStrategy
     HeaderColumnNameMappingStrategy<LocalBusinessTrainingPairDTO> mappingStrategy = new HeaderColumnNameMappingStrategy<>();
     mappingStrategy.setType(LocalBusinessTrainingPairDTO.class);
     //Setting predefined order using String comparator
     mappingStrategy.setColumnOrderOnWrite(new OrderedComparatorIgnoringCase(LocalBusinessTrainingPairDTO.FIELDS_ORDER));
     String csv = convertToCsv(localBusinessTrainingPairsDTO, mappingStrategy);
     System.out.println(csv);
}

Lire le fichier CSV exporté dans POJO (en plus de la réponse d'origine)

Important: le format CSV peut être désordonné, car nous utilisons toujours la liaison par nom:

public static void main(String[] args) throws Exception {
    //omitted code from writing
    String csv = convertToCsv(localBusinessTrainingPairsDTO, mappingStrategy);

    //Exported CSV should be compatible for further import
    File temp = File.createTempFile("tempTrainingPairs", ".csv");
    temp.deleteOnExit();
    BufferedWriter bw = new BufferedWriter(new FileWriter(temp));
    bw.write(csv);
    bw.close();
    MultipartFile multipartFile = new MockMultipartFile("tempTrainingPairs.csv", new FileInputStream(temp));

    List<LocalBusinessTrainingPairDTO> localBusinessTrainingPairDTOList = convertFromCsv(multipartFile, LocalBusinessTrainingPairDTO.class);
}

De conclure:

  1. Nous pouvons lire CSV en POJO, quel que soit l'ordre des colonnes - car nous utilisons En utilisant @CsvBindByName 
  2. Nous pouvons contrôler l'ordre des colonnes lors de l'écriture à l'aide du comparateur personnalisé