/*
* 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.application.ApplicationManager;
import com.intellij.openapi.components.ApplicationComponent;
import com.intellij.openapi.vfs.DeprecatedVirtualFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.LocalTimeCounter;
import jetbrains.mps.ide.MPSCoreComponents;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.smodel.RepoListenerRegistrar;
import jetbrains.mps.smodel.SNodePointer;
import jetbrains.mps.smodel.event.NodeChangeCollector;
import jetbrains.mps.util.annotation.ToRemove;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.event.AbstractModelChangeEvent;
import org.jetbrains.mps.openapi.event.SNodeAddEvent;
import org.jetbrains.mps.openapi.event.SNodeRemoveEvent;
import org.jetbrains.mps.openapi.event.SPropertyChangeEvent;
import org.jetbrains.mps.openapi.event.SReferenceChangeEvent;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.module.SRepositoryContentAdapter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
public final class NodeVirtualFileSystem extends DeprecatedVirtualFileSystem implements ApplicationComponent {
public static NodeVirtualFileSystem getInstance() {
return ApplicationManager.getApplication().getComponent(NodeVirtualFileSystem.class);
}
public NodeVirtualFileSystem(MPSCoreComponents coreComponents) {
// FIXME this component shall be ProjectComponent, pass MPSProject.getRepository(); initialize in projectOpened()
SRepository myRepository = coreComponents.getModuleRepository();
myGlobalRepoFiles = new RepositoryVirtualFiles(this, myRepository);
myRepositoryListener = new MyRepositoryListener(myGlobalRepoFiles);
}
/*
* For transition period, left container of virtual files coming from MPSModuleRepository.getInstance(), and use it
* as default when supplied repository is not found (regardless of whether supplied repo matches MPSModuleRepository instance) for
* compatibility with existing code, that doesn't manage SRepository well. Shall drop as soon as MPSModuleRepository instance is history
* (or at least managed and not exposed to user code).
*/
@ToRemove(version = 3.4)
private final RepositoryVirtualFiles myGlobalRepoFiles;
private final Object myRepoVFLock = new Object();
// I don't expect this collection to grow significantly, hence just List
private final List<RepositoryVirtualFiles> myPerRepositoryFiles = new CopyOnWriteArrayList<>();
private final Map<RepositoryVirtualFiles, MyRepositoryListener> myFiles2ListenerMap = new HashMap<>();
private final SRepositoryContentAdapter myRepositoryListener;
private boolean myDisposed = false;
void register(@NotNull RepositoryVirtualFiles repoFiles) {
MyRepositoryListener listener;
synchronized (myRepoVFLock) {
// assert not more than 1 file container per repository
RepositoryVirtualFiles existing = findForRepository(repoFiles.getRepository());
if (existing != null) {
throw new IllegalArgumentException("Attempt to register another VirtualFile container for the same repository");
}
// sort of stack, most recent first. just for fun, no hidden assumptions.
myPerRepositoryFiles.add(0, repoFiles);
listener = new MyRepositoryListener(repoFiles);
myFiles2ListenerMap.put(repoFiles, listener);
}
new RepoListenerRegistrar(repoFiles.getRepository(), listener).attach();
}
void unregister(@NotNull RepositoryVirtualFiles repoFiles) {
MyRepositoryListener listener;
synchronized (myRepoVFLock) {
myPerRepositoryFiles.remove(repoFiles);
listener = myFiles2ListenerMap.remove(repoFiles);
}
if (listener != null) {
new RepoListenerRegistrar(repoFiles.getRepository(), listener).detach();
}
}
public MPSNodeVirtualFile getFileFor(@NotNull SRepository repository, @NotNull final SNode node) {
return getFileFor(repository, node.getReference());
}
public MPSNodeVirtualFile getFileFor(@NotNull SRepository repository, @NotNull final SNodeReference nodePointer) {
final RepositoryVirtualFiles rvf = findForRepository(repository);
return rvf != null ? rvf.getFileFor(nodePointer) : myGlobalRepoFiles.getFileFor(nodePointer);
}
public MPSModelVirtualFile getFileFor(@NotNull SRepository repository, @NotNull final SModelReference modelReference) {
final RepositoryVirtualFiles rvf = findForRepository(repository);
return rvf != null ? rvf.getFileFor(modelReference) : myGlobalRepoFiles.getFileFor(modelReference);
}
@Nullable
private RepositoryVirtualFiles findForRepository(@NotNull SRepository repo) {
synchronized (myRepoVFLock) {
for (RepositoryVirtualFiles rvf : myPerRepositoryFiles) {
if (repo.equals(rvf.getRepository())) {
return rvf;
}
}
}
return null;
}
@Override
@NonNls
@NotNull
public String getComponentName() {
return "MPS File System";
}
@Override
public void initComponent() {
new RepoListenerRegistrar(myGlobalRepoFiles.getRepository(), myRepositoryListener).attach();
}
@Override
public void disposeComponent() {
new RepoListenerRegistrar(myGlobalRepoFiles.getRepository(), myRepositoryListener).detach();
myDisposed = true;
}
@Override
@NotNull
@NonNls
public String getProtocol() {
return "mps";
}
@Override
@Nullable
public VirtualFile findFileByPath(final @NotNull @NonNls String path) {
for (RepositoryVirtualFiles rvf : myPerRepositoryFiles) { // going by snapshot here and checking all persisted repositories
VirtualFile file = new ModelAccessHelper(rvf.getRepository()).runReadAction(() -> {
synchronized (myRepoVFLock) {
if (myPerRepositoryFiles.contains(rvf)) { // double check
return rvf.findFileByPath(path);
}
return null;
}
});
if (file != null) {
return file;
}
}
return new ModelAccessHelper(myGlobalRepoFiles.getRepository()).runReadAction(() -> myGlobalRepoFiles.findFileByPath(path));
}
@Override
public void refresh(boolean asynchronous) {
// no-op
}
@Override
@Nullable
public VirtualFile refreshAndFindFileByPath(@NotNull String path) {
return null;
}
private void updateModificationStamp(Collection<MPSNodeVirtualFile> files) {
// identical timestamp for all roots touched simultaneously
final long vfsStamp = LocalTimeCounter.currentTime();
final long localStamp = System.currentTimeMillis();
for (MPSNodeVirtualFile vf : files) {
vf.setModificationStamp(vfsStamp);
vf.setTimeStamp(localStamp);
}
}
private class MyRepositoryListener extends SRepositoryContentAdapter {
private final RepositoryVirtualFiles myRepoFiles;
private final NodeChangeCollector myChangeCollector = new NodeChangeCollector();
/**
* FIXME the only reason we don't use single listener instance (we can obtain proper SRepository from the change event's model/node)
* FIXME is that our project repository implementation is not capable of event sending, all events come from global repository.
* Thus, it would be impossible to find proper RepositoryVirtualFiles instance. Shall fix ProjectRepository and its base impl
* to send events on its own.
*/
public MyRepositoryListener(RepositoryVirtualFiles repoFiles) {
myRepoFiles = repoFiles;
}
@Override
protected boolean isIncluded(SModule module) {
return !module.isReadOnly();
}
@Override
protected void startListening(SModel model) {
// we listen to SModelListener#modelReplaced
model.addModelListener(this);
// we care about node changes
model.addChangeListener(this);
}
@Override
protected void stopListening(SModel model) {
model.removeChangeListener(this);
model.removeModelListener(this);
forget(model);
}
// XXX I keep this method instead of direct access to myRepoFiles field in a desperate hope to have single repo listener some day,
// which would pick RVF instance based on model's repository. And that's the reason I check for null return value
@Nullable
private RepositoryVirtualFiles findRepoFiles(SModel m) {
if (m.getRepository() == null) {
return null;
}
return findRepoFiles(m.getRepository());
}
@Nullable
private RepositoryVirtualFiles findRepoFiles(SRepository repository) {
// TODO single listener instance and find RVF by repo
return myRepoFiles;
}
private void forget(SModel modelDescriptor) {
final RepositoryVirtualFiles rvf = findRepoFiles(modelDescriptor);
if (rvf == null) {
return;
}
VFSNotifier vfsNotifier = rvf.getNotifier(new VFSNotifier(rvf));
vfsNotifier.deleted(rvf.getKnownVirtualFilesIn(modelDescriptor.getReference()));
vfsNotifier.execute();
}
// SModelListener#modelReplaced
@Override
public void modelReplaced(SModel md) {
final RepositoryVirtualFiles rvf = findRepoFiles(md);
if (rvf == null) {
return;
}
final Collection<MPSNodeVirtualFile> filesInModel = rvf.getKnownVirtualFilesIn(md.getReference());
updateModificationStamp(filesInModel);
Collection<MPSNodeVirtualFile> deletedFiles = new ArrayList<MPSNodeVirtualFile>();
Collection<MPSNodeVirtualFile> changedFiles = new ArrayList<MPSNodeVirtualFile>();
for (MPSNodeVirtualFile vf : filesInModel) {
// XXX reconsider vf.getNode() (with SRepository in file construction time), vf.getNode(myRepository) and explicit resolve here
if (vf.getNode() == null) {
deletedFiles.add(vf);
} else {
changedFiles.add(vf);
}
}
VFSNotifier vfsNotifier = rvf.getNotifier(new VFSNotifier(rvf));
vfsNotifier.deleted(deletedFiles);
vfsNotifier.changed(changedFiles);
vfsNotifier.execute();
}
@Override
public void commandStarted(SRepository repository) {
myChangeCollector.start();
}
@Override
public void commandFinished(SRepository repository) {
myChangeCollector.stop();
final List<AbstractModelChangeEvent> events = myChangeCollector.purge();
final RepositoryVirtualFiles rvf = findRepoFiles(repository);
if (rvf == null || events.isEmpty()) {
return;
}
Collection<MPSNodeVirtualFile> deletedFiles = new ArrayList<MPSNodeVirtualFile>();
Collection<MPSNodeVirtualFile> changedFiles = new ArrayList<MPSNodeVirtualFile>();
for (AbstractModelChangeEvent evt : events) {
if (evt instanceof SPropertyChangeEvent) {
// candidate for rename
MPSNodeVirtualFile vf = rvf.getVirtualFile(((SPropertyChangeEvent) evt).getNode().getReference());
if (vf != null) {
changedFiles.add(vf);
}
} else if (evt instanceof SNodeRemoveEvent) {
// SNode.getReference() for deleted node produces invalid pointer
MPSNodeVirtualFile vf = rvf.getVirtualFile(new SNodePointer(evt.getModel().getReference(), ((SNodeRemoveEvent) evt).getChild().getNodeId()));
if (vf != null) {
deletedFiles.add(vf);
}
}
}
VFSNotifier vfsNotifier = rvf.getNotifier(new VFSNotifier(rvf));
vfsNotifier.deleted(deletedFiles);
vfsNotifier.changed(changedFiles);
vfsNotifier.execute();
}
@Override
public void propertyChanged(@NotNull SPropertyChangeEvent event) {
updateFileTimestampOfAffectedNodes(event, event.getNode().getReference(), new SNodePointer(event.getNode().getContainingRoot()));
myChangeCollector.propertyChanged(event);
}
@Override
public void referenceChanged(@NotNull SReferenceChangeEvent event) {
updateFileTimestampOfAffectedNodes(event, event.getNode().getReference(), new SNodePointer(event.getNode().getContainingRoot()));
}
@Override
public void nodeAdded(@NotNull SNodeAddEvent event) {
if (event.isRoot()) {
// added root of no interest - there could be no file for it yet.
return;
}
final SNode affectedNode = event.getParent();
updateFileTimestampOfAffectedNodes(event, new SNodePointer(affectedNode), new SNodePointer(affectedNode.getContainingRoot()));
}
@Override
public void nodeRemoved(@NotNull SNodeRemoveEvent event) {
// SNode.getReference() for deleted node produces invalid pointer
final SNodeReference removedNode = new SNodePointer(event.getModel().getReference(), event.getChild().getNodeId());
updateFileTimestampOfAffectedNodes(event, removedNode, event.isRoot() ? removedNode : new SNodePointer(event.getParent().getContainingRoot()));
myChangeCollector.nodeRemoved(event);
}
/*
* SModelAdapter that used to update timestamps was deliberately extracted out of end of command listener. Guess, either to
* update TS immediately or to support multiple TS changes within single command. That's why I keep this immediate TS update approach
* in the new repository listener, too.
*
* XXX 1. Do we need to update TS on model imports change? Present openapi listener doesn't support these changes, but old code didn't care either
* XXX 2. Why don't we update TS of MPSModelVirtualFile?
*/
private void updateFileTimestampOfAffectedNodes(AbstractModelChangeEvent event, /*not null*/ SNodeReference changed, @Nullable SNodeReference root) {
final RepositoryVirtualFiles rvf = findRepoFiles(event.getModel());
if (rvf == null) {
return;
}
ArrayList<MPSNodeVirtualFile> files = new ArrayList<>(2);
final MPSNodeVirtualFile vf1 = rvf.getVirtualFile(changed);
if (vf1 != null) {
files.add(vf1);
}
if (root != null && root != changed) {
MPSNodeVirtualFile vf2 = rvf.getVirtualFile(root);
if (vf2 != null && vf2 != vf1) {
files.add(vf2);
}
}
updateModificationStamp(files);
}
}
private class VFSNotifier implements Runnable {
private final RepositoryVirtualFiles mySource;
private final Set<MPSNodeVirtualFile> myDeletedFiles = new HashSet<>();
private final Set<MPSNodeVirtualFile> myChangedFiles = new HashSet<>();
private final AtomicBoolean myPendingChanges = new AtomicBoolean();
public VFSNotifier(@NotNull RepositoryVirtualFiles source) {
mySource = source;
}
public synchronized void deleted(Collection<MPSNodeVirtualFile> deletedFiles) {
if (!deletedFiles.isEmpty()) {
myPendingChanges.set(true);
}
myDeletedFiles.addAll(deletedFiles);
}
public synchronized void changed(Collection<MPSNodeVirtualFile> changed) {
if (!changed.isEmpty()) {
myPendingChanges.set(true);
}
myChangedFiles.addAll(changed);
}
/**
* Asynchronous invocation does not guarantee that node reference will persist in the given repository.
* However Artem proposed to get rid of SNodeReference#resolve at all.
*/
public void execute() {
if (hasPendingNotifications()) {
mySource.scheduleNotifier(this);
}
}
@Override
public void run() {
if (myDisposed) {
return;
}
ArrayList<MPSNodeVirtualFile> deletedFiles;
ArrayList<MPSNodeVirtualFile> changedFiles;
synchronized (this) {
deletedFiles = new ArrayList<>(myDeletedFiles);
changedFiles = new ArrayList<>(myChangedFiles);
myDeletedFiles.clear();
myChangedFiles.clear();
}
// notifier is shared, it's possible to get both changed and deleted notification for the same file
// no reason to report changes for deleted.
changedFiles.removeAll(deletedFiles);
for (MPSNodeVirtualFile deletedFile : deletedFiles) {
fireBeforeFileDeletion(this, deletedFile);
deletedFile.invalidate();
fireFileDeleted(this, deletedFile, deletedFile.getName(), null);
}
for (MPSNodeVirtualFile changedFile : changedFiles) {
String oldName = changedFile.getName();
changedFile.updateFields();
String newName = changedFile.getName();
if (!oldName.equals(newName)) {
// XXX this effectively reverts 0ec4b371f9acef4c82b644dfa3a295961b515efc, I wonder what's the reason not to send file rename events?
firePropertyChanged(this, changedFile, VirtualFile.PROP_NAME, oldName, newName);
}
}
}
private boolean hasPendingNotifications() {
return myPendingChanges.get();
}
}
}