001/*
002 *   Licensed to the Apache Software Foundation (ASF) under one
003 *   or more contributor license agreements.  See the NOTICE file
004 *   distributed with this work for additional information
005 *   regarding copyright ownership.  The ASF licenses this file
006 *   to you under the Apache License, Version 2.0 (the
007 *   "License"); you may not use this file except in compliance
008 *   with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *   Unless required by applicable law or agreed to in writing,
013 *   software distributed under the License is distributed on an
014 *   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *   KIND, either express or implied.  See the License for the
016 *   specific language governing permissions and limitations
017 *   under the License.
018 *
019 */
020
021package org.apache.directory.api.util;
022
023
024import java.io.File;
025import java.io.FileInputStream;
026import java.io.FileNotFoundException;
027import java.io.FileOutputStream;
028import java.io.IOException;
029import java.io.InputStream;
030import java.io.OutputStream;
031import java.nio.channels.FileChannel;
032import java.nio.charset.Charset;
033import java.nio.file.Files;
034import java.nio.file.Paths;
035import java.nio.file.StandardOpenOption;
036import java.util.List;
037
038
039/**
040 * This code comes from Apache commons.io library.
041 * 
042 * Origin of code: Excalibur, Alexandria, Tomcat, Commons-Utils.
043 *
044 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
045 */
046public final class FileUtils
047{
048    /**
049     * The Windows separator character.
050     */
051    private static final char WINDOWS_SEPARATOR = '\\';
052
053    /**
054     * The system separator character.
055     */
056    private static final char SYSTEM_SEPARATOR = File.separatorChar;
057
058    /**
059     * The number of bytes in a kilobyte.
060     */
061    public static final long ONE_KB = 1024;
062
063    /**
064     * The number of bytes in a megabyte.
065     */
066    public static final long ONE_MB = ONE_KB * ONE_KB;
067
068    /**
069     * The file copy buffer size (30 MB)
070     */
071    private static final long FILE_COPY_BUFFER_SIZE = ONE_MB * 30;
072
073
074    /**
075     * Creates a new instance of FileUtils.
076     */
077    private FileUtils()
078    {
079        // Nothing to do.
080    }
081
082
083    /**
084     * Deletes a directory recursively.
085     *
086     * @param directory  directory to delete
087     * @throws IOException in case deletion is unsuccessful
088     */
089    public static void deleteDirectory( File directory ) throws IOException
090    {
091        if ( !directory.exists() )
092        {
093            return;
094        }
095
096        if ( !isSymlink( directory ) )
097        {
098            cleanDirectory( directory );
099        }
100
101        if ( !directory.delete() )
102        {
103            String message = "Unable to delete directory " + directory + ".";
104            throw new IOException( message );
105        }
106    }
107
108
109    /**
110     * Determines whether the specified file is a Symbolic Link rather than an actual file.
111     * <p>
112     * Will not return true if there is a Symbolic Link anywhere in the path,
113     * only if the specific file is.
114     * <p>
115     * <b>Note:</b> the current implementation always returns {@code false} if the system
116     * is detected as Windows.
117     * <p>
118     * For code that runs on Java 1.7 or later, use the following method instead:
119     * <br>
120     * {@code boolean java.nio.file.Files.isSymbolicLink(Path path)}
121     * @param file the file to check
122     * @return true if the file is a Symbolic Link
123     * @throws IOException if an IO error occurs while checking the file
124     * @since 2.0
125     */
126    public static boolean isSymlink( File file ) throws IOException
127    {
128        if ( file == null )
129        {
130            throw new NullPointerException( "File must not be null" );
131        }
132
133        if ( SYSTEM_SEPARATOR == WINDOWS_SEPARATOR )
134        {
135            return false;
136        }
137
138        File fileInCanonicalDir = null;
139
140        if ( file.getParent() == null )
141        {
142            fileInCanonicalDir = file;
143        }
144        else
145        {
146            File canonicalDir = file.getParentFile().getCanonicalFile();
147            fileInCanonicalDir = new File( canonicalDir, file.getName() );
148        }
149
150        return !fileInCanonicalDir.getCanonicalFile().equals( fileInCanonicalDir.getAbsoluteFile() );
151    }
152
153
154    /**
155     * Deletes a directory recursively.
156     *
157     * @param directory  directory to delete
158     * @throws IOException in case deletion is unsuccessful
159     */
160    public static void cleanDirectory( File directory ) throws IOException
161    {
162        if ( !directory.exists() )
163        {
164            String message = directory + " does not exist";
165            throw new IllegalArgumentException( message );
166        }
167
168        if ( !directory.isDirectory() )
169        {
170            String message = directory + " is not a directory";
171            throw new IllegalArgumentException( message );
172        }
173
174        File[] files = directory.listFiles();
175
176        if ( files == null )
177        {
178            // null if security restricted
179            String message = "Failed to list contents of " + directory;
180            throw new IOException( message );
181        }
182
183        IOException exception = null;
184
185        for ( File file : files )
186        {
187            try
188            {
189                forceDelete( file );
190            }
191            catch ( IOException ioe )
192            {
193                exception = ioe;
194            }
195        }
196
197        if ( null != exception )
198        {
199            throw exception;
200        }
201    }
202
203
204    /**
205     * Deletes a file. If file is a directory, delete it and all sub-directories.
206     * <p>
207     * The difference between File.delete() and this method are:
208     * <ul>
209     * <li>A directory to be deleted does not have to be empty.</li>
210     * <li>You get exceptions when a file or directory cannot be deleted.
211     *      (java.io.File methods returns a boolean)</li>
212     * </ul>
213     *
214     * @param file  file or directory to delete, must not be {@code null}
215     * @throws NullPointerException if the directory is {@code null}
216     * @throws FileNotFoundException if the file was not found
217     * @throws IOException in case deletion is unsuccessful
218     */
219    public static void forceDelete( File file ) throws IOException
220    {
221        if ( file.isDirectory() )
222        {
223            deleteDirectory( file );
224        }
225        else
226        {
227            boolean filePresent = file.exists();
228
229            if ( !file.delete() )
230            {
231                if ( !filePresent )
232                {
233                    String message = "File does not exist: " + file;
234                    throw new FileNotFoundException( message );
235                }
236
237                String message = "Unable to delete file: " + file;
238                throw new IOException( message );
239            }
240        }
241    }
242
243
244    /**
245     * Returns the path to the system temporary directory.
246     *
247     * @return the path to the system temporary directory.
248     *
249     * @since 2.0
250     */
251    public static String getTempDirectoryPath()
252    {
253        return System.getProperty( "java.io.tmpdir" );
254    }
255
256
257    /**
258     * Reads the contents of a file into a String using the default encoding for the VM.
259     * The file is always closed.
260     *
261     * @param file  the file to read, must not be {@code null}
262     * @return the file contents, never {@code null}
263     * @throws IOException in case of an I/O error
264     * @since 1.3.1
265     * @deprecated 2.5 use {@link #readFileToString(File, Charset)} instead
266     */
267    @Deprecated
268    public static String readFileToString( File file ) throws IOException
269    {
270        return readFileToString( file, Charset.defaultCharset() );
271    }
272
273
274    /**
275     * Reads the contents of a file into a String.
276     * The file is always closed.
277     *
278     * @param file  the file to read, must not be {@code null}
279     * @param encoding  the encoding to use, {@code null} means platform default
280     * @return the file contents, never {@code null}
281     * @throws IOException in case of an I/O error
282     * @since 2.3
283     */
284    public static String readFileToString( File file, Charset encoding ) throws IOException
285    {
286        InputStream in = null;
287
288        try
289        {
290            in = openInputStream( file );
291            return IOUtils.toString( in, IOUtils.toCharset( encoding ) );
292        }
293        finally
294        {
295            IOUtils.closeQuietly( in );
296        }
297    }
298
299
300    /**
301     * Reads the contents of a file into a String. The file is always closed.
302     *
303     * @param file the file to read, must not be {@code null}
304     * @param encoding the encoding to use, {@code null} means platform default
305     * @return the file contents, never {@code null}
306     * @throws IOException in case of an I/O error
307     * @since 2.3
308     */
309    public static String readFileToString( File file, String encoding ) throws IOException
310    {
311        InputStream in = null;
312
313        try
314        {
315            in = openInputStream( file );
316            return IOUtils.toString( in, IOUtils.toCharset( encoding ) );
317        }
318        finally
319        {
320            IOUtils.closeQuietly( in );
321        }
322    }
323
324
325    /**
326     * Opens a {@link FileInputStream} for the specified file, providing better
327     * error messages than simply calling <code>new FileInputStream(file)</code>.
328     * <p>
329     * At the end of the method either the stream will be successfully opened,
330     * or an exception will have been thrown.
331     * <p>
332     * An exception is thrown if the file does not exist.
333     * An exception is thrown if the file object exists but is a directory.
334     * An exception is thrown if the file exists but cannot be read.
335     *
336     * @param file  the file to open for input, must not be {@code null}
337     * @return a new {@link FileInputStream} for the specified file
338     * @throws FileNotFoundException if the file does not exist
339     * @throws IOException if the file object is a directory
340     * @throws IOException if the file cannot be read
341     * @since 1.3
342     */
343    public static InputStream openInputStream( File file ) throws IOException
344    {
345        if ( file.exists() )
346        {
347            if ( file.isDirectory() )
348            {
349                throw new IOException( "File '" + file + "' exists but is a directory" );
350            }
351
352            if ( !file.canRead() )
353            {
354                throw new IOException( "File '" + file + "' cannot be read" );
355            }
356        }
357        else
358        {
359            throw new FileNotFoundException( "File '" + file + "' does not exist" );
360        }
361
362        return Files.newInputStream( Paths.get( file.getPath() ) );
363    }
364
365
366    /**
367     * Writes a String to a file creating the file if it does not exist using the default encoding for the VM.
368     *
369     * @param file  the file to write
370     * @param data  the content to write to the file
371     * @throws IOException in case of an I/O error
372     * @deprecated 2.5 use {@link #writeStringToFile(File, String, Charset, boolean)} instead
373     */
374    @Deprecated
375    public static void writeStringToFile( File file, String data ) throws IOException
376    {
377        writeStringToFile( file, data, Charset.defaultCharset(), false );
378    }
379
380
381    /**
382     * Writes a String to a file creating the file if it does not exist.
383     *
384     * NOTE: As from v1.3, the parent directories of the file will be created
385     * if they do not exist.
386     *
387     * @param file  the file to write
388     * @param data  the content to write to the file
389     * @param encoding  the encoding to use, {@code null} means platform default
390     * @throws IOException in case of an I/O error
391     * @throws java.io.UnsupportedEncodingException if the encoding is not supported by the VM
392     */
393    public static void writeStringToFile( File file, String data, String encoding ) throws IOException
394    {
395        writeStringToFile( file, data, IOUtils.toCharset( encoding ), false );
396    }
397
398
399    /**
400     * Writes a String to a file creating the file if it does not exist.
401     *
402     * @param file  the file to write
403     * @param data  the content to write to the file
404     * @param encoding  the encoding to use, {@code null} means platform default
405     * @param append if {@code true}, then the String will be added to the
406     * end of the file rather than overwriting
407     * @throws IOException in case of an I/O error
408     * @since 2.3
409     */
410    public static void writeStringToFile( File file, String data, Charset encoding, boolean append ) throws IOException
411    {
412        OutputStream out = null;
413
414        try
415        {
416            out = openOutputStream( file, append );
417            IOUtils.write( data, out, encoding );
418            out.close(); // don't swallow close Exception if copy completes normally
419        }
420        finally
421        {
422            IOUtils.closeQuietly( out );
423        }
424    }
425
426
427    /**
428     * Opens a {@link FileOutputStream} for the specified file, checking and
429     * creating the parent directory if it does not exist.
430     * <p>
431     * At the end of the method either the stream will be successfully opened,
432     * or an exception will have been thrown.
433     * <p>
434     * The parent directory will be created if it does not exist.
435     * The file will be created if it does not exist.
436     * An exception is thrown if the file object exists but is a directory.
437     * An exception is thrown if the file exists but cannot be written to.
438     * An exception is thrown if the parent directory cannot be created.
439     *
440     * @param file  the file to open for output, must not be {@code null}
441     * @param append if {@code true}, then bytes will be added to the
442     * end of the file rather than overwriting
443     * @return a new {@link FileOutputStream} for the specified file
444     * @throws IOException if the file object is a directory
445     * @throws IOException if the file cannot be written to
446     * @throws IOException if a parent directory needs creating but that fails
447     * @since 2.1
448     */
449    public static OutputStream openOutputStream( File file, boolean append ) throws IOException
450    {
451        if ( file.exists() )
452        {
453            if ( file.isDirectory() )
454            {
455                throw new IOException( "File '" + file + "' exists but is a directory" );
456            }
457
458            if ( !file.canWrite() )
459            {
460                throw new IOException( "File '" + file + "' cannot be written to" );
461            }
462        }
463        else
464        {
465            File parent = file.getParentFile();
466
467            if ( parent != null )
468            {
469                if ( !parent.mkdirs() && !parent.isDirectory() )
470                {
471                    throw new IOException( "Directory '" + parent + "' could not be created" );
472                }
473            }
474        }
475
476        if ( append )
477        {
478            return Files.newOutputStream( Paths.get( file.getPath() ), StandardOpenOption.CREATE, StandardOpenOption.APPEND );
479        }
480        else
481        {
482            return Files.newOutputStream( Paths.get( file.getPath() ) );
483        }
484    }
485
486
487    /**
488     * Returns a {@link File} representing the system temporary directory.
489     *
490     * @return the system temporary directory.
491     *
492     * @since 2.0
493     */
494    public static File getTempDirectory()
495    {
496        return new File( getTempDirectoryPath() );
497    }
498
499
500    /**
501     * Deletes a file, never throwing an exception. If file is a directory, delete it and all sub-directories.
502     * <p>
503     * The difference between File.delete() and this method are:
504     * <ul>
505     * <li>A directory to be deleted does not have to be empty.</li>
506     * <li>No exceptions are thrown when a file or directory cannot be deleted.</li>
507     * </ul>
508     *
509     * @param file  file or directory to delete, can be {@code null}
510     * @return {@code true} if the file or directory was deleted, otherwise
511     * {@code false}
512     *
513     * @since 1.4
514     */
515    public static boolean deleteQuietly( File file )
516    {
517        if ( file == null )
518        {
519            return false;
520        }
521
522        try
523        {
524            if ( file.isDirectory() )
525            {
526                cleanDirectory( file );
527            }
528        }
529        catch ( Exception ignored )
530        {
531        }
532
533        try
534        {
535            return file.delete();
536        }
537        catch ( Exception ignored )
538        {
539            return false;
540        }
541    }
542
543
544    /**
545     * Copies a file to a new location preserving the file date.
546     * <p>
547     * This method copies the contents of the specified source file to the
548     * specified destination file. The directory holding the destination file is
549     * created if it does not exist. If the destination file exists, then this
550     * method will overwrite it.
551     * <p>
552     * <strong>Note:</strong> This method tries to preserve the file's last
553     * modified date/times using {@link File#setLastModified(long)}, however
554     * it is not guaranteed that the operation will succeed.
555     * If the modification operation fails, no indication is provided.
556     *
557     * @param srcFile  an existing file to copy, must not be {@code null}
558     * @param destFile  the new file, must not be {@code null}
559     *
560     * @throws NullPointerException if source or destination is {@code null}
561     * @throws IOException if source or destination is invalid
562     * @throws IOException if an IO error occurs during copying
563     * @throws IOException if the output file length is not the same as the input file length after the copy completes
564     * @see #copyFile(File, File, boolean)
565     */
566    public static void copyFile( File srcFile, File destFile ) throws IOException
567    {
568        copyFile( srcFile, destFile, true );
569    }
570
571
572    /**
573     * Copies a file to a new location.
574     * <p>
575     * This method copies the contents of the specified source file
576     * to the specified destination file.
577     * The directory holding the destination file is created if it does not exist.
578     * If the destination file exists, then this method will overwrite it.
579     * <p>
580     * <strong>Note:</strong> Setting <code>preserveFileDate</code> to
581     * {@code true} tries to preserve the file's last modified
582     * date/times using {@link File#setLastModified(long)}, however it is
583     * not guaranteed that the operation will succeed.
584     * If the modification operation fails, no indication is provided.
585     *
586     * @param srcFile  an existing file to copy, must not be {@code null}
587     * @param destFile  the new file, must not be {@code null}
588     * @param preserveFileDate  true if the file date of the copy
589     *  should be the same as the original
590     *
591     * @throws NullPointerException if source or destination is {@code null}
592     * @throws IOException if source or destination is invalid
593     * @throws IOException if an IO error occurs during copying
594     * @throws IOException if the output file length is not the same as the input file length after the copy completes
595     */
596    public static void copyFile( File srcFile, File destFile, boolean preserveFileDate ) throws IOException
597    {
598        if ( srcFile == null )
599        {
600            throw new NullPointerException( "Source must not be null" );
601        }
602
603        if ( destFile == null )
604        {
605            throw new NullPointerException( "Destination must not be null" );
606        }
607
608        if ( !srcFile.exists() )
609        {
610            throw new FileNotFoundException( "Source '" + srcFile + "' does not exist" );
611        }
612
613        if ( srcFile.isDirectory() )
614        {
615            throw new IOException( "Source '" + srcFile + "' exists but is a directory" );
616        }
617
618        if ( srcFile.getCanonicalPath().equals( destFile.getCanonicalPath() ) )
619        {
620            throw new IOException( "Source '" + srcFile + "' and destination '" + destFile + "' are the same" );
621        }
622
623        File parentFile = destFile.getParentFile();
624
625        if ( parentFile != null )
626        {
627            if ( !parentFile.mkdirs() && !parentFile.isDirectory() )
628            {
629                throw new IOException( "Destination '" + parentFile + "' directory cannot be created" );
630            }
631        }
632
633        if ( destFile.exists() && !destFile.canWrite() )
634        {
635            throw new IOException( "Destination '" + destFile + "' exists but is read-only" );
636        }
637
638        doCopyFile( srcFile, destFile, preserveFileDate );
639    }
640
641
642    /**
643     * Internal copy file method.
644     * This caches the original file length, and throws an IOException 
645     * if the output file length is different from the current input file length.
646     * So it may fail if the file changes size.
647     * It may also fail with "IllegalArgumentException: Negative size" if the input file is truncated part way
648     * through copying the data and the new file size is less than the current position.
649     *
650     * @param srcFile  the validated source file, must not be {@code null}
651     * @param destFile  the validated destination file, must not be {@code null}
652     * @param preserveFileDate  whether to preserve the file date
653     * @throws IOException if an error occurs
654     * @throws IOException if the output file length is not the same as the input file length after the copy completes
655     * @throws IllegalArgumentException "Negative size" if the file is truncated so that the size is less than the position
656     */
657    private static void doCopyFile( File srcFile, File destFile, boolean preserveFileDate ) throws IOException
658    {
659        if ( destFile.exists() && destFile.isDirectory() )
660        {
661            throw new IOException( "Destination '" + destFile + "' exists but is a directory" );
662        }
663
664        FileInputStream fis = null;
665        FileOutputStream fos = null;
666        FileChannel input = null;
667        FileChannel output = null;
668
669        try
670        {
671            fis = ( FileInputStream ) Files.newInputStream( Paths.get( srcFile.getPath() ) );
672            fos = ( FileOutputStream ) Files.newOutputStream( Paths.get( destFile.getPath() ) );
673            input = fis.getChannel();
674            output = fos.getChannel();
675            long size = input.size(); // TODO See IO-386
676            long pos = 0;
677            long count = 0;
678
679            while ( pos < size )
680            {
681                long remain = size - pos;
682                count = remain > FILE_COPY_BUFFER_SIZE ? FILE_COPY_BUFFER_SIZE : remain;
683                long bytesCopied = output.transferFrom( input, pos, count );
684
685                if ( bytesCopied == 0 )
686                { // IO-385 - can happen if file is truncated after caching the size
687                    break; // ensure we don't loop forever
688                }
689
690                pos += bytesCopied;
691            }
692        }
693        finally
694        {
695            IOUtils.closeQuietly( output, fos, input, fis );
696        }
697
698        long srcLen = srcFile.length(); // TODO See IO-386
699        long dstLen = destFile.length(); // TODO See IO-386
700
701        if ( srcLen != dstLen )
702        {
703            throw new IOException( "Failed to copy full contents from '"
704                + srcFile + "' to '" + destFile + "' Expected length: " + srcLen + " Actual: " + dstLen );
705        }
706
707        if ( preserveFileDate )
708        {
709            destFile.setLastModified( srcFile.lastModified() );
710        }
711    }
712
713
714    /**
715     * Writes a byte array to a file creating the file if it does not exist.
716     * <p>
717     * NOTE: As from v1.3, the parent directories of the file will be created
718     * if they do not exist.
719     *
720     * @param file  the file to write to
721     * @param data  the content to write to the file
722     * @throws IOException in case of an I/O erroe
723     * @since 1.1
724     */
725    public static void writeByteArrayToFile( final File file, final byte[] data ) throws IOException
726    {
727        writeByteArrayToFile( file, data, false );
728    }
729
730
731    /**
732     * Writes a byte array to a file creating the file if it does not exist.
733     *
734     * @param file  the file to write to
735     * @param data  the content to write to the file
736     * @param append if {@code true}, then bytes will be added to the
737     * end of the file rather than overwriting
738     * @throws IOException in case of an I/O error
739     * @since 2.1
740     */
741    public static void writeByteArrayToFile( File file, byte[] data, boolean append ) throws IOException
742    {
743        writeByteArrayToFile( file, data, 0, data.length, append );
744    }
745
746
747    /**
748     * Writes {@code len} bytes from the specified byte array starting
749     * at offset {@code off} to a file, creating the file if it does
750     * not exist.
751     *
752     * @param file  the file to write to
753     * @param data  the content to write to the file
754     * @param off   the start offset in the data
755     * @param len   the number of bytes to write
756     * @param append if {@code true}, then bytes will be added to the
757     * end of the file rather than overwriting
758     * @throws IOException in case of an I/O error
759     * @since 2.5
760     */
761    public static void writeByteArrayToFile( File file, byte[] data, int off, int len, boolean append ) throws IOException
762    {
763        OutputStream out = null;
764        
765        try
766        {
767            out = openOutputStream( file, append );
768            out.write( data, off, len );
769            out.close(); // don't swallow close Exception if copy completes normally
770        }
771        finally
772        {
773            IOUtils.closeQuietly( out );
774        }
775    }
776
777    
778    /**
779     * Reads the contents of a file into a byte array.
780     * The file is always closed.
781     *
782     * @param file  the file to read, must not be {@code null}
783     * @return the file contents, never {@code null}
784     * @throws IOException in case of an I/O error
785     * @since 1.1
786     */
787    public static byte[] readFileToByteArray( File file ) throws IOException 
788    {
789        InputStream in = null;
790        
791        try 
792        {
793            in = openInputStream( file );
794            return IOUtils.toByteArray( in, file.length() );
795        } 
796        finally 
797        {
798            IOUtils.closeQuietly( in );
799        }
800    }
801
802    
803    /**
804     * Opens a {@link FileOutputStream} for the specified file, checking and
805     * creating the parent directory if it does not exist.
806     * <p>
807     * At the end of the method either the stream will be successfully opened,
808     * or an exception will have been thrown.
809     * <p>
810     * The parent directory will be created if it does not exist.
811     * The file will be created if it does not exist.
812     * An exception is thrown if the file object exists but is a directory.
813     * An exception is thrown if the file exists but cannot be written to.
814     * An exception is thrown if the parent directory cannot be created.
815     *
816     * @param file  the file to open for output, must not be {@code null}
817     * @return a new {@link FileOutputStream} for the specified file
818     * @throws IOException if the file object is a directory
819     * @throws IOException if the file cannot be written to
820     * @throws IOException if a parent directory needs creating but that fails
821     * @since 1.3
822     */
823    public static OutputStream openOutputStream( File file ) throws IOException 
824    {
825        return openOutputStream( file, false );
826    }
827    
828    
829    /**
830     * Reads the contents of a file line by line to a List of Strings using the default encoding for the VM.
831     * The file is always closed.
832     *
833     * @param file  the file to read, must not be {@code null}
834     * @return the list of Strings representing each line in the file, never {@code null}
835     * @throws IOException in case of an I/O error
836     * @since 1.3
837     * @deprecated 2.5 use {@link #readLines(File, Charset)} instead
838     */
839    @Deprecated
840    public static List<String> readLines( File file ) throws IOException 
841    {
842        return readLines( file, Charset.defaultCharset() );
843    }
844    
845    
846    /**
847     * Reads the contents of a file line by line to a List of Strings.
848     * The file is always closed.
849     *
850     * @param file  the file to read, must not be {@code null}
851     * @param encoding  the encoding to use, {@code null} means platform default
852     * @return the list of Strings representing each line in the file, never {@code null}
853     * @throws IOException in case of an I/O error
854     * @since 2.3
855     */
856    public static List<String> readLines( File file, Charset encoding ) throws IOException 
857    {
858        InputStream in = null;
859        
860        try 
861        {
862            in = openInputStream( file );
863            return IOUtils.readLines( in, IOUtils.toCharset( encoding ) );
864        } 
865        finally 
866        {
867            IOUtils.closeQuietly( in );
868        }
869    }
870}