blob: 98c3884cc99369b41d88a47cd167d9e01937b4a3 [file] [log] [blame]
/*
* Copyright 2000-2014 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.notification.NotificationGroup;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.NotNullLazyValue;
import com.intellij.openapi.util.ShutDownTracker;
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.vfs.JarFile;
import com.intellij.openapi.vfs.JarFileSystem;
import com.intellij.openapi.vfs.VfsBundle;
import com.intellij.openapi.vfs.impl.ZipHandler;
import com.intellij.openapi.vfs.newvfs.persistent.FSRecords;
import com.intellij.openapi.vfs.newvfs.persistent.FlushingDaemon;
import com.intellij.util.io.DataExternalizer;
import com.intellij.util.io.EnumeratorStringDescriptor;
import com.intellij.util.io.IOUtil;
import com.intellij.util.io.PersistentHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.zip.ZipFile;
/**
* @author max
*/
public class JarHandler extends ZipHandler {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.impl.jar.JarHandler");
private static final String JARS_FOLDER = "jars";
private static final int FS_TIME_RESOLUTION = 2000;
private final JarFileSystemImpl myFileSystem;
private volatile File myFileWithMirrorResolved;
public JarHandler(@NotNull String path) {
super(path);
myFileSystem = (JarFileSystemImpl)JarFileSystem.getInstance();
}
@NotNull
@Override
protected File getFileToUse() {
File fileWithMirrorResolved = myFileWithMirrorResolved;
if (fileWithMirrorResolved == null) {
myFileWithMirrorResolved = fileWithMirrorResolved = getMirrorFile(getFile());
}
return fileWithMirrorResolved;
}
private File getMirrorFile(@NotNull File originalFile) {
if (!myFileSystem.isMakeCopyOfJar(originalFile)) return originalFile;
final FileAttributes originalAttributes = FileSystemUtil.getAttributes(originalFile);
if (originalAttributes == null) return originalFile;
final String folderPath = getJarsDir();
if (!new File(folderPath).exists() && !new File(folderPath).mkdirs()) {
return originalFile;
}
if (FSRecords.weHaveContentHashes) {
return getMirrorWithContentHash(originalFile, originalAttributes);
}
final String mirrorName = originalFile.getName() + "." + Integer.toHexString(originalFile.getPath().hashCode());
final File mirrorFile = new File(folderPath, mirrorName);
final FileAttributes mirrorAttributes = FileSystemUtil.getAttributes(mirrorFile);
return mirrorDiffers(originalAttributes, mirrorAttributes, false) ? copyToMirror(originalFile, mirrorFile) : mirrorFile;
}
private File getMirrorWithContentHash(File originalFile, FileAttributes originalAttributes) {
File mirrorFile = null;
String jarDir = getJarsDir();
try {
String path = originalFile.getPath();
CacheLibraryInfo info = CacheLibraryInfo.ourCachedLibraryInfo.get(path);
if (info != null &&
originalAttributes.length == info.myFileLength &&
Math.abs(originalAttributes.lastModified - info.myModificationTime) <= FS_TIME_RESOLUTION) {
mirrorFile = new File(jarDir, info.mySnapshotPath);
if (!mirrorDiffers(originalAttributes, FileSystemUtil.getAttributes(mirrorFile), true)) {
return mirrorFile;
}
}
MessageDigest sha1 = null;
File tempJarFile = null;
try {
tempJarFile = FileUtil.createTempFile(new File(jarDir), originalFile.getName(), "", true, false);
DataOutputStream os = new DataOutputStream(new FileOutputStream(tempJarFile));
try {
FileInputStream is = new FileInputStream(originalFile);
try {
byte[] buffer = new byte[20 * 1024];
sha1 = MessageDigest.getInstance("SHA1");
sha1.update(String.valueOf(originalAttributes.length).getBytes(Charset.defaultCharset()));
sha1.update((byte)0);
while (true) {
int read = is.read(buffer);
if (read < 0) break;
sha1.update(buffer, 0, read);
os.write(buffer, 0, read);
}
}
finally {
is.close();
}
}
finally {
os.close();
}
}
catch (IOException ex) {
File target = mirrorFile != null ? mirrorFile : tempJarFile != null ? tempJarFile : new File(jarDir);
reportIOErrorWithJars(originalFile, target, ex);
return originalFile;
}
catch (NoSuchAlgorithmException ex) {
LOG.error(ex);
return originalFile; // should never happen for sha1
}
String mirrorName = getSnapshotName(originalFile.getName(), sha1.digest());
mirrorFile = new File(jarDir, mirrorName);
if (mirrorDiffers(originalAttributes, FileSystemUtil.getAttributes(mirrorFile), true)) {
try {
FileUtil.delete(mirrorFile);
FileUtil.rename(tempJarFile, mirrorFile);
FileUtil.setLastModified(mirrorFile, originalAttributes.lastModified);
}
catch (IOException ex) {
reportIOErrorWithJars(originalFile, mirrorFile, ex);
return originalFile;
}
}
else {
FileUtil.delete(tempJarFile);
}
info = new CacheLibraryInfo(mirrorFile.getName(), originalAttributes.lastModified, originalAttributes.length);
CacheLibraryInfo.ourCachedLibraryInfo.put(path, info);
return mirrorFile;
}
catch (IOException ex) {
reportIOErrorWithJars(originalFile, mirrorFile != null ? mirrorFile : new File(jarDir, originalFile.getName()), ex);
return originalFile;
}
}
private static boolean mirrorDiffers(FileAttributes original, @Nullable FileAttributes mirror, boolean permitOlderMirror) {
if (mirror == null || mirror.length != original.length) return true;
long timeDiff = mirror.lastModified - original.lastModified;
if (!permitOlderMirror) timeDiff = Math.abs(timeDiff);
return timeDiff > FS_TIME_RESOLUTION;
}
private static String getSnapshotName(String name, byte[] digest) {
StringBuilder builder = new StringBuilder(name.length() + 1 + 2 * digest.length);
builder.append(name).append('.');
for (byte b : digest) {
builder.append(Character.forDigit((b & 0xF0) >> 4, 16));
builder.append(Character.forDigit(b & 0xF, 16));
}
return builder.toString();
}
@NotNull
private static String getJarsDir() {
String dir = System.getProperty("jars_dir");
return dir == null ? PathManager.getSystemPath() + File.separatorChar + JARS_FOLDER : dir;
}
@NotNull
private File copyToMirror(@NotNull File original, @NotNull File mirror) {
ProgressIndicator progress = ProgressManager.getInstance().getProgressIndicator();
if (progress != null) {
progress.pushState();
progress.setText(VfsBundle.message("jar.copy.progress", original.getPath()));
progress.setFraction(0);
}
try {
FileUtil.copy(original, mirror);
}
catch (final IOException e) {
reportIOErrorWithJars(original, mirror, e);
return original;
}
if (progress != null) {
progress.popState();
}
return mirror;
}
private static class CacheLibraryInfo {
private final String mySnapshotPath;
private final long myModificationTime;
private final long myFileLength;
private static final PersistentHashMap<String, CacheLibraryInfo> ourCachedLibraryInfo;
static {
File file = new File(new File(getJarsDir()), "snapshots_info");
PersistentHashMap<String, CacheLibraryInfo> info = null;
for (int i = 0; i < 2; ++i) {
try {
info = new PersistentHashMap<String, CacheLibraryInfo>(
file, new EnumeratorStringDescriptor(), new DataExternalizer<CacheLibraryInfo>() {
@Override
public void save(@NotNull DataOutput out, CacheLibraryInfo value) throws IOException {
IOUtil.writeUTF(out, value.mySnapshotPath);
out.writeLong(value.myModificationTime);
out.writeLong(value.myFileLength);
}
@Override
public CacheLibraryInfo read(@NotNull DataInput in) throws IOException {
return new CacheLibraryInfo(IOUtil.readUTF(in), in.readLong(), in.readLong());
}
}
);
break;
} catch (IOException ex) {
PersistentHashMap.deleteFilesStartingWith(file);
}
}
assert info != null;
ourCachedLibraryInfo = info;
FlushingDaemon.everyFiveSeconds(new Runnable() {
@Override
public void run() {
flushCachedLibraryInfos();
}
});
ShutDownTracker.getInstance().registerShutdownTask(new Runnable() {
@Override
public void run() {
flushCachedLibraryInfos();
}
});
}
private static void flushCachedLibraryInfos() {
if (ourCachedLibraryInfo.isDirty()) ourCachedLibraryInfo.force();
}
private CacheLibraryInfo(@NotNull String path, long time, long length) {
mySnapshotPath = path;
myModificationTime = time;
myFileLength = length;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CacheLibraryInfo info = (CacheLibraryInfo)o;
if (myFileLength != info.myFileLength) return false;
if (myModificationTime != info.myModificationTime) return false;
if (!mySnapshotPath.equals(info.mySnapshotPath)) return false;
return true;
}
@Override
public int hashCode() {
int result = mySnapshotPath.hashCode();
result = 31 * result + (int)(myModificationTime ^ (myModificationTime >>> 32));
result = 31 * result + (int)(myFileLength ^ (myFileLength >>> 32));
return result;
}
}
private static final NotNullLazyValue<NotificationGroup> ERROR_COPY_NOTIFICATION = new NotNullLazyValue<NotificationGroup>() {
@NotNull
@Override
protected NotificationGroup compute() {
return NotificationGroup.balloonGroup(VfsBundle.message("jar.copy.error.title"));
}
};
private void reportIOErrorWithJars(File original, File target, IOException e) {
LOG.warn(e);
String path = original.getPath();
myFileSystem.setNoCopyJarForPath(path);
String message = VfsBundle.message("jar.copy.error.message", path, target.getPath(), e.getMessage());
ERROR_COPY_NOTIFICATION.getValue().createNotification(message, NotificationType.ERROR).notify(null);
}
/** @deprecated to be removed in IDEA 15 */
@SuppressWarnings("deprecation")
public JarFile getJar() {
File original = getFile();
try {
return new JarHandlerBase.MyJarFile(new ZipFile(getMirrorFile(original)));
}
catch (IOException e) {
LOG.warn(e.getMessage() + ": " + original, e);
return null;
}
}
}