001/*
002 *  Copyright 2020 Anyware Services
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.ametys.plugins.contentio.archive;
017
018import java.io.FilterInputStream;
019import java.io.IOException;
020import java.io.InputStream;
021import java.nio.file.DirectoryStream;
022import java.nio.file.FileSystem;
023import java.nio.file.FileSystems;
024import java.nio.file.Files;
025import java.nio.file.Path;
026import java.nio.file.attribute.BasicFileAttributes;
027import java.util.Optional;
028import java.util.function.BiPredicate;
029import java.util.stream.Stream;
030import java.util.stream.StreamSupport;
031import java.util.zip.ZipEntry;
032import java.util.zip.ZipFile;
033
034import org.apache.commons.lang.StringUtils;
035
036/**
037 * Utility class to retrieve {@link Path} representing folders or files under a ZIP archive.
038 */
039public final class ZipEntryHelper
040{
041    private ZipEntryHelper()
042    { /*empty*/ }
043    
044    private static FileSystem _zipFileSystem(Path zipArchivePath) throws IOException
045    {
046        return FileSystems.newFileSystem(zipArchivePath, ClassLoader.getSystemClassLoader());
047    }
048    
049    /**
050     * Return the {@link Path} of the root of the given ZIP archive.
051     * @param zipArchivePath The zip path
052     * @return the PAth of the root
053     * @throws IOException if an I/O error is thrown when accessing a file.
054     */
055    public static Path zipFileRoot(Path zipArchivePath) throws IOException
056    {
057        FileSystem zipFileSystem = _zipFileSystem(zipArchivePath);
058        Path[] roots = StreamSupport.stream(zipFileSystem.getRootDirectories().spliterator(), false)
059                .toArray(Path[]::new);
060        if (roots.length != 1)
061        {
062            throw new IllegalStateException(String.format("Unexpected error, the zip file '%s' should have 1 and only 1 root directory", zipArchivePath));
063        }
064        
065        Path zipFileRoot = roots[0];
066        return zipFileRoot;
067    }
068    
069    /**
070     * Returns a Stream that is lazily populated with Path by searching for files in a file tree rooted at a given starting path within the ZIP, or the ZIP root. 
071     * @param zipArchivePath The zip path
072     * @param startingPath The (optional) starting path within the ZIP
073     * @param filter The filter
074     * @return the {@link Stream} of {@link Path}
075     * @throws IOException if an I/O error is thrown when accessing a file.
076     */
077    public static Stream<Path> zipFileTree(Path zipArchivePath, Optional<String> startingPath, BiPredicate<Path, BasicFileAttributes> filter) throws IOException
078    {
079        return _zipFileTree(zipArchivePath, startingPath, Integer.MAX_VALUE, filter);
080    }
081    
082    private static Stream<Path> _zipFileTree(Path zipArchivePath, Optional<String> startingPath, int maxDepth, BiPredicate<Path, BasicFileAttributes> filter) throws IOException
083    {
084        Path zipFileRoot = zipFileRoot(zipArchivePath);
085        Path start = startingPath
086                .map(zipFileRoot::resolve)
087                .orElse(zipFileRoot);
088        return Files.find(start, maxDepth, filter);
089    }
090    
091    /**
092     * Opens a directory, returning a {@link DirectoryStream} to iterate over the entries in the directory.
093     * @param zipArchivePath The zip path
094     * @param startingPath The (optional) starting path within the ZIP
095     * @param filter The filter
096     * @return a new and opened {@link DirectoryStream} object
097     * @throws IOException if an I/O error is thrown when accessing a file.
098     */
099    public static DirectoryStream<Path> children(Path zipArchivePath, Optional<String> startingPath, DirectoryStream.Filter<? super Path> filter) throws IOException
100    {
101        Path zipFileRoot = zipFileRoot(zipArchivePath);
102        Path start = startingPath
103                .map(zipFileRoot::resolve)
104                .orElse(zipFileRoot);
105        return Files.newDirectoryStream(start, filter);
106    }
107    
108    /**
109     * Checks if the given folder entry exists in the ZIP archive.
110     * @param zipPath The zip path
111     * @param zipEntryFolderPath The path of the folder within the ZIP
112     * @return <code>true</code> if the entry exists
113     * @throws IOException if an I/O error is thrown when accessing a file.
114     */
115    public static boolean zipEntryFolderExists(Path zipPath, String zipEntryFolderPath) throws IOException
116    {
117        Path zipEntryPath = _zipFileSystem(zipPath)
118                .getPath(zipEntryFolderPath);
119        return Files.exists(zipEntryPath) && Files.isDirectory(zipEntryPath);
120    }
121    
122    /**
123     * Checks if the given file entry exists in the ZIP archive.
124     * @param zipPath The zip path
125     * @param zipEntryFilePath The path of the file within the ZIP
126     * @return <code>true</code> if the entry exists
127     * @throws IOException if an I/O error is thrown when accessing a file.
128     */
129    public static boolean zipEntryFileExists(Path zipPath, String zipEntryFilePath) throws IOException
130    {
131        Path zipEntryPath = _zipFileSystem(zipPath)
132                .getPath(zipEntryFilePath);
133        return Files.exists(zipEntryPath) && Files.isRegularFile(zipEntryPath);
134    }
135    
136    /**
137     * Returns an input stream for reading the content of an entry file of a ZIP archive.
138     * <br>You may check if the entry exist with {@link #zipEntryFileExists}, otherwise you may get an {@link IOException}.
139     * <br>It must be closed.
140     * @param zipPath The zip path
141     * @param zipEntryFilePath The path within the ZIP
142     * @return a new input stream
143     * @throws IOException if an I/O error is thrown when accessing a file.
144     */
145    public static InputStream zipEntryFileInputStream(Path zipPath, String zipEntryFilePath) throws IOException
146    {
147        var zipFile = new ZipFile(zipPath.toFile());
148        String name = StringUtils.stripStart(zipEntryFilePath, "/");
149        ZipEntry zipEntry = zipFile.getEntry(name);
150        if (zipEntry == null)
151        {
152            throw new IOException(String.format("Wrong zip entry file path '%s', it cannot be found in the archive '%s'", zipEntryFilePath, zipPath));
153        }
154        
155        InputStream manifestIs = zipFile.getInputStream(zipEntry);
156        return new FilterInputStream(manifestIs)
157        {
158            // according to java.util.zip.ZipFile.getInputStream(ZipEntry),
159            // "Closing this ZIP file will, in turn, close all input streams that have been returned by invocations of this method"
160            // thus we need to override the close method of the returned InputStream in order to close the zip file
161            @Override
162            public void close() throws IOException
163            {
164                super.close();
165                zipFile.close();
166            }
167        };
168    }
169}
170