001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.vfs2.provider.zip;
018
019import java.io.File;
020import java.io.IOException;
021import java.nio.charset.Charset;
022import java.nio.charset.StandardCharsets;
023import java.util.Collection;
024import java.util.Enumeration;
025import java.util.HashMap;
026import java.util.Map;
027import java.util.zip.ZipEntry;
028import java.util.zip.ZipFile;
029
030import org.apache.commons.io.IOUtils;
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033import org.apache.commons.vfs2.Capability;
034import org.apache.commons.vfs2.FileName;
035import org.apache.commons.vfs2.FileObject;
036import org.apache.commons.vfs2.FileSystemException;
037import org.apache.commons.vfs2.FileSystemOptions;
038import org.apache.commons.vfs2.Selectors;
039import org.apache.commons.vfs2.VfsLog;
040import org.apache.commons.vfs2.provider.AbstractFileName;
041import org.apache.commons.vfs2.provider.AbstractFileSystem;
042import org.apache.commons.vfs2.provider.UriParser;
043
044/**
045 * A read-only file system for ZIP and JAR files.
046 */
047public class ZipFileSystem extends AbstractFileSystem {
048
049    private static final char[] ENC = {'!'};
050
051    private static final Log LOG = LogFactory.getLog(ZipFileSystem.class);
052
053    private final File file;
054    private final Charset charset;
055    private ZipFile zipFile;
056
057    /**
058     * Cache doesn't need to be synchronized since it is read-only.
059     */
060    private final Map<FileName, FileObject> cache = new HashMap<>();
061
062    /**
063     * Constructs a new instance.
064     *
065     * @param rootFileName The root file name of this file system.
066     * @param parentLayer The parent layer of this file system.
067     * @param fileSystemOptions Options to build this file system.
068     * @throws FileSystemException If the parent layer does not exist, or on error replicating the file.
069     */
070    public ZipFileSystem(final AbstractFileName rootFileName, final FileObject parentLayer, final FileSystemOptions fileSystemOptions)
071        throws FileSystemException {
072        super(rootFileName, parentLayer, fileSystemOptions);
073
074        // Make a local copy of the file
075        file = parentLayer.getFileSystem().replicateFile(parentLayer, Selectors.SELECT_SELF);
076        charset = ZipFileSystemConfigBuilder.getInstance().getCharset(fileSystemOptions);
077
078        // Open the Zip file
079        if (!file.exists()) {
080            // Don't need to do anything
081            zipFile = null;
082        }
083    }
084
085    /**
086     * Returns the capabilities of this file system.
087     */
088    @Override
089    protected void addCapabilities(final Collection<Capability> caps) {
090        caps.addAll(ZipFileProvider.capabilities);
091    }
092
093    /**
094     * Creates a file object.
095     */
096    @Override
097    protected FileObject createFile(final AbstractFileName name) throws FileSystemException {
098        // This is only called for files which do not exist in the Zip file
099        return new ZipFileObject(name, null, this, false);
100    }
101
102    /**
103     * Creates a Zip file.
104     *
105     * @param file the underlying file.
106     * @return a Zip file.
107     * @throws FileSystemException if a file system error occurs.
108     */
109    protected ZipFile createZipFile(final File file) throws FileSystemException {
110        try {
111            return new ZipFile(file, charset);
112        } catch (final IOException ioe) {
113            throw new FileSystemException("vfs.provider.zip/open-zip-file.error", file, ioe);
114        }
115    }
116
117    /**
118     * Creates a new Zip file object.
119     *
120     * @param fileName the underlying file.
121     * @param entry the Zip entry.
122     * @return a new ZipFileObject.
123     * @throws FileSystemException if a file system error occurs.
124     */
125    protected ZipFileObject createZipFileObject(final AbstractFileName fileName, final ZipEntry entry) throws FileSystemException {
126        return new ZipFileObject(fileName, entry, this, true);
127    }
128
129    @Override
130    protected void doCloseCommunicationLink() {
131        // Release the zip file
132        try {
133            IOUtils.close(zipFile);
134            zipFile = null;
135        } catch (final IOException e) {
136            // getLogger().warn("vfs.provider.zip/close-zip-file.error :" + file, e);
137            VfsLog.warn(getLogger(), LOG, "vfs.provider.zip/close-zip-file.error :" + file, e);
138        }
139    }
140
141    /**
142     * Gets the Charset, defaults to {@link StandardCharsets#UTF_8}, the value used in {@link ZipFile}.
143     *
144     * @return the Charset.
145     */
146    protected Charset getCharset() {
147        return charset;
148    }
149
150    /**
151     * Gets a cached file.
152     */
153    @Override
154    protected FileObject getFileFromCache(final FileName name) {
155        return cache.get(name);
156    }
157
158    /**
159     * Gets the zip file.
160     *
161     * @return the zip file.
162     * @throws FileSystemException if a file system error occurs.
163     */
164    protected ZipFile getZipFile() throws FileSystemException {
165        if (zipFile == null && file.exists()) {
166            zipFile = createZipFile(file);
167        }
168        return zipFile;
169    }
170
171    @Override
172    public void init() throws FileSystemException {
173        super.init();
174
175        try {
176            // Build the index
177            final Enumeration<? extends ZipEntry> entries = getZipFile().entries();
178            while (entries.hasMoreElements()) {
179                final ZipEntry entry = entries.nextElement();
180                final AbstractFileName name = (AbstractFileName) getFileSystemManager().resolveName(getRootName(),
181                        UriParser.encode(entry.getName(), ENC));
182
183                // Create the file
184                ZipFileObject fileObj;
185                if (entry.isDirectory() && getFileFromCache(name) != null) {
186                    fileObj = (ZipFileObject) getFileFromCache(name);
187                    fileObj.setZipEntry(entry);
188                    continue;
189                }
190
191                fileObj = createZipFileObject(name, entry);
192                putFileToCache(fileObj);
193
194                // Make sure all ancestors exist
195                // TODO - create these on demand
196                ZipFileObject parent;
197                for (AbstractFileName parentName = (AbstractFileName) name
198                        .getParent(); parentName != null; fileObj = parent, parentName = (AbstractFileName) parentName
199                                .getParent()) {
200                    // Locate the parent
201                    parent = (ZipFileObject) getFileFromCache(parentName);
202                    if (parent == null) {
203                        parent = createZipFileObject(parentName, null);
204                        putFileToCache(parent);
205                    }
206
207                    // Attach child to parent
208                    parent.attachChild(fileObj.getName());
209                }
210            }
211        } finally {
212            closeCommunicationLink();
213        }
214    }
215
216    /**
217     * Adds a file object to the cache.
218     */
219    @Override
220    protected void putFileToCache(final FileObject file) {
221        cache.put(file.getName(), file);
222    }
223
224    /**
225     * remove a cached file.
226     */
227    @Override
228    protected void removeFileFromCache(final FileName name) {
229        cache.remove(name);
230    }
231
232    @Override
233    public String toString() {
234        return super.toString() + " for " + file;
235    }
236
237    /*
238      will be called after all file-objects closed their streams. protected void notifyAllStreamsClosed() {
239      closeCommunicationLink(); }
240     */
241}