/* * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.openapi.vfs.impl; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.io.FileAttributes; import com.intellij.openapi.util.io.FileSystemUtil; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.io.FileAccessorCache; import com.intellij.util.text.ByteArrayCharSequence; import consulo.vfs.impl.archive.ArchiveEntry; import consulo.vfs.impl.archive.ArchiveFile; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.Iterator; import java.util.Map; public abstract class ZipHandler extends ArchiveHandler { private static final FileAccessorCache<ZipHandler, ArchiveFile> ourZipFileFileAccessorCache = new FileAccessorCache<ZipHandler, ArchiveFile>(20, 10) { @Override protected ArchiveFile createAccessor(ZipHandler key) throws IOException { final String canonicalPathToZip = key.getCanonicalPathToZip(); FileAttributes attributes = FileSystemUtil.getAttributes(canonicalPathToZip); key.myFileStamp = attributes != null ? attributes.lastModified : DEFAULT_TIMESTAMP; key.myFileLength = attributes != null ? attributes.length : DEFAULT_LENGTH; return key.createArchiveFile(canonicalPathToZip); } @Override protected void disposeAccessor(final ArchiveFile fileAccessor) throws IOException { // todo: ZipFile isn't disposable for Java6, replace the code below with 'disposeCloseable(fileAccessor);' fileAccessor.close(); } @Override public boolean isEqual(ZipHandler val1, ZipHandler val2) { return val1 == val2; // reference equality to handle different jars for different ZipHandlers on the same path } }; private volatile String myCanonicalPathToZip; private volatile long myFileStamp; private volatile long myFileLength; public ZipHandler(@NotNull String path) { super(path); } public abstract ArchiveFile createArchiveFile(@NotNull String path) throws IOException; @NotNull private String getCanonicalPathToZip() throws IOException { String value = myCanonicalPathToZip; if (value == null) { myCanonicalPathToZip = value = getFileToUse().getCanonicalPath(); } return value; } @NotNull @Override protected Map<String, EntryInfo> createEntriesMap() throws IOException { FileAccessorCache.Handle<ArchiveFile> zipRef = getZipFileHandle(); try { ArchiveFile zip = zipRef.get(); Map<String, EntryInfo> map = new ZipEntryMap(zip.getSize()); map.put("", createRootEntry()); Iterator<? extends ArchiveEntry> entries = zip.entries(); while (entries.hasNext()) { getOrCreate(entries.next(), map, zip); } return map; } finally { zipRef.release(); } } @NotNull private FileAccessorCache.Handle<ArchiveFile> getZipFileHandle() throws IOException { FileAccessorCache.Handle<ArchiveFile> handle = ourZipFileFileAccessorCache.get(this); if (getFile() == getFileToUse()) { // files are canonicalized // IDEA-148458, http://bugs.java.com/view_bug.do?bug_id=4425695, JVM crashes on use of opened ZipFile after it was updated // Reopen file if the file has been changed FileAttributes attributes = FileSystemUtil.getAttributes(getCanonicalPathToZip()); if (attributes == null) { throw new FileNotFoundException(getCanonicalPathToZip()); } if (attributes.lastModified == myFileStamp && attributes.length == myFileLength) return handle; // Note that zip_util.c#ZIP_Get_From_Cache will allow us to have duplicated ZipFile instances without a problem removeZipHandlerFromCache(); handle.release(); handle = ourZipFileFileAccessorCache.get(this); } return handle; } private void removeZipHandlerFromCache() { ourZipFileFileAccessorCache.remove(this); } @NotNull protected File getFileToUse() { return getFile(); } @Override public void dispose() { super.dispose(); removeZipHandlerFromCache(); } @NotNull private EntryInfo getOrCreate(@NotNull ArchiveEntry entry, @NotNull Map<String, EntryInfo> map, @NotNull ArchiveFile zip) { boolean isDirectory = entry.isDirectory(); String entryName = entry.getName(); if (StringUtil.endsWithChar(entryName, '/')) { entryName = entryName.substring(0, entryName.length() - 1); isDirectory = true; } EntryInfo info = map.get(entryName); if (info != null) return info; Pair<String, String> path = splitPath(entryName); EntryInfo parentInfo = getOrCreate(path.first, map, zip); if (".".equals(path.second)) { return parentInfo; } info = store(map, parentInfo, path.second, isDirectory, entry.getSize(), myFileStamp, entryName); return info; } @NotNull private static EntryInfo store(@NotNull Map<String, EntryInfo> map, @Nullable EntryInfo parentInfo, @NotNull CharSequence shortName, boolean isDirectory, long size, long time, @NotNull String entryName) { CharSequence sequence = shortName instanceof ByteArrayCharSequence ? shortName : ByteArrayCharSequence.convertToBytesIfAsciiString(shortName); EntryInfo info = new EntryInfo(sequence, isDirectory, size, time, parentInfo); map.put(entryName, info); return info; } @NotNull private EntryInfo getOrCreate(@NotNull String entryName, Map<String, EntryInfo> map, @NotNull ArchiveFile zip) { EntryInfo info = map.get(entryName); if (info == null) { ArchiveEntry entry = zip.getEntry(entryName + "/"); if (entry != null) { return getOrCreate(entry, map, zip); } Pair<String, String> path = splitPath(entryName); EntryInfo parentInfo = getOrCreate(path.first, map, zip); info = store(map, parentInfo, path.second, true, DEFAULT_LENGTH, DEFAULT_TIMESTAMP, entryName); } if (!info.isDirectory) { Logger.getInstance(getClass()).info(zip.getName() + ": " + entryName + " should be a directory"); info = store(map, info.parent, info.shortName, true, info.length, info.timestamp, entryName); } return info; } @NotNull @Override public byte[] contentsToByteArray(@NotNull String relativePath) throws IOException { FileAccessorCache.Handle<ArchiveFile> zipRef; try { zipRef = getZipFileHandle(); } catch (RuntimeException ex) { Throwable cause = ex.getCause(); if (cause instanceof IOException) throw (IOException)cause; throw ex; } try { ArchiveFile zip = zipRef.get(); ArchiveEntry entry = zip.getEntry(relativePath); if (entry != null) { InputStream stream = zip.getInputStream(entry); if (stream != null) { // ZipFile.c#Java_java_util_zip_ZipFile_read reads data in 8K (stack allocated) blocks - no sense to create BufferedInputStream try { return FileUtil.loadBytes(stream, (int)entry.getSize()); } finally { stream.close(); } } } } finally { zipRef.release(); } throw new FileNotFoundException(getFile() + "!/" + relativePath); } // also used in Kotlin public static void clearFileAccessorCache() { ourZipFileFileAccessorCache.clear(); } }