001/*
002 * (C) Copyright 2006-2015 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Nuxeo - initial API and implementation
018 *
019 */
020
021package org.nuxeo.common.utils;
022
023import java.io.BufferedOutputStream;
024import java.io.BufferedReader;
025import java.io.Closeable;
026import java.io.File;
027import java.io.FileInputStream;
028import java.io.FileOutputStream;
029import java.io.IOException;
030import java.io.InputStream;
031import java.io.InputStreamReader;
032import java.io.OutputStream;
033import java.io.PrintWriter;
034import java.io.UnsupportedEncodingException;
035import java.net.MalformedURLException;
036import java.net.URISyntaxException;
037import java.net.URL;
038import java.net.URLDecoder;
039import java.util.ArrayList;
040import java.util.List;
041
042import org.apache.commons.io.Charsets;
043import org.apache.commons.io.IOUtils;
044import org.apache.commons.logging.Log;
045import org.apache.commons.logging.LogFactory;
046
047/**
048 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
049 */
050public final class FileUtils {
051
052    private static final int BUFFER_SIZE = 1024 * 64; // 64K
053
054    private static final int MAX_BUFFER_SIZE = 1024 * 1024; // 64K
055
056    private static final int MIN_BUFFER_SIZE = 1024 * 8; // 64K
057
058    private static final Log log = LogFactory.getLog(FileUtils.class);
059
060    // This is an utility class
061    private FileUtils() {
062    }
063
064    public static void safeClose(Closeable stream) {
065        try {
066            stream.close();
067        } catch (IOException e) {
068            // do nothing
069        }
070    }
071
072    private static byte[] createBuffer(int preferredSize) {
073        if (preferredSize < 1) {
074            preferredSize = BUFFER_SIZE;
075        }
076        if (preferredSize > MAX_BUFFER_SIZE) {
077            preferredSize = MAX_BUFFER_SIZE;
078        } else if (preferredSize < MIN_BUFFER_SIZE) {
079            preferredSize = MIN_BUFFER_SIZE;
080        }
081        return new byte[preferredSize];
082    }
083
084    public static void copy(InputStream in, OutputStream out) throws IOException {
085        byte[] buffer = createBuffer(in.available());
086        int read;
087        while ((read = in.read(buffer)) != -1) {
088            out.write(buffer, 0, read);
089        }
090    }
091
092    /**
093     * Read the byte stream as a string assuming a UTF-8 encoding.
094     *
095     * @deprecated Since 5.7. Use {@link IOUtils#toString(InputStream, java.nio.charset.Charset)} explicitly instead (or
096     *             any other encoding when provided by the source of the byte stream).
097     */
098    @Deprecated
099    public static String read(InputStream in) throws IOException {
100        // UTF-8 should be configured as the default "file.encoding" in a system property configured in the nuxeo.conf
101        // file. However this option might not be passed when running the Nuxeo as a library or using the Maven test
102        // runner. Therefore we hardcode the default charset to "UTF-8" to ensure consistency.
103        return IOUtils.toString(in, Charsets.UTF_8);
104    }
105
106    public static byte[] readBytes(URL url) throws IOException {
107        return readBytes(url.openStream());
108    }
109
110    public static byte[] readBytes(InputStream in) throws IOException {
111        byte[] buffer = createBuffer(in.available());
112        int w = 0;
113        try {
114            int read = 0;
115            int len;
116            do {
117                w += read;
118                len = buffer.length - w;
119                if (len <= 0) { // resize buffer
120                    byte[] b = new byte[buffer.length + BUFFER_SIZE];
121                    System.arraycopy(buffer, 0, b, 0, w);
122                    buffer = b;
123                    len = buffer.length - w;
124                }
125            } while ((read = in.read(buffer, w, len)) != -1);
126        } finally {
127            in.close();
128        }
129        if (buffer.length > w) { // compact buffer
130            byte[] b = new byte[w];
131            System.arraycopy(buffer, 0, b, 0, w);
132            buffer = b;
133        }
134        return buffer;
135    }
136
137    public static String readFile(File file) throws IOException {
138        FileInputStream in = null;
139        try {
140            in = new FileInputStream(file);
141            return read(in);
142        } finally {
143            if (in != null) {
144                in.close();
145            }
146        }
147    }
148
149    public static List<String> readLines(File file) throws IOException {
150        List<String> lines = new ArrayList<>();
151        BufferedReader reader = null;
152        try {
153            InputStream in = new FileInputStream(file);
154            reader = new BufferedReader(new InputStreamReader(in));
155            String line;
156            while ((line = reader.readLine()) != null) {
157                lines.add(line);
158            }
159        } finally {
160            if (reader != null) {
161                try {
162                    reader.close();
163                } catch (IOException e) {
164                }
165            }
166        }
167        return lines;
168    }
169
170    public static void writeLines(File file, List<String> lines) throws IOException {
171        PrintWriter out = null;
172        try {
173            out = new PrintWriter(new FileOutputStream(file));
174            for (String line : lines) {
175                out.println(line);
176            }
177        } finally {
178            if (out != null) {
179                out.close();
180            }
181        }
182    }
183
184    public static byte[] readBytes(File file) throws IOException {
185        FileInputStream in = null;
186        try {
187            in = new FileInputStream(file);
188            return readBytes(in);
189        } finally {
190            if (in != null) {
191                in.close();
192            }
193        }
194    }
195
196    public static void writeFile(File file, byte[] buf) throws IOException {
197        writeFile(file, buf, false);
198    }
199
200    /**
201     * @param file
202     * @param buf
203     * @param append
204     * @throws IOException
205     * @since 5.5
206     */
207    public static void writeFile(File file, byte[] buf, boolean append) throws IOException {
208        FileOutputStream fos = null;
209        try {
210            fos = new FileOutputStream(file, append);
211            fos.write(buf);
212        } finally {
213            if (fos != null) {
214                fos.close();
215            }
216        }
217    }
218
219    public static void writeFile(File file, String buf) throws IOException {
220        writeFile(file, buf.getBytes(), false);
221    }
222
223    /**
224     * @param file
225     * @param buf
226     * @param append
227     * @throws IOException
228     * @since 5.5
229     */
230    public static void writeFile(File file, String buf, boolean append) throws IOException {
231        writeFile(file, buf.getBytes(), append);
232    }
233
234    public static void download(URL url, File file) throws IOException {
235        InputStream in = url.openStream();
236        OutputStream out = new FileOutputStream(file);
237        try {
238            copy(in, out);
239        } finally {
240            if (in != null) {
241                in.close();
242            }
243            out.close();
244        }
245    }
246
247    /**
248     * @deprecated Since 5.6. Use {@link org.apache.commons.io.FileUtils#deleteDirectory(File)} or
249     *             {@link org.apache.commons.io.FileUtils#deleteQuietly(File)} instead.
250     */
251    @Deprecated
252    public static void deleteTree(File dir) {
253        emptyDirectory(dir);
254        dir.delete();
255    }
256
257    /**
258     * @deprecated Since 5.6. Use {@link org.apache.commons.io.FileUtils#deleteDirectory(File)} or
259     *             {@link org.apache.commons.io.FileUtils#deleteQuietly(File)} instead. Warning: suggested methods will
260     *             delete the root directory whereas current method doesn't.
261     */
262    @Deprecated
263    public static void emptyDirectory(File dir) {
264        File[] files = dir.listFiles();
265        if (files == null) {
266            return;
267        }
268        int len = files.length;
269        for (int i = 0; i < len; i++) {
270            File file = files[i];
271            if (file.isDirectory()) {
272                deleteTree(file);
273            } else {
274                file.delete();
275            }
276        }
277    }
278
279    public static void copyToFile(InputStream in, File file) throws IOException {
280        OutputStream out = null;
281        try {
282            out = new FileOutputStream(file);
283            byte[] buffer = createBuffer(in.available());
284            int read;
285            while ((read = in.read(buffer)) != -1) {
286                out.write(buffer, 0, read);
287            }
288        } finally {
289            if (out != null) {
290                out.close();
291            }
292        }
293    }
294
295    public static void append(File src, File dst) throws IOException {
296        append(src, dst, false);
297    }
298
299    public static void append(File src, File dst, boolean appendNewLine) throws IOException {
300        InputStream in = null;
301        try {
302            in = new FileInputStream(src);
303            append(in, dst, appendNewLine);
304        } finally {
305            if (in != null) {
306                in.close();
307            }
308        }
309    }
310
311    public static void append(InputStream in, File file) throws IOException {
312        append(in, file, false);
313    }
314
315    public static void append(InputStream in, File file, boolean appendNewLine) throws IOException {
316        OutputStream out = null;
317        try {
318            out = new BufferedOutputStream(new FileOutputStream(file, true));
319            if (appendNewLine) {
320                out.write(System.getProperty("line.separator").getBytes());
321            }
322            byte[] buffer = new byte[BUFFER_SIZE];
323            int read;
324            while ((read = in.read(buffer)) != -1) {
325                out.write(buffer, 0, read);
326            }
327        } finally {
328            if (out != null) {
329                out.close();
330            }
331        }
332    }
333
334    /**
335     * Copies source to destination. If source and destination are the same, does nothing. Both single files and
336     * directories are handled.
337     *
338     * @param src the source file or directory
339     * @param dst the destination file or directory
340     * @throws IOException
341     */
342    public static void copy(File src, File dst) throws IOException {
343        if (src.equals(dst)) {
344            return;
345        }
346        if (src.isFile()) {
347            copyFile(src, dst);
348        } else {
349            copyTree(src, dst);
350        }
351    }
352
353    public static void copy(File[] src, File dst) throws IOException {
354        for (File file : src) {
355            copy(file, dst);
356        }
357    }
358
359    public static void copyFile(File src, File dst) throws IOException {
360        if (dst.isDirectory()) {
361            dst = new File(dst, src.getName());
362        }
363        FileInputStream in = null;
364        FileOutputStream out = null;
365        try {
366            out = new FileOutputStream(dst);
367            in = new FileInputStream(src);
368            copy(in, out);
369        } finally {
370            IOUtils.closeQuietly(in);
371            IOUtils.closeQuietly(out);
372        }
373    }
374
375    /**
376     * Copies recursively source to destination.
377     * <p>
378     * The source file is assumed to be a directory.
379     *
380     * @param src the source directory
381     * @param dst the destination directory
382     * @throws IOException
383     */
384    public static void copyTree(File src, File dst) throws IOException {
385        if (src.isFile()) {
386            copyFile(src, dst);
387        } else if (src.isDirectory()) {
388            if (dst.exists()) {
389                dst = new File(dst, src.getName());
390                dst.mkdir();
391            } else { // allows renaming dest dir
392                dst.mkdirs();
393            }
394            File[] files = src.listFiles();
395            for (File file : files) {
396                copyTree(file, dst);
397            }
398        }
399    }
400
401    public static void copyTree(File src, File dst, PathFilter filter) throws IOException {
402        copyTree(src, dst, new Path("/"), filter);
403    }
404
405    public static void copyTree(File src, File dst, Path prefix, PathFilter filter) throws IOException {
406        if (!prefix.isAbsolute()) {
407            prefix = prefix.makeAbsolute();
408        }
409        int rootIndex = src.getPath().length() + 1;
410        for (File file : src.listFiles()) {
411            copyTree(rootIndex, file, new File(dst, file.getName()), prefix, filter);
412        }
413    }
414
415    protected static void copyTree(int rootIndex, File src, File dst, Path prefix, PathFilter filter)
416            throws IOException {
417        if (src.isFile()) {
418            String relPath = src.getPath().substring(rootIndex);
419            if (!filter.accept(new Path(relPath))) {
420                return;
421            }
422            if (!prefix.isRoot()) { // remove prefix from path
423                String path = dst.getPath();
424                String pff = prefix.toString();
425                int prefixIndex = path.lastIndexOf(pff);
426                if (prefixIndex > 0) {
427                    path = path.substring(0, prefixIndex) + path.substring(prefixIndex + pff.length());
428                    dst = new File(path.toString());
429                }
430            }
431            dst.getParentFile().mkdirs();
432            copyFile(src, dst);
433        } else if (src.isDirectory()) {
434            File[] files = src.listFiles();
435            for (File file : files) {
436                copyTree(rootIndex, file, new File(dst, file.getName()), prefix, filter);
437            }
438        }
439    }
440
441    /**
442     * Decodes an URL path so that is can be processed as a filename later.
443     *
444     * @param url the Url to be processed.
445     * @return the decoded path.
446     */
447    public static String getFilePathFromUrl(URL url) {
448        String path = "";
449        if (url.getProtocol().equals("file")) {
450            try {
451                path = URLDecoder.decode(url.getPath(), "UTF-8");
452            } catch (UnsupportedEncodingException e) {
453                log.error(e);
454            }
455        }
456        return path;
457    }
458
459    public static File getFileFromURL(URL url) {
460        File file;
461        String filename = getFilePathFromUrl(url);
462        if (filename.equals("")) {
463            file = null;
464        } else {
465            file = new File(filename);
466        }
467        return file;
468    }
469
470    public static String getParentPath(String path) {
471        int p = path.lastIndexOf(File.separator);
472        if (p == -1) {
473            return null;
474        }
475        return path.substring(0, p);
476    }
477
478    public static String getFileName(String path) {
479        int p = path.lastIndexOf(File.separator);
480        if (p == -1) {
481            return path;
482        }
483        return path.substring(p + 1);
484    }
485
486    public static String getFileExtension(String path) {
487        int p = path.lastIndexOf('.');
488        if (p == -1) {
489            return null;
490        }
491        return path.substring(p + 1);
492    }
493
494    public static String getFileNameNoExt(String path) {
495        String name = getFileName(path);
496        int p = name.lastIndexOf('.');
497        if (p == -1) {
498            return name;
499        }
500        return name.substring(0, p);
501    }
502
503    /**
504     * Retrieves the total path of a resource from the Thread Context.
505     *
506     * @param resource the resource name to be retrieved.
507     * @return the decoded path.
508     */
509    public static String getResourcePathFromContext(String resource) {
510        URL url = Thread.currentThread().getContextClassLoader().getResource(resource);
511        return getFilePathFromUrl(url);
512    }
513
514    public static File getResourceFileFromContext(String resource) {
515        File file;
516        String filename = getResourcePathFromContext(resource);
517        if (filename.equals("")) {
518            file = null;
519        } else {
520            file = new File(filename);
521        }
522        return file;
523    }
524
525    public static File[] findFiles(File root, String pattern, boolean recurse) {
526        List<File> result = new ArrayList<>();
527        if (pattern == null) {
528            if (recurse) {
529                collectFiles(root, result);
530            } else {
531                return root.listFiles();
532            }
533        } else {
534            FileNamePattern pat = new FileNamePattern(pattern);
535            if (recurse) {
536                collectFiles(root, pat, result);
537            } else {
538                File[] files = root.listFiles();
539                for (File file : files) {
540                    if (pat.match(file.getName())) {
541                        result.add(file);
542                    }
543                }
544            }
545        }
546        return result.toArray(new File[result.size()]);
547    }
548
549    public static void collectFiles(File root, FileNamePattern pattern, List<File> result) {
550        File[] files = root.listFiles();
551        for (File file : files) {
552            if (pattern.match(file.getName())) {
553                result.add(file);
554                if (file.isDirectory()) {
555                    collectFiles(file, pattern, result);
556                }
557            }
558        }
559    }
560
561    public static void collectFiles(File root, List<File> result) {
562        File[] files = root.listFiles();
563        for (File file : files) {
564            result.add(file);
565            if (file.isDirectory()) {
566                collectFiles(file, result);
567            }
568        }
569    }
570
571    public static void close(InputStream in) {
572        if (in != null) {
573            try {
574                in.close();
575            } catch (IOException e) {
576                log.error(e);
577            }
578        }
579    }
580
581    public static void close(OutputStream out) {
582        if (out != null) {
583            try {
584                out.close();
585            } catch (IOException e) {
586                log.error(e);
587            }
588        }
589    }
590
591    /**
592     * Create a file handler (this doesn't create a real file) given a file URI. This method can be used to create files
593     * from invalid URL strings (e.g. containing spaces ..)
594     *
595     * @return a file object
596     */
597    public static File urlToFile(String url) throws MalformedURLException {
598        return urlToFile(new URL(url));
599    }
600
601    public static File urlToFile(URL url) {
602        try {
603            return new File(url.toURI());
604        } catch (URISyntaxException e) {
605            return new File(url.getPath());
606        }
607    }
608
609    public static List<String> readLines(InputStream in) throws IOException {
610        List<String> lines = new ArrayList<>();
611        BufferedReader reader = null;
612        try {
613            reader = new BufferedReader(new InputStreamReader(in));
614            String line;
615            while ((line = reader.readLine()) != null) {
616                lines.add(line);
617            }
618        } finally {
619            if (reader != null) {
620                try {
621                    reader.close();
622                } catch (IOException e) {
623                }
624            }
625        }
626        return lines;
627    }
628
629    /**
630     * Compares two files content as String even if their EOL are different
631     *
632     * @param expected a file content with Windows or Unix like EOL
633     * @param source another file content with Windows or Unix like EOL
634     * @return the result of equals after replacing their EOL
635     */
636    public static boolean areFilesContentEquals(String expected, String source) {
637        if (expected == source) {
638            return true;
639        }
640
641        if (expected == null || source == null) {
642            return false;
643        }
644
645        if (expected.length() != source.length()) {
646            // Prevent from comparing files with Windows EOL
647            return expected.replace("\r\n", "\n").equals(source.replace("\r\n", "\n"));
648        } else {
649            return expected.equals(source);
650        }
651    }
652
653}