/* * Copyright 2003-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 jetbrains.mps.nodefs; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileSystem; import com.intellij.util.LocalTimeCounter; import jetbrains.mps.extapi.module.TransientSModule; import jetbrains.mps.smodel.ModelAccessHelper; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.model.SNodeReference; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; public final class MPSNodeVirtualFile extends VirtualFile { private static final byte[] CONTENTS = new byte[0]; private static final Logger LOG = LogManager.getLogger(MPSNodeVirtualFile.class); static final String NODE_PREFIX = "node://"; private SNodeReference myNode; private final RepositoryVirtualFiles myRepoFiles; private String myPath; private String myName; private String myPresentationName; private long myModificationStamp = LocalTimeCounter.currentTime(); private long myTimeStamp = -1; MPSNodeVirtualFile(@NotNull SNodeReference nodePointer, @NotNull RepositoryVirtualFiles vfs) { myNode = nodePointer; myRepoFiles = vfs; updateFields(); } // FIXME: check, perhaps is invoked with proper model access already. // for exposed files, this shall happen in exclusive read (so that different threads from readAction do not get different // result e.g. for getName(). /*package*/ void updateFields() { myRepoFiles.getRepository().getModelAccess().runReadAction(() -> { SNode node = myNode.resolve(myRepoFiles.getRepository()); if (node == null) { LOG.warn("Cannot find node for passed SNodeReference: " + myNode); myName = myPresentationName = ""; myPath = ""; } else { myName = myPresentationName = String.valueOf(node.getPresentation()); if (node.getModel() != null && node.getModel().getModule() instanceof TransientSModule) { // it's common to open same node from different generation steps (transient models) // and to tell nodes from different steps we append model's identification final String s = node.getModel().getName().getStereotype(); if (!s.isEmpty()) { myPresentationName = myName + '@' + s; } } myPath = NODE_PREFIX + myRepoFiles.getPathFacility().serializeNode(node); myTimeStamp = node.getModel().getSource().getTimestamp(); } }); } @Nullable public SNode getNode() { return myNode.resolve(myRepoFiles.getRepository()); } @NotNull public SNodeReference getSNodePointer() { return myNode; } @NotNull @Override public String getPath() { return myPath; } @Override @NotNull public VirtualFileSystem getFileSystem() { return myRepoFiles.getFileSystem(); } /** * Pre-evaluated name of the file. This method doesn't require model access. */ @Override @NotNull @NonNls public String getName() { return myName; } /** * Pre-evaluated user-presentable name of the file, may include extra information to distinguish nodes with the same {@linkplain #getName() name}. * This method doesn't require model access. */ @Override public String getPresentableName() { return myPresentationName; } @Override public boolean isDirectory() { return false; } @Override public long getLength() { return 0; } @Override public InputStream getInputStream() throws IOException { throw new UnsupportedOperationException(); } @Override @NotNull public OutputStream getOutputStream(Object requestor, long newModificationStamp, long newTimeStamp) throws IOException { throw new UnsupportedOperationException(); } @Override @NotNull public byte[] contentsToByteArray() throws IOException { return CONTENTS; } @Override @Nullable public VirtualFile getParent() { // Returning the parent of this node's model virtual file // i.e. a real directory wherein the model file lives // Needed for idea scope to work (see PsiSearchScopeUtil.isInScope) // but why it's not MPSModelVirtualFile that serves as parent for node VF? if (myNode == null || myNode.getModelReference() == null) return null; return new ModelAccessHelper(myRepoFiles.getRepository()).runReadAction(() -> { if (myNode == null) { // wow! this double check is needed even with the fact, that read action is run in the same thread // i.e. getParent() and this runnable are in the same thread // But! idea waits for the current write action to complete before proceeding to the read action // (see ApplicationalImpl.startRead()) // And it happens so that invalidate() which sets myNode to null reproducibly happens exactly // in the write action we're waiting for, hence NPE return null; } org.jetbrains.mps.openapi.model.SModelReference modelRef = myNode.getModelReference(); if (modelRef.resolve(myRepoFiles.getRepository()) == null) { return null; } MPSModelVirtualFile modelVFile = myRepoFiles.getFileFor(modelRef); if (modelVFile != null) { return modelVFile.getParent(); } return null; }); } @Override public VirtualFile[] getChildren() { return null; } @Override public void refresh(boolean asynchronous, boolean recursive, Runnable postRunnable) { if (postRunnable != null) { postRunnable.run(); } } @Override public boolean isWritable() { return true; } @Override public boolean isValid() { return myNode != null; } /*package*/ void invalidate() { if (myNode == null) { // With proper fix of https://youtrack.jetbrains.com/issue/MPS-24244 (shared VFS notifier instance), shall not happen, // nevertheless, doesn't hurt to be alert. LOG.error("Attempt to invalidate already disposed file", new Throwable()); return; } myRepoFiles.forgetVirtualFile(myNode); myNode = null; } // XXX what's the contract of the method??? public boolean hasValidMPSNode() { return isValid() && myRepoFiles.hasVirtualFileFor(myNode); } @Override public long getTimeStamp() { return myTimeStamp; } public void setTimeStamp(long newTimeStamp) { myTimeStamp = newTimeStamp; } @Override public long getModificationStamp() { return myModificationStamp; } public void setModificationStamp(long newValue) { myModificationStamp = newValue; } }