1 /*
2 * Copyright (c) 2002-2025, 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 }