View Javadoc
1   /*
2    * Copyright (c) 2002-2022, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.portal.service.csv;
35  
36  import java.io.ByteArrayInputStream;
37  import java.io.FileReader;
38  import java.io.IOException;
39  import java.io.InputStream;
40  import java.io.InputStreamReader;
41  import java.io.Reader;
42  import java.util.ArrayList;
43  import java.util.Collections;
44  import java.util.Iterator;
45  import java.util.List;
46  import java.util.Locale;
47  
48  import org.apache.commons.collections.CollectionUtils;
49  import org.apache.commons.fileupload.FileItem;
50  
51  import au.com.bytecode.opencsv.CSVReader;
52  import fr.paris.lutece.portal.business.file.File;
53  import fr.paris.lutece.portal.business.physicalfile.PhysicalFile;
54  import fr.paris.lutece.portal.business.physicalfile.PhysicalFileHome;
55  import fr.paris.lutece.portal.service.i18n.I18nService;
56  import fr.paris.lutece.portal.service.util.AppLogService;
57  import fr.paris.lutece.portal.service.util.AppPropertiesService;
58  import fr.paris.lutece.util.stream.StreamUtil;
59  
60  /**
61   * Service to get data from a CSV file. The CSV might be a physical file, or a memory file. Implementations can either be statefull or stateless, but if the
62   * separator or the escape character are controlled by the user, then it has to be statefull.
63   */
64  public abstract class CSVReaderService
65  {
66      private static final String MESSAGE_NO_FILE_FOUND = "portal.util.message.noFileFound";
67      private static final String MESSAGE_ERROR_READING_FILE = "portal.util.message.errorReadingFile";
68      private static final String MESSAGE_ERROR_NUMBER_COLUMNS = "portal.xsl.message.errorNumberColumns";
69      private static final String MESSAGE_UNKOWN_ERROR = "portal.xsl.message.errorUnknown";
70      private static final String PROPERTY_DEFAULT_CSV_SEPARATOR = "lutece.csvReader.defaultCSVSeparator";
71      private static final String PROPERTY_DEFAULT_CSV_ESCAPE_CHARACTER = "lutece.csvReader.defaultCSVEscapeCharacter";
72      private static final String CONSTANT_DEFAULT_CSV_SEPARATOR = ";";
73      private static final String CONSTANT_DEFAULT_CSV_ESCAPE_CHARACTER = "\"";
74      private Character _strCSVSeparator;
75      private Character _strCSVEscapeCharacter;
76  
77      /**
78       * Read a line of the CSV file.
79       * 
80       * @param strLineDataArray
81       *            The content of the line of the CSV file.
82       * @param nLineNumber
83       *            Number of the current line
84       * @param locale
85       *            the locale
86       * @param strBaseUrl
87       *            The base URL
88       * @return Returns the list of messages associated with the line.
89       */
90      protected abstract List<CSVMessageDescriptor> readLineOfCSVFile( String [ ] strLineDataArray, int nLineNumber, Locale locale, String strBaseUrl );
91  
92      /**
93       * Check the line of the CSV file. This method is called once on each line of the file if the number of columns is correct. If the file is entirely checked
94       * before processing, then this method is called before any line is processed. Otherwise it is called just before the processing of the line.
95       * 
96       * @param strLineDataArray
97       *            The content of the line of the CSV file.
98       * @param nLineNumber
99       *            Number of the current line
100      * @param locale
101      *            the locale
102      * @return The list of messages of the lines. <strong>Lines that contain messages with messages levels other than {@link CSVMessageLevel#INFO INFO} will NOT
103      *         be processed, and the global processing may stop if the ExitOnError flag has been set to true !</strong>
104      */
105     protected abstract List<CSVMessageDescriptor> checkLineOfCSVFile( String [ ] strLineDataArray, int nLineNumber, Locale locale );
106 
107     /**
108      * Get messages after the process is completed.
109      * 
110      * @param nNbLineParses
111      *            The number of lines parses. If the first line was skipped, it is not counted.
112      * @param nNbLinesWithoutErrors
113      *            the number of lines parses whitout error.
114      * @param locale
115      *            The locale
116      * @return A list of messages.
117      */
118     protected abstract List<CSVMessageDescriptor> getEndOfProcessMessages( int nNbLineParses, int nNbLinesWithoutErrors, Locale locale );
119 
120     /**
121      * Get the default CSV separator to use. If the property of the default separator to use is not set, then the semi-colon is returned.
122      * 
123      * @return the default CSV separator to use
124      */
125     public static Character getDefaultCSVSeparator( )
126     {
127         return AppPropertiesService.getProperty( PROPERTY_DEFAULT_CSV_SEPARATOR, CONSTANT_DEFAULT_CSV_SEPARATOR ).charAt( 0 );
128     }
129 
130     /**
131      * Get the default CSV escape character to use. If the property of the default escape character to use is not set, then the comma is returned.
132      * 
133      * @return the default CSV escape character to use
134      */
135     public static Character getDefaultCSVEscapeCharacter( )
136     {
137         return AppPropertiesService.getProperty( PROPERTY_DEFAULT_CSV_ESCAPE_CHARACTER, CONSTANT_DEFAULT_CSV_ESCAPE_CHARACTER ).charAt( 0 );
138     }
139 
140     /**
141      * Read a CSV file and call the method {@link #readLineOfCSVFile(String[], int, Locale, String) readLineOfCSVFile} for each of its lines.
142      * 
143      * @param fileItem
144      *            FileItem to get the CSV file from. If the creation of the input stream associated to this file throws a IOException, then an error is returned
145      *            and the file is not red.
146      * @param nColumnNumber
147      *            Number of columns of each lines. Use 0 to skip column number check (for example if every lines don't have the same number of columns)
148      * @param bCheckFileBeforeProcessing
149      *            Indicates if the file should be check before processing any of its line. If it is set to true, then then no line is processed if the file has
150      *            any error.
151      * @param bExitOnError
152      *            Indicates if the processing of the CSV file should end on the first error, or at the end of the file.
153      * @param bSkipFirstLine
154      *            Indicates if the first line of the file should be skipped or not.
155      * @param locale
156      *            the locale
157      * @param strBaseUrl
158      *            The base URL
159      * @return Returns the list of errors that occurred during the processing of the file. The returned list is sorted
160      * @see CSVMessageDescriptor#compareTo(CSVMessageDescriptor) CSVMessageDescriptor.compareTo(CSVMessageDescriptor) for information about sort
161      */
162     public List<CSVMessageDescriptor> readCSVFile( FileItem fileItem, int nColumnNumber, boolean bCheckFileBeforeProcessing, boolean bExitOnError,
163             boolean bSkipFirstLine, Locale locale, String strBaseUrl )
164     {
165         if ( fileItem != null )
166         {
167             InputStreamReader inputStreamReader = null;
168 
169             try
170             {
171                 inputStreamReader = new InputStreamReader( fileItem.getInputStream( ) );
172             }
173             catch( IOException e )
174             {
175                 AppLogService.error( e.getMessage( ), e );
176             }
177 
178             if ( inputStreamReader != null )
179             {
180                 CSVReader csvReader = new CSVReader( inputStreamReader, getCSVSeparator( ), getCSVEscapeCharacter( ) );
181 
182                 return readCSVFile( inputStreamReader, csvReader, nColumnNumber, bCheckFileBeforeProcessing, bExitOnError, bSkipFirstLine, locale, strBaseUrl );
183             }
184         }
185 
186         List<CSVMessageDescriptor> listErrors = new ArrayList<>( );
187         CSVMessageDescriptorescriptor.html#CSVMessageDescriptor">CSVMessageDescriptor errorDescription = new CSVMessageDescriptor( CSVMessageLevel.ERROR, 0,
188                 I18nService.getLocalizedString( MESSAGE_NO_FILE_FOUND, locale ) );
189         listErrors.add( errorDescription );
190 
191         return listErrors;
192     }
193 
194     /**
195      * Read a CSV file and call the method {@link #readLineOfCSVFile(String[], int, Locale, String) readLineOfCSVFile} for each of its lines.
196      * 
197      * @param strPath
198      *            Path if the file to read in the file system.
199      * @param nColumnNumber
200      *            Number of columns of each lines. Use 0 to skip column number check (for example if every lines don't have the same number of columns)
201      * @param bCheckFileBeforeProcessing
202      *            Indicates if the file should be check before processing any of its line. If it is set to true, then then no line is processed if the file has
203      *            any error.
204      * @param bExitOnError
205      *            Indicates if the processing of the CSV file should end on the first error, or at the end of the file.
206      * @param bSkipFirstLine
207      *            Indicates if the first line of the file should be skipped or not.
208      * @param locale
209      *            the locale
210      * @param strBaseUrl
211      *            The base URL
212      * @return Returns the list of errors that occurred during the processing of the file. The returned list is sorted
213      * @see CSVMessageDescriptor#compareTo(CSVMessageDescriptor) CSVMessageDescriptor.compareTo(CSVMessageDescriptor) for information about sort
214      */
215     public List<CSVMessageDescriptor> readCSVFile( String strPath, int nColumnNumber, boolean bCheckFileBeforeProcessing, boolean bExitOnError,
216             boolean bSkipFirstLine, Locale locale, String strBaseUrl )
217     {
218         java.io.File file = new java.io.File( strPath );
219 
220         try ( FileReader fileReader = new FileReader( file ) )
221         {
222             CSVReader csvReader = new CSVReader( fileReader, getCSVSeparator( ), getCSVEscapeCharacter( ) );
223 
224             return readCSVFile( fileReader, csvReader, nColumnNumber, bCheckFileBeforeProcessing, bExitOnError, bSkipFirstLine, locale, strBaseUrl );
225         }
226         catch( IOException e )
227         {
228             AppLogService.error( e.getMessage( ), e );
229         }
230 
231         List<CSVMessageDescriptor> listErrors = new ArrayList<>( );
232         CSVMessageDescriptorescriptor.html#CSVMessageDescriptor">CSVMessageDescriptor errorDescription = new CSVMessageDescriptor( CSVMessageLevel.ERROR, 0,
233                 I18nService.getLocalizedString( MESSAGE_NO_FILE_FOUND, locale ) );
234         listErrors.add( errorDescription );
235 
236         return listErrors;
237     }
238 
239     /**
240      * Read a CSV file and call the method {@link #readLineOfCSVFile(String[], int, Locale, String) readLineOfCSVFile} for each of its lines.
241      * 
242      * @param file
243      *            File to get the values from. If the physical file of this file has no value, then it is gotten from the database.
244      * @param nColumnNumber
245      *            Number of columns of each lines. Use 0 to skip column number check (for example if every lines don't have the same number of columns)
246      * @param bCheckFileBeforeProcessing
247      *            Indicates if the file should be check before processing any of its line. If it is set to true, then then no line is processed if the file has
248      *            any error.
249      * @param bExitOnError
250      *            Indicates if the processing of the CSV file should end on the first error, or at the end of the file.
251      * @param bSkipFirstLine
252      *            Indicates if the first line of the file should be skipped or not.
253      * @param locale
254      *            the locale
255      * @param strBaseUrl
256      *            The base URL
257      * @return Returns the list of errors that occurred during the processing of the file. The returned list is sorted
258      * @see CSVMessageDescriptor#compareTo(CSVMessageDescriptor) CSVMessageDescriptor.compareTo(CSVMessageDescriptor) for information about sort
259      */
260     public List<CSVMessageDescriptor> readCSVFile( File file, int nColumnNumber, boolean bCheckFileBeforeProcessing, boolean bExitOnError,
261             boolean bSkipFirstLine, Locale locale, String strBaseUrl )
262     {
263         return readCSVFile( file.getPhysicalFile( ), nColumnNumber, bCheckFileBeforeProcessing, bExitOnError, bSkipFirstLine, locale, strBaseUrl );
264     }
265 
266     /**
267      * Read a CSV file and call the method {@link #readLineOfCSVFile(String[], int, Locale, String) readLineOfCSVFile} for each of its lines.
268      * 
269      * @param physicalFile
270      *            The physicalFile to get the values from. If the physical file has no value, then it is gotten from the database.
271      * @param nColumnNumber
272      *            Number of columns of each lines. Use 0 to skip column number check (for example if every lines don't have the same number of columns)
273      * @param bCheckFileBeforeProcessing
274      *            Indicates if the file should be check before processing any of its line. If it is set to true, then then no line is processed if the file has
275      *            any error.
276      * @param bExitOnError
277      *            Indicates if the processing of the CSV file should end on the first error, or at the end of the file.
278      * @param bSkipFirstLine
279      *            Indicates if the first line of the file should be skipped or not.
280      * @param locale
281      *            the locale
282      * @param strBaseUrl
283      *            The base URL
284      * @return Returns the list of errors that occurred during the processing of the file. The returned list is sorted
285      * @see CSVMessageDescriptor#compareTo(CSVMessageDescriptor) CSVMessageDescriptor.compareTo(CSVMessageDescriptor) for information about sort
286      */
287     public List<CSVMessageDescriptor> readCSVFile( PhysicalFile physicalFile, int nColumnNumber, boolean bCheckFileBeforeProcessing, boolean bExitOnError,
288             boolean bSkipFirstLine, Locale locale, String strBaseUrl )
289     {
290         PhysicalFile importedPhysicalFile = physicalFile;
291 
292         if ( ( importedPhysicalFile != null ) && ( importedPhysicalFile.getValue( ) == null ) )
293         {
294             if ( importedPhysicalFile.getValue( ) == null )
295             {
296                 importedPhysicalFile = PhysicalFileHome.findByPrimaryKey( importedPhysicalFile.getIdPhysicalFile( ) );
297             }
298 
299             if ( ( importedPhysicalFile != null ) && ( importedPhysicalFile.getValue( ) == null ) )
300             {
301                 InputStream inputStream = new ByteArrayInputStream( importedPhysicalFile.getValue( ) );
302                 InputStreamReader inputStreamReader = new InputStreamReader( inputStream );
303                 CSVReader csvReader = new CSVReader( inputStreamReader, getCSVSeparator( ), getCSVEscapeCharacter( ) );
304 
305                 return readCSVFile( inputStreamReader, csvReader, nColumnNumber, bCheckFileBeforeProcessing, bExitOnError, bSkipFirstLine, locale, strBaseUrl );
306             }
307         }
308 
309         List<CSVMessageDescriptor> listErrors = new ArrayList<>( );
310         CSVMessageDescriptorescriptor.html#CSVMessageDescriptor">CSVMessageDescriptor errorDescription = new CSVMessageDescriptor( CSVMessageLevel.ERROR, 0,
311                 I18nService.getLocalizedString( MESSAGE_NO_FILE_FOUND, locale ) );
312         listErrors.add( errorDescription );
313 
314         return listErrors;
315     }
316 
317     /**
318      * Read a CSV file and call the method {@link #readLineOfCSVFile(String[], int, Locale, String) readLineOfCSVFile} for each of its lines.
319      * 
320      * @param reader
321      *            The file reader that was used to create the CSV reader. This reader will be closed by this method
322      * @param csvReader
323      *            CSV reader to use to read the CSV file
324      * @param nColumnNumber
325      *            Number of columns of each lines. Use 0 to skip column number check (for example if every lines don't have the same number of columns)
326      * @param bCheckFileBeforeProcessing
327      *            Indicates if the file should be check before processing any of its line. If it is set to true, then then no line is processed if the file has
328      *            any error.
329      * @param bExitOnError
330      *            Indicates if the processing of the CSV file should end on the first error, or at the end of the file.
331      * @param bSkipFirstLine
332      *            Indicates if the first line of the file should be skipped or not.
333      * @param locale
334      *            the locale
335      * @param strBaseUrl
336      *            The base URL
337      * @return Returns the list of errors that occurred during the processing of the file. The returned list is sorted
338      * @see CSVMessageDescriptor#compareTo(CSVMessageDescriptor) CSVMessageDescriptor.compareTo(CSVMessageDescriptor) for information about sort
339      */
340     protected List<CSVMessageDescriptor> readCSVFile( Reader reader, CSVReader csvReader, int nColumnNumber, boolean bCheckFileBeforeProcessing,
341             boolean bExitOnError, boolean bSkipFirstLine, Locale locale, String strBaseUrl )
342     {
343         List<CSVMessageDescriptor> listMessages = new ArrayList<>( );
344         int nLineNumber = 0;
345 
346         if ( bSkipFirstLine )
347         {
348             try
349             {
350                 nLineNumber++;
351                 csvReader.readNext( );
352             }
353             catch( IOException e )
354             {
355                 AppLogService.error( e.getMessage( ), e );
356 
357                 CSVMessageDescriptorCSVMessageDescriptor.html#CSVMessageDescriptor">CSVMessageDescriptor error = new CSVMessageDescriptor( CSVMessageLevel.ERROR, 1,
358                         I18nService.getLocalizedString( MESSAGE_ERROR_READING_FILE, locale ) );
359                 listMessages.add( error );
360 
361                 if ( bExitOnError )
362                 {
363                     StreamUtil.safeClose( csvReader );
364                     StreamUtil.safeClose( reader );
365 
366                     return listMessages;
367                 }
368             }
369         }
370 
371         List<String [ ]> listLines = null;
372 
373         if ( bCheckFileBeforeProcessing )
374         {
375             listLines = new ArrayList<>( );
376 
377             String [ ] strLine = null;
378 
379             do
380             {
381                 try
382                 {
383                     nLineNumber++;
384                     strLine = csvReader.readNext( );
385                 }
386                 catch( IOException e )
387                 {
388                     AppLogService.error( e.getMessage( ), e );
389 
390                     CSVMessageDescriptorCSVMessageDescriptor.html#CSVMessageDescriptor">CSVMessageDescriptor error = new CSVMessageDescriptor( CSVMessageLevel.ERROR, nLineNumber,
391                             I18nService.getLocalizedString( MESSAGE_ERROR_READING_FILE, locale ) );
392                     listMessages.add( error );
393 
394                     if ( bExitOnError )
395                     {
396                         StreamUtil.safeClose( csvReader );
397                         StreamUtil.safeClose( reader );
398 
399                         Collections.sort( listMessages );
400 
401                         return listMessages;
402                     }
403                 }
404 
405                 if ( strLine != null )
406                 {
407                     listLines.add( strLine );
408                 }
409             }
410             while ( strLine != null );
411 
412             List<CSVMessageDescriptor> listCheckErrors = checkCSVFileValidity( listLines, nColumnNumber, bSkipFirstLine, locale );
413 
414             if ( CollectionUtils.isNotEmpty( listCheckErrors ) && doesListMessageContainError( listCheckErrors ) )
415             {
416                 listCheckErrors.addAll( 0, listMessages );
417                 StreamUtil.safeClose( csvReader );
418                 StreamUtil.safeClose( reader );
419 
420                 Collections.sort( listMessages );
421 
422                 return listCheckErrors;
423             }
424 
425             nLineNumber = 0;
426         }
427 
428         boolean bHasMoreLines = true;
429         int nNbLinesWithoutErrors = 0;
430         String [ ] strLine = null;
431         Iterator<String [ ]> iterator = null;
432 
433         if ( listLines != null )
434         {
435             iterator = listLines.iterator( );
436         }
437 
438         while ( bHasMoreLines )
439         {
440             nLineNumber++;
441 
442             if ( iterator != null )
443             {
444                 if ( iterator.hasNext( ) )
445                 {
446                     strLine = iterator.next( );
447                 }
448                 else
449                 {
450                     strLine = null;
451                     bHasMoreLines = false;
452                 }
453             }
454             else
455             {
456                 try
457                 {
458                     strLine = csvReader.readNext( );
459                 }
460                 catch( IOException e )
461                 {
462                     strLine = null;
463                     AppLogService.error( e.getMessage( ), e );
464 
465                     CSVMessageDescriptorCSVMessageDescriptor.html#CSVMessageDescriptor">CSVMessageDescriptor error = new CSVMessageDescriptor( CSVMessageLevel.ERROR, nLineNumber,
466                             I18nService.getLocalizedString( MESSAGE_ERROR_READING_FILE, locale ) );
467                     listMessages.add( error );
468 
469                     if ( bExitOnError )
470                     {
471                         bHasMoreLines = false;
472                     }
473                 }
474             }
475 
476             if ( strLine != null )
477             {
478                 try
479                 {
480                     List<CSVMessageDescriptor> listLinesMessages = null;
481 
482                     if ( !bCheckFileBeforeProcessing )
483                     {
484                         listLinesMessages = checkCSVLineColumnNumber( strLine, nColumnNumber, nLineNumber, locale );
485 
486                         if ( !doesListMessageContainError( listLinesMessages ) )
487                         {
488                             List<CSVMessageDescriptor> listFileCheckMessages = checkLineOfCSVFile( strLine, nLineNumber, locale );
489 
490                             if ( CollectionUtils.isNotEmpty( listFileCheckMessages ) )
491                             {
492                                 if ( CollectionUtils.isNotEmpty( listLinesMessages ) )
493                                 {
494                                     listLinesMessages.addAll( listFileCheckMessages );
495                                 }
496                                 else
497                                 {
498                                     listLinesMessages = listFileCheckMessages;
499                                 }
500                             }
501                         }
502 
503                         if ( CollectionUtils.isNotEmpty( listLinesMessages ) )
504                         {
505                             listMessages.addAll( listLinesMessages );
506                         }
507                     }
508 
509                     // If the line has no error
510                     if ( !doesListMessageContainError( listLinesMessages ) )
511                     {
512                         List<CSVMessageDescriptor> listMessagesOfCurrentLine = readLineOfCSVFile( strLine, nLineNumber, locale, strBaseUrl );
513 
514                         if ( CollectionUtils.isNotEmpty( listMessagesOfCurrentLine ) )
515                         {
516                             listMessages.addAll( listMessagesOfCurrentLine );
517                         }
518 
519                         if ( doesListMessageContainError( listMessagesOfCurrentLine ) )
520                         {
521                             if ( bExitOnError )
522                             {
523                                 bHasMoreLines = false;
524                             }
525                         }
526                         else
527                         {
528                             nNbLinesWithoutErrors++;
529                         }
530                     }
531                 }
532                 catch( Exception e )
533                 {
534                     AppLogService.error( e.getMessage( ), e );
535 
536                     CSVMessageDescriptorCSVMessageDescriptor.html#CSVMessageDescriptor">CSVMessageDescriptor error = new CSVMessageDescriptor( CSVMessageLevel.ERROR, nLineNumber,
537                             I18nService.getLocalizedString( MESSAGE_UNKOWN_ERROR, locale ) );
538                     listMessages.add( error );
539 
540                     if ( bExitOnError )
541                     {
542                         bHasMoreLines = false;
543                     }
544                 }
545             }
546             else
547             {
548                 bHasMoreLines = false;
549             }
550         }
551         StreamUtil.safeClose( csvReader );
552         StreamUtil.safeClose( reader );
553 
554         // We incremented the line number for the last line that didn't exist
555         nLineNumber--;
556 
557         if ( bSkipFirstLine )
558         {
559             nLineNumber--;
560         }
561 
562         List<CSVMessageDescriptor> listMessagesEndOfProcess = getEndOfProcessMessages( nLineNumber, nNbLinesWithoutErrors, locale );
563 
564         if ( CollectionUtils.isNotEmpty( listMessagesEndOfProcess ) )
565         {
566             listMessages.addAll( 0, listMessagesEndOfProcess );
567         }
568 
569         Collections.sort( listMessages );
570 
571         return listMessages;
572     }
573 
574     /**
575      * Check the validity of the whole CSV file.
576      * 
577      * @param listLines
578      *            The list of lines of the file
579      * @param nColumnNumber
580      *            The number of columns every line must have.
581      * @param bSkipFirstLine
582      *            True if the first line should be ignored, false otherwise
583      * @param locale
584      *            The locale
585      * @return Returns a list of errors found in the file.
586      */
587     protected List<CSVMessageDescriptor> checkCSVFileValidity( List<String [ ]> listLines, int nColumnNumber, boolean bSkipFirstLine, Locale locale )
588     {
589         List<CSVMessageDescriptor> listErrors = new ArrayList<>( );
590         int nLineNumber = 0;
591 
592         if ( bSkipFirstLine )
593         {
594             nLineNumber++;
595         }
596 
597         for ( String [ ] strLine : listLines )
598         {
599             nLineNumber++;
600 
601             List<CSVMessageDescriptor> listMessages = checkCSVLineColumnNumber( strLine, nColumnNumber, nLineNumber, locale );
602 
603             if ( CollectionUtils.isNotEmpty( listMessages ) )
604             {
605                 listErrors.addAll( listMessages );
606             }
607 
608             if ( !doesListMessageContainError( listMessages ) )
609             {
610                 listMessages = checkLineOfCSVFile( strLine, nLineNumber, locale );
611 
612                 if ( CollectionUtils.isNotEmpty( listMessages ) )
613                 {
614                     listErrors.addAll( listMessages );
615                 }
616             }
617         }
618 
619         return listErrors;
620     }
621 
622     /**
623      * Check the number of columns of a line.
624      * 
625      * @param strLine
626      *            The line to check
627      * @param nColumnNumber
628      *            The number of columns the line must have
629      * @param nLineNumber
630      *            The number of the current line
631      * @param locale
632      *            The locale
633      * @return The error if an error is found, or null if there is none.
634      */
635     protected List<CSVMessageDescriptor> checkCSVLineColumnNumber( String [ ] strLine, int nColumnNumber, int nLineNumber, Locale locale )
636     {
637         if ( ( strLine == null ) || ( ( nColumnNumber > 0 ) && ( strLine.length != nColumnNumber ) ) )
638         {
639             List<CSVMessageDescriptor> listMessages = new ArrayList<>( );
640             Object [ ] args = {
641                     ( strLine == null ) ? 0 : strLine.length, nColumnNumber
642             };
643             String strErrorMessage = I18nService.getLocalizedString( MESSAGE_ERROR_NUMBER_COLUMNS, args, locale );
644             CSVMessageDescriptorCSVMessageDescriptor.html#CSVMessageDescriptor">CSVMessageDescriptor error = new CSVMessageDescriptor( CSVMessageLevel.ERROR, nLineNumber, strErrorMessage );
645             listMessages.add( error );
646 
647             return listMessages;
648         }
649 
650         return null;
651     }
652 
653     /**
654      * Get the separator used for CSV files. If no separator has been set, then the default CSV separator is used.
655      * 
656      * @return the separator used for CSV files, of the default one if non has been set.
657      */
658     public Character getCSVSeparator( )
659     {
660         if ( this._strCSVSeparator == null )
661         {
662             this._strCSVSeparator = getDefaultCSVSeparator( );
663         }
664 
665         return _strCSVSeparator;
666     }
667 
668     /**
669      * Set the separator to use for CSV files.
670      * 
671      * @param strCSVSeparator
672      *            The separator to use for CSV files.
673      */
674     public void setCSVSeparator( Character strCSVSeparator )
675     {
676         this._strCSVSeparator = strCSVSeparator;
677     }
678 
679     /**
680      * Get the escape character used for CSV files. If no escape character has been set, then the default CSV escape character is used.
681      * 
682      * @return the escape character used for CSV files, of the default one if non has been set.
683      */
684     public Character getCSVEscapeCharacter( )
685     {
686         if ( this._strCSVEscapeCharacter == null )
687         {
688             this._strCSVEscapeCharacter = getDefaultCSVEscapeCharacter( );
689         }
690 
691         return _strCSVEscapeCharacter;
692     }
693 
694     /**
695      * Set the escape character to use for CSV files.
696      * 
697      * @param strCSVEscapeCharacter
698      *            The escape character to use for CSV files.
699      */
700     public void setCSVEscapeCharacter( Character strCSVEscapeCharacter )
701     {
702         this._strCSVEscapeCharacter = strCSVEscapeCharacter;
703     }
704 
705     /**
706      * Check if a list of messages contains messages with the {@link CSVMessageLevel#ERROR ERROR} level
707      * 
708      * @param listMessageOfCurrentLine
709      *            The list of messages. The list might be null or empty.
710      * @return True if an error is found, false otherwise
711      */
712     private boolean doesListMessageContainError( List<CSVMessageDescriptor> listMessageOfCurrentLine )
713     {
714         if ( CollectionUtils.isNotEmpty( listMessageOfCurrentLine ) )
715         {
716             for ( CSVMessageDescriptor message : listMessageOfCurrentLine )
717             {
718                 if ( message.getMessageLevel( ) == CSVMessageLevel.ERROR )
719                 {
720                     return true;
721                 }
722             }
723         }
724 
725         return false;
726     }
727 }