001/*
002 * ModeShape (http://www.modeshape.org)
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 */
016package org.modeshape.common.util;
017
018import java.io.BufferedInputStream;
019import java.io.BufferedOutputStream;
020import java.io.File;
021import java.io.FileInputStream;
022import java.io.FileOutputStream;
023import java.io.FilenameFilter;
024import java.io.IOException;
025import java.io.InputStream;
026import java.net.MalformedURLException;
027import java.net.URL;
028import java.nio.file.FileVisitResult;
029import java.nio.file.Files;
030import java.nio.file.Path;
031import java.nio.file.Paths;
032import java.nio.file.SimpleFileVisitor;
033import java.nio.file.attribute.BasicFileAttributes;
034import java.util.Objects;
035import java.util.concurrent.atomic.AtomicLong;
036import java.util.zip.ZipEntry;
037import java.util.zip.ZipInputStream;
038import java.util.zip.ZipOutputStream;
039import org.modeshape.common.annotation.Immutable;
040
041/**
042 * A set of utilities for working with files and directories.
043 */
044@Immutable
045public class FileUtil {
046
047    private static FilenameFilter ACCEPT_ALL = new FilenameFilter() {
048
049        @Override
050        public boolean accept( File dir,
051                               String name ) {
052            return true;
053        }
054    };
055
056    /**
057     * Delete the file or directory at the supplied path. This method works on a directory that is not empty, unlike the
058     * {@link File#delete()} method.
059     * 
060     * @param path the path to the file or directory that is to be deleted
061     * @return true if the file or directory at the supplied path existed and was successfully deleted, or false otherwise
062     */
063    public static boolean delete( String path ) {
064        if (path == null || path.trim().length() == 0) return false;
065        return delete(new File(path));
066    }
067
068    /**
069     * Delete the file or directory given by the supplied reference. This method works on a directory that is not empty, unlike
070     * the {@link File#delete()} method.
071     * 
072     * @param fileOrDirectory the reference to the Java File object that is to be deleted
073     * @return true if the supplied file or directory existed and was successfully deleted, or false otherwise
074     */
075    public static boolean delete( File fileOrDirectory ) {
076        if (fileOrDirectory == null) return false;
077        if (!fileOrDirectory.exists()) return false;
078
079        // The file/directory exists, so if a directory delete all of the contents ...
080        if (fileOrDirectory.isDirectory()) {
081            File[] files = fileOrDirectory.listFiles();
082            if (files != null) {
083                for (File childFile : files) {
084                    delete(childFile); // recursive call (good enough for now until we need something better)
085                }
086            }
087            // Now an empty directory ...
088        }
089        // Whether this is a file or empty directory, just delete it ...
090        return fileOrDirectory.delete();
091    }
092
093    /**
094     * Copy the source file system structure into the supplied target location. If the source is a file, the destination will be
095     * created as a file; if the source is a directory, the destination will be created as a directory.
096     * 
097     * @param sourceFileOrDirectory the file or directory whose contents are to be copied into the target location
098     * @param destinationFileOrDirectory the location where the copy is to be placed; does not need to exist, but if it does its
099     *        type must match that of <code>src</code>
100     * @return the number of files (not directories) that were copied
101     * @throws IllegalArgumentException if the <code>src</code> or <code>dest</code> references are null
102     * @throws IOException
103     */
104    public static int copy( File sourceFileOrDirectory,
105                            File destinationFileOrDirectory ) throws IOException {
106        return copy(sourceFileOrDirectory, destinationFileOrDirectory, null);
107    }
108
109    /**
110     * Copy the source file system structure into the supplied target location. If the source is a file, the destination will be
111     * created as a file; if the source is a directory, the destination will be created as a directory.
112     * 
113     * @param sourceFileOrDirectory the file or directory whose contents are to be copied into the target location
114     * @param destinationFileOrDirectory the location where the copy is to be placed; does not need to exist, but if it does its
115     *        type must match that of <code>src</code>
116     * @param exclusionFilter a filter that matches files or folders that should _not_ be copied; null indicates that all files
117     *        and folders should be copied
118     * @return the number of files (not directories) that were copied
119     * @throws IllegalArgumentException if the <code>src</code> or <code>dest</code> references are null
120     * @throws IOException
121     */
122    public static int copy( File sourceFileOrDirectory,
123                            File destinationFileOrDirectory,
124                            FilenameFilter exclusionFilter ) throws IOException {
125        if (exclusionFilter == null) exclusionFilter = ACCEPT_ALL;
126        int numberOfFilesCopied = 0;
127        if (sourceFileOrDirectory.isDirectory()) {
128            destinationFileOrDirectory.mkdirs();
129            String list[] = sourceFileOrDirectory.list(exclusionFilter);
130
131            for (int i = 0; i < list.length; i++) {
132                String dest1 = destinationFileOrDirectory.getPath() + File.separator + list[i];
133                String src1 = sourceFileOrDirectory.getPath() + File.separator + list[i];
134
135                numberOfFilesCopied += copy(new File(src1), new File(dest1), exclusionFilter);
136            }
137        } else {
138            try (FileInputStream fis = new FileInputStream(sourceFileOrDirectory);
139                 BufferedInputStream bis = new BufferedInputStream(fis);
140                 FileOutputStream fos = new FileOutputStream(destinationFileOrDirectory);
141                 BufferedOutputStream bos = new BufferedOutputStream(fos)) {
142                int c;
143                while ((c = bis.read()) >= 0) {
144                    bos.write(c);
145                }
146            }
147            numberOfFilesCopied++;
148        }
149        return numberOfFilesCopied;
150    }
151
152    /**
153     * Utility to convert {@link File} to {@link URL}.
154     * 
155     * @param filePath the path of the file
156     * @return the {@link URL} representation of the file.
157     * @throws MalformedURLException
158     * @throws IllegalArgumentException if the file path is null, empty or blank
159     */
160    public static URL convertFileToURL( String filePath ) throws MalformedURLException {
161        CheckArg.isNotEmpty(filePath, "filePath");
162        File file = new File(filePath.trim());
163        return file.toURI().toURL();
164    }
165
166    /**
167     * Determines the size (in bytes) of the file or directory at the given path.
168     *
169     * @param filePath the path of the file; may not be {@code null}
170     * @return the size in bytes of the file or the total computed size of the folder. If the given path is not a valid file or
171     * folder, this will return 0.
172     * @throws IOException if anything unexpected fails.
173     */
174    public static long size(String filePath) throws IOException {
175        CheckArg.isNotEmpty(filePath, "filePath");
176        File file = new File(filePath);
177        if (!file.exists()) {
178            return 0;
179        }
180        if (file.isFile()) {
181            return file.length();
182        }
183        final AtomicLong size = new AtomicLong();
184        Files.walkFileTree(Paths.get(filePath), new SimpleFileVisitor<Path>() {
185            @Override
186            public FileVisitResult visitFile( Path file, BasicFileAttributes attrs ) {
187                size.addAndGet(attrs.size());
188                return FileVisitResult.CONTINUE;
189            }
190
191            @Override
192            public FileVisitResult visitFileFailed( Path file, IOException exc ) {
193                return FileVisitResult.CONTINUE;
194            }
195        });
196        return size.get();
197    }
198
199    /**
200     * Unzip archive to the specified destination.
201     * 
202     * @param zipFile zip archive
203     * @param dest directory where archive will be uncompressed
204     * @throws IOException 
205     */
206    public static void unzip(InputStream zipFile, String dest) throws IOException {
207        byte[] buffer = new byte[1024];
208
209        //create output directory is not exists
210        File folder = new File(dest);
211
212        if (folder.exists()) {
213            FileUtil.delete(folder);
214        }
215
216        folder.mkdir();
217
218        //get the zip file content
219        try (ZipInputStream zis = new ZipInputStream(zipFile)) {
220            //get the zipped file list entry
221            ZipEntry ze = zis.getNextEntry();
222            File parent = new File(dest);
223            while (ze != null) {
224                String fileName = ze.getName();
225                if (ze.isDirectory()) {
226                    File newFolder = new File(parent, fileName);
227                    newFolder.mkdir();
228                } else {
229                    File newFile = new File(parent, fileName);
230                    try (FileOutputStream fos = new FileOutputStream(newFile)) {
231                        int len;
232                        while ((len = zis.read(buffer)) > 0) {
233                            fos.write(buffer, 0, len);
234                        }
235                    }
236                }
237                ze = zis.getNextEntry();
238            }
239
240            zis.closeEntry();
241        }
242    }
243    
244    /**
245     * Compresses directory into zip archive.
246     * 
247     * @param dirName the path to the directory
248     * @param nameZipFile archive name.
249     * @throws IOException 
250     */
251    public static void zipDir(String dirName, String nameZipFile) throws IOException {
252        try (FileOutputStream fW = new FileOutputStream(nameZipFile);
253             ZipOutputStream zip = new ZipOutputStream(fW)) {
254            addFolderToZip("", dirName, zip);
255        }
256    }
257
258    /**
259     * Adds folder to the archive.
260     * 
261     * @param path path to the folder
262     * @param srcFolder folder name
263     * @param zip zip archive
264     * @throws IOException 
265     */
266    public static void addFolderToZip(String path, String srcFolder, ZipOutputStream zip) throws IOException {
267        File folder = new File(srcFolder);
268        if (folder.list().length == 0) {
269            addFileToZip(path, srcFolder, zip, true);
270        } else {
271            for (String fileName : folder.list()) {
272                if (path.equals("")) {
273                    addFileToZip(folder.getName(), srcFolder + "/" + fileName, zip, false);
274                } else {
275                    addFileToZip(path + "/" + folder.getName(), srcFolder + "/" + fileName, zip, false);
276                }
277            }
278        }
279    }
280
281    /**
282     * Appends file to the archive.
283     * 
284     * @param path path to the file
285     * @param srcFile file name
286     * @param zip archive
287     * @param flag
288     * @throws IOException 
289     */
290    public static void addFileToZip(String path, String srcFile, ZipOutputStream zip, boolean flag) throws IOException {
291        File folder = new File(srcFile);
292        if (flag) {
293            zip.putNextEntry(new ZipEntry(path + "/" + folder.getName() + "/"));
294        } else {
295            if (folder.isDirectory()) {
296                addFolderToZip(path, srcFile, zip);
297            } else {
298                byte[] buf = new byte[1024];
299                int len;
300                try (FileInputStream in = new FileInputStream(srcFile)) {
301                    zip.putNextEntry(new ZipEntry(path + "/" + folder.getName()));
302                    while ((len = in.read(buf)) > 0) {
303                        zip.write(buf, 0, len);
304                    }
305                }
306            }
307        }
308    }
309
310    /**
311     * Returns the extension of a file, including the dot.
312     * 
313     * @param filename the name of a file, may be not be null
314     * @return the file extension or an empty string if the extension cannot be determined.
315     */
316    public static String getExtension(final String filename) {
317        Objects.requireNonNull(filename, "filename cannot be null");
318        int lastDotIdx = filename.lastIndexOf(".");
319        return lastDotIdx >= 0 ? filename.substring(lastDotIdx) : "";
320    }
321    
322    private FileUtil() {
323    }
324
325}