/* * Copyright 2000-2013 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.jar; import com.intellij.openapi.diagnostic.LogUtil; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.io.BufferExposingByteArrayInputStream; import com.intellij.openapi.util.io.FileAttributes; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.JarFile; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.reference.SoftReference; import com.intellij.util.ArrayUtil; import com.intellij.util.TimedReference; import gnu.trove.THashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.ref.Reference; import java.util.*; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; public class JarHandlerBase { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.impl.jar.JarHandlerBase"); private static final long DEFAULT_LENGTH = 0L; private static final long DEFAULT_TIMESTAMP = -1L; private final TimedReference<JarFile> myJarFile = new TimedReference<JarFile>(null); private Reference<Map<String, EntryInfo>> myRelPathsToEntries = new SoftReference<Map<String, EntryInfo>>(null); private final Object lock = new Object(); protected final String myBasePath; protected static class EntryInfo { protected final boolean isDirectory; protected final String shortName; protected final EntryInfo parent; public EntryInfo(@NotNull String shortName, final EntryInfo parent, final boolean directory) { this.shortName = shortName; this.parent = parent; isDirectory = directory; } } public JarHandlerBase(@NotNull String path) { myBasePath = path; } protected void clear() { synchronized (lock) { myRelPathsToEntries = null; myJarFile.set(null); } } public File getMirrorFile(@NotNull File originalFile) { return originalFile; } @Nullable public JarFile getJar() { JarFile jar = myJarFile.get(); if (jar == null) { synchronized (lock) { jar = myJarFile.get(); if (jar == null) { jar = createJarFile(); if (jar != null) { myJarFile.set(jar); } } } } return jar; } @Nullable protected JarFile createJarFile() { final File originalFile = getOriginalFile(); try { @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") final ZipFile zipFile = new ZipFile(getMirrorFile(originalFile)); class MyJarEntry implements JarFile.JarEntry { private final ZipEntry myEntry; MyJarEntry(ZipEntry entry) { myEntry = entry; } public ZipEntry getEntry() { return myEntry; } @Override public String getName() { return myEntry.getName(); } @Override public long getSize() { return myEntry.getSize(); } @Override public long getTime() { return myEntry.getTime(); } @Override public boolean isDirectory() { return myEntry.isDirectory(); } } return new JarFile() { @Override public JarFile.JarEntry getEntry(String name) { try { ZipEntry entry = zipFile.getEntry(name); if (entry != null) { return new MyJarEntry(entry); } } catch (IllegalArgumentException e) { LOG.warn(e); } return null; } @Override public InputStream getInputStream(JarFile.JarEntry entry) throws IOException { return zipFile.getInputStream(((MyJarEntry)entry).myEntry); } @Override public Enumeration<? extends JarFile.JarEntry> entries() { return new Enumeration<JarEntry>() { private final Enumeration<? extends ZipEntry> entries = zipFile.entries(); @Override public boolean hasMoreElements() { return entries.hasMoreElements(); } @Override public JarEntry nextElement() { try { ZipEntry entry = entries.nextElement(); if (entry != null) { return new MyJarEntry(entry); } } catch (IllegalArgumentException e) { LOG.warn(e); } return null; } }; } @Override public ZipFile getZipFile() { return zipFile; } }; } catch (IOException e) { LOG.warn(e.getMessage() + ": " + originalFile.getPath(), e); return null; } } @NotNull protected File getOriginalFile() { return new File(myBasePath); } @NotNull private static EntryInfo getOrCreate(@NotNull String entryName, boolean isDirectory, @NotNull Map<String, EntryInfo> map) { EntryInfo info = map.get(entryName); if (info == null) { int idx = entryName.lastIndexOf('/'); final String parentEntryName = idx > 0 ? entryName.substring(0, idx) : ""; String shortName = idx > 0 ? entryName.substring(idx + 1) : entryName; if (".".equals(shortName)) return getOrCreate(parentEntryName, true, map); info = new EntryInfo(shortName, getOrCreate(parentEntryName, true, map), isDirectory); map.put(entryName, info); } return info; } @NotNull public String[] list(@NotNull final VirtualFile file) { synchronized (lock) { EntryInfo parentEntry = getEntryInfo(file); Set<String> names = new HashSet<String>(); for (EntryInfo info : getEntriesMap().values()) { if (info.parent == parentEntry) { names.add(info.shortName); } } return ArrayUtil.toStringArray(names); } } protected EntryInfo getEntryInfo(@NotNull VirtualFile file) { synchronized (lock) { String parentPath = getRelativePath(file); return getEntryInfo(parentPath); } } public EntryInfo getEntryInfo(@NotNull String parentPath) { return getEntriesMap().get(parentPath); } @NotNull protected Map<String, EntryInfo> getEntriesMap() { synchronized (lock) { Map<String, EntryInfo> map = SoftReference.dereference(myRelPathsToEntries); if (map == null) { final JarFile zip = getJar(); if (zip != null) { LogUtil.debug(LOG, "mapping %s", myBasePath); map = new THashMap<String, EntryInfo>(); map.put("", new EntryInfo("", null, true)); final Enumeration<? extends JarFile.JarEntry> entries = zip.entries(); while (entries.hasMoreElements()) { final JarFile.JarEntry entry = entries.nextElement(); final String name = entry.getName(); final boolean isDirectory = StringUtil.endsWithChar(name, '/'); getOrCreate(isDirectory ? name.substring(0, name.length() - 1) : name, isDirectory, map); } myRelPathsToEntries = new SoftReference<Map<String, EntryInfo>>(map); } else { map = Collections.emptyMap(); } } return map; } } @NotNull private String getRelativePath(@NotNull VirtualFile file) { final String path = file.getPath().substring(myBasePath.length() + 1); return StringUtil.startsWithChar(path, '/') ? path.substring(1) : path; } @Nullable private JarFile.JarEntry convertToEntry(@NotNull VirtualFile file) { String path = getRelativePath(file); final JarFile jar = getJar(); return jar == null ? null : jar.getEntry(path); } public long getLength(@NotNull final VirtualFile file) { final JarFile.JarEntry entry = convertToEntry(file); synchronized (lock) { return entry == null ? DEFAULT_LENGTH : entry.getSize(); } } @NotNull public InputStream getInputStream(@NotNull final VirtualFile file) throws IOException { return new BufferExposingByteArrayInputStream(contentsToByteArray(file)); } @NotNull public byte[] contentsToByteArray(@NotNull final VirtualFile file) throws IOException { final JarFile.JarEntry entry = convertToEntry(file); if (entry == null) { return ArrayUtil.EMPTY_BYTE_ARRAY; } synchronized (lock) { final JarFile jar = getJar(); assert jar != null : file; final InputStream stream = jar.getInputStream(entry); assert stream != null : file; try { return FileUtil.loadBytes(stream, (int)entry.getSize()); } finally { stream.close(); } } } public long getTimeStamp(@NotNull final VirtualFile file) { if (file.getParent() == null) return getOriginalFile().lastModified(); // Optimization final JarFile.JarEntry entry = convertToEntry(file); synchronized (lock) { return entry == null ? DEFAULT_TIMESTAMP : entry.getTime(); } } public boolean isDirectory(@NotNull final VirtualFile file) { if (file.getParent() == null) return true; // Optimization synchronized (lock) { final String path = getRelativePath(file); final EntryInfo info = getEntryInfo(path); return info == null || info.isDirectory; } } public boolean exists(@NotNull final VirtualFile fileOrDirectory) { if (fileOrDirectory.getParent() == null) { // Optimization. Do not build entries if asked for jar root existence. return myJarFile.get() != null || getOriginalFile().exists(); } return getEntryInfo(fileOrDirectory) != null; } @Nullable public FileAttributes getAttributes(@NotNull final VirtualFile file) { final JarFile.JarEntry entry = convertToEntry(file); synchronized (lock) { final EntryInfo entryInfo = getEntryInfo(getRelativePath(file)); if (entryInfo == null) return null; final long length = entry != null ? entry.getSize() : DEFAULT_LENGTH; final long timeStamp = entry != null ? entry.getTime() : DEFAULT_TIMESTAMP; return new FileAttributes(entryInfo.isDirectory, false, false, false, length, timeStamp, false); } } }