/* * Copyright 2003-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 jetbrains.mps.ide.vfs; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.Result; import com.intellij.openapi.application.WriteAction; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.JarFileSystem; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.newvfs.ArchiveFileSystem; import com.intellij.openapi.vfs.newvfs.NewVirtualFile; import jetbrains.mps.util.EqualUtil; import jetbrains.mps.vfs.CachingContext; import jetbrains.mps.vfs.CachingFile; import jetbrains.mps.vfs.CachingFileSystem; import jetbrains.mps.vfs.IFile; import jetbrains.mps.vfs.path.Path; import jetbrains.mps.vfs.path.UniPath; import jetbrains.mps.vfs.ex.IFileEx; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.annotations.Internal; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * TODO rewrite using {@link Path}; rewrite {@link #getChildren(),#getDescendant(String)} behavior in the case of jar system */ public class IdeaFile implements IFileEx, CachingFile { private final static Logger LOG = LogManager.getLogger(IdeaFile.class); private final IdeaFileSystem myFileSystem; /* * remember the name used to create this instance, as it might be different from a name in fs on case-insensitive filesystem */ private String myPath; private VirtualFile myVirtualFile = null; @Internal public IdeaFile(IdeaFileSystem fileSystem, @NotNull String path) { myFileSystem = fileSystem; myPath = jetbrains.mps.util.FileUtil.normalize(path); } @Internal public IdeaFile(IdeaFileSystem fileSystem, @NotNull VirtualFile virtualFile) { myFileSystem = fileSystem; myVirtualFile = virtualFile; myPath = virtualFile.getPath(); } @NotNull @Override public String getPath() { if (findVirtualFile()) { return myVirtualFile.getPath(); } else { return myPath; } } @NotNull @Override public UniPath toPath() { return UniPath.fromString(getPath()); } @Override public URL getUrl() throws MalformedURLException { if (findVirtualFile()) { return VfsUtilCore.convertToURL(myVirtualFile.getUrl()); } else { return new File(myPath).toURI().toURL(); } } @NotNull @Override public CachingFileSystem getFileSystem() { return myFileSystem; } @NotNull @Override public String getName() { if (findVirtualFile()) { return myVirtualFile.getName(); } else { return truncFileName(myPath); } } @Override public IdeaFile getParent() { if (findVirtualFile()) { VirtualFile parentVirtualFile = myVirtualFile.getParent(); if (parentVirtualFile != null) { return new IdeaFile(myFileSystem, parentVirtualFile); } return null; } else { return new IdeaFile(myFileSystem, truncateDirPath(myPath)); } } @Override public List<IFile> getChildren() { if (findVirtualFile()) { VirtualFile[] children = new VirtualFile[0]; if (myVirtualFile.isValid()) { children = myVirtualFile.getChildren(); } ArrayList<IdeaFile> result = new ArrayList<>(); for (VirtualFile child : children) { result.add(new IdeaFile(myFileSystem, child)); } return Collections.unmodifiableList(result); } else { return Collections.emptyList(); } } @Override @NotNull public IdeaFile getDescendant(@NotNull String suffix) { String path = getPath(); String separator = Path.UNIX_SEPARATOR; // we are system-independent underneath return new IdeaFile(myFileSystem, path + (path.endsWith(separator) ? "" : separator) + suffix); } @Override public boolean isDirectory() { return findVirtualFile() ? myVirtualFile.isDirectory() : new File(myPath).isDirectory(); } @Override public boolean isReadOnly() { return exists() && !myVirtualFile.isWritable(); } @Override public long lastModified() { if (findVirtualFile()) { return myVirtualFile.getTimeStamp(); } else { return -1; } } @Override public long length() { if (findVirtualFile()) { return myVirtualFile.getLength(); } else { return -1; } } @Override public boolean createNewFile() { ApplicationManager.getApplication().assertWriteAccessAllowed(); if (findVirtualFile()) { return !myVirtualFile.isDirectory(); } else { try { VirtualFile directory = createDirectories(truncateDirPath(myPath)); String fileName = truncFileName(myPath); directory.findChild(fileName); // This is a workaround for IDEA-67279 myVirtualFile = directory.createChildData(myFileSystem, fileName); return true; } catch (IOException e) { LOG.error(null, e); return false; } } } //this was copied from Idea's VfsUtil. The point of copying is changing the requestor not to get back-events during saving models private VirtualFile createDirectories(final String directoryPath) throws IOException { return new WriteAction<VirtualFile>() { @Override protected void run(@NotNull Result<VirtualFile> result) throws Throwable { VirtualFile res = createDirectoryIfMissing(directoryPath); result.setResult(res); } }.execute().throwException().getResultObject(); } //this was copied from Idea's VfsUtil. The point of copying is changing the requestor not to get back-events during saving models private VirtualFile createDirectoryIfMissing(String directoryPath) throws IOException { String path = FileUtil.toSystemIndependentName(directoryPath); final VirtualFile file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path); if (file == null) { int pos = path.lastIndexOf('/'); if (pos < 0) return null; VirtualFile parent = createDirectoryIfMissing(path.substring(0, pos)); if (parent == null) return null; final String dirName = path.substring(pos + 1); VirtualFile child = parent.findChild(dirName); if (child != null && child.isDirectory()) return child; return parent.createChildDirectory(myFileSystem, dirName); } return file; } @Override public boolean mkdirs() { ApplicationManager.getApplication().assertWriteAccessAllowed(); if (findVirtualFile()) { return myVirtualFile.isDirectory(); } else { try { myVirtualFile = createDirectories(myPath); return true; } catch (IOException e) { return false; } } } @Override public boolean exists() { return findVirtualFile() && myVirtualFile.exists(); } @Override public boolean delete() { if (findVirtualFile()) { try { myVirtualFile.delete(myFileSystem); myVirtualFile = null; return true; } catch (IOException e) { LOG.warn("Could not delete file: ", e); return false; } } else { return true; } } @Override public boolean rename(String newName) { try { myVirtualFile.rename(myFileSystem, newName); myPath = myVirtualFile.getPath(); return true; } catch (IOException e) { LOG.warn("Could not rename file: ", e); return false; } } @Override public boolean move(IFile newParent) { if (newParent instanceof IdeaFile && ((IdeaFile) newParent).findVirtualFile()) { try { myVirtualFile.move(myFileSystem, ((IdeaFile) newParent).myVirtualFile); return true; } catch (IOException e) { LOG.warn("Could not rename file: ", e); return false; } } else { return false; } } @Override public InputStream openInputStream() throws IOException { if (findVirtualFile()) { return myVirtualFile.getInputStream(); } else { throw new FileNotFoundException("File not found: " + myPath); } } @Override public OutputStream openOutputStream() throws IOException { ApplicationManager.getApplication().assertWriteAccessAllowed(); if (findVirtualFile() || createNewFile()) { if (myVirtualFile.getFileSystem() instanceof JarFileSystem) { throw new UnsupportedOperationException("Cannot write to Jar files"); } else { if (!myVirtualFile.getFileSystem().isCaseSensitive()) { // Mac default (HFS), NTFS - are case-insensitive, looking up file "b/A" when there's "b/a" gives // existing file. However, Java is strict about case, and won't allow class A to live in file a.java // Hence, when we try to write into a file with the name different from one requested initially, // try to bring the name up to the desired one. final String desiredFileName = truncFileName(myPath); if (!myVirtualFile.getName().equals(desiredFileName)) { rename(desiredFileName); } } return myVirtualFile.getOutputStream(myFileSystem); } } else { throw new IOException("Could not create file: " + myPath); } } @Nullable public VirtualFile getVirtualFile() { findVirtualFile(); return myVirtualFile; } @Override public boolean setTimeStamp(long time) { if (findVirtualFile() && myVirtualFile instanceof NewVirtualFile) { try { ((NewVirtualFile) myVirtualFile).setTimeStamp(time); return true; } catch (IOException e) { LOG.warn("", e); } } return false; } @Override public void refresh(@NotNull CachingContext context) { if (findVirtualFile()) { myVirtualFile.getChildren(); // This was added to force refresh myVirtualFile.refresh(!context.isSynchronous(), context.isRecursive()); } else { findVirtualFile(true); } } @Override public boolean isInArchive() { if (findVirtualFile()) { return myVirtualFile.getFileSystem() instanceof ArchiveFileSystem; } else { return myPath.contains("!"); } } @Override public boolean isArchive() { return myPath.endsWith(JarFileSystem.PROTOCOL) || myPath.endsWith(JarFileSystem.JAR_SEPARATOR); } @Override public IFile getBundleHome() { if (findVirtualFile()) { if (myVirtualFile.getFileSystem() instanceof ArchiveFileSystem) { VirtualFile fileForJar = ((ArchiveFileSystem) myVirtualFile.getFileSystem()).getLocalByEntry(myVirtualFile); if (fileForJar == null) { return null; } return new IdeaFile(myFileSystem, fileForJar); } else { return getParent(); } } else { if (myPath.contains("!")) { return new IdeaFile(myFileSystem, myPath.substring(0, myPath.indexOf("!"))); } else { return getParent(); } } } private boolean findVirtualFile() { return findVirtualFile(false); } /** * in the case when the underlying virtual file is not valid we perform a "refresh" */ private boolean findVirtualFile(boolean withRefresh) { if (myVirtualFile == null || !myVirtualFile.isValid()) { myVirtualFile = findIdeaFile(myPath, withRefresh); } return myVirtualFile != null; } // obviously all string-processing methods might be cached @Nullable private static VirtualFile findIdeaFile(String path, boolean withRefresh) { LocalFileSystem localFileSystem = LocalFileSystem.getInstance(); if (path.contains("!")) { int index = path.indexOf("!"); String jarPath = path.substring(0, index); String entryPath = path.substring(index + 1); assert entryPath.indexOf('\\') == -1 : "No backslashes are allowed in DOT_JAR entry path: " + path; entryPath = entryPath.replace('\\', '/'); if (entryPath.startsWith("/")) { entryPath = entryPath.substring(1); } VirtualFile jarFile; if (withRefresh) { jarFile = localFileSystem.refreshAndFindFileByPath(jarPath); } else { jarFile = localFileSystem.findFileByPath(jarPath); } JarFileSystem jarFileSystem = JarFileSystem.getInstance(); if (jarFile != null) { VirtualFile jarRootFile = jarFileSystem.getJarRootForLocalFile(jarFile); if (jarRootFile != null) { return jarRootFile.findFileByRelativePath(entryPath); } } return null; } else { if (withRefresh) { return localFileSystem.refreshAndFindFileByPath(path); } else { return localFileSystem.findFileByPath(path); } } } @NotNull private static String truncateDirPath(String path) { int index = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); if (index == -1) { return path; } else { return path.substring(0, index); } } private static String truncFileName(String path) { int index = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); if (index == -1) { return path; } else { return path.substring(index + 1); } } private String getSystemIndependentPath() { return getPath().replace('\\', '/'); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; IdeaFile ideaFile = (IdeaFile) o; return EqualUtil.equals(getSystemIndependentPath(), ideaFile.getSystemIndependentPath()); } @Override public int hashCode() { return getSystemIndependentPath().hashCode(); } @Override public String toString() { if (myVirtualFile != null) { return "IdeaFile{" + myVirtualFile + "}"; } else { return "IdeaFile{path: " + myPath + "}"; } } }