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