/* * Copyright 2003-2015 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.extapi.persistence; import jetbrains.mps.project.AbstractModule; import jetbrains.mps.project.MPSExtentions; import jetbrains.mps.project.MementoWithFS; import jetbrains.mps.util.FileUtil; import jetbrains.mps.util.ModulePathConverter; import jetbrains.mps.util.annotation.ToRemove; import jetbrains.mps.vfs.FileSystemEvent; import jetbrains.mps.vfs.FileSystemListener; import jetbrains.mps.vfs.IFile; import jetbrains.mps.vfs.openapi.FileSystem; import jetbrains.mps.vfs.path.Path; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.annotations.Immutable; import org.jetbrains.mps.annotations.Internal; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.persistence.Memento; import org.jetbrains.mps.openapi.util.ProgressMonitor; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.unmodifiableList; import static jetbrains.mps.util.FileUtil.getAbsolutePath; import static jetbrains.mps.util.FileUtil.getUnixPath; import static jetbrains.mps.util.PathConverters.ID_CONVERTER; import static jetbrains.mps.util.PathConverters.forModules; /** * <code>FileBasedModelRoot</code> contains several {@link SourceRoot} which contain models. * The source roots might be marked with a {@link SourceRootKind} which determine how do we treat the model files/folders * we discover under those source roots. * * The class is in the state of migration from <code>String</code> source roots to the interface {@link SourceRoot} * that is why it is such a mess. * * Paths represented by string either must have a clear contract (absolute, relative) or (better) * replaced with some <code>Path</code> entities. * AP * * @author evgeny, 12/11/12 * apyshkin, 15/12/16 */ public abstract class FileBasedModelRoot extends ModelRootBase implements FileSystemListener { /** * @deprecated use {@link SourceRootKinds#SOURCES} instead */ @Deprecated public static final String SOURCE_ROOTS = "sourceRoot"; /** * @deprecated use {@link SourceRootKinds#EXCLUDED} instead */ @Deprecated public static final String EXCLUDED = "excluded"; private /*final*/ FileSystem myFileSystem = jetbrains.mps.vfs.FileSystem.getInstance(); // TODO not read from memento /** * This is a private model root persistence notation, ought to be concealed from the general public */ @Internal public static final String CONTENT_PATH = "contentPath"; // TODO: 12/20/16 lower visibility @Internal public static final String LOCATION = "location"; // TODO: 12/20/16 lower visibility private static final String PATH = "path"; @ToRemove(version = 3.5) @Deprecated @Nullable @Immutable private final List<String> mySupportedFileKinds; // null <=> default constructor is used /** * Ancestor for all the source paths * Commonly it is a module root folder and 'models' directory is its default source root * @see SourceRoot */ private IFile myContentDir; // might be null when just created private final SourcePaths mySourcePathStorage; private final List<PathListener> myListeners = new ArrayList<>(); protected FileBasedModelRoot() { mySupportedFileKinds = null; mySourcePathStorage = new SourcePaths(); } /** * @deprecated use {@link #FileBasedModelRoot()} instead and * define your own {@link #getSupportedFileKinds1()} */ @Deprecated @ToRemove(version = 3.5) protected FileBasedModelRoot(String[] supportedFileKinds) { mySupportedFileKinds = supportedFileKinds != null ? unmodifiableList(asList(supportedFileKinds)) : emptyList(); mySourcePathStorage = new SourcePaths((sourceRootKind) -> getSupportedFileKinds1().contains(sourceRootKind)); } /** * @deprecated use {@link #getContentDirectory()} instead */ @Deprecated @Nullable public final String getContentRoot() { return myContentDir != null ? myContentDir.getPath() : null; } /** * @deprecated use {@link #setContentDirectory(IFile)} instead */ @Deprecated public final void setContentRoot(@NotNull String path) { checkNotRegistered(); myContentDir = myFileSystem.getFile(path); } @Nullable public final IFile getContentDirectory() { return myContentDir; } public final void setContentDirectory(@NotNull IFile contentDir) { checkNotRegistered(); myContentDir = contentDir; } @NotNull public final FileSystem getFileSystem() { return myFileSystem; } /** * To become abstract in the 3.5 */ @NotNull @Immutable public /*abstract*/ List<SourceRootKind> getSupportedFileKinds1() { return unmodifiableList(asList(SourceRootKinds.values())); } /** * @return <code>SourceRoot</code>s of the specified kind * They might contain relative paths (unlike the legacy counterpart method!!). * FBModelRoot is going to store relative path, all we need is * some api to provide relative path instances. * Now we do not have such abstraction since <code>IFile</code> * is effectively absolute (just since the idea's <code>VirtualFile</code> is absolute as well). * * AP */ @NotNull @Immutable public final List<SourceRoot> getSourceRoots(@NotNull SourceRootKind kind) { return mySourcePathStorage.getByKind(kind); } public final void addSourceRoot(@NotNull SourceRootKind kind, @NotNull SourceRoot root) { mySourcePathStorage.addSourceRoot(kind, root); } /** * @return the <code>FileKind</code> of the removed <code>SourceRoot</code> if it was successfully removed. */ @Nullable public final SourceRootKind removeSourceRoot(@NotNull SourceRoot root) { return mySourcePathStorage.removeSourceRoot(root); } /** * helper method (for legacy resolve) */ @NotNull private SourceRootKind resolveKindByName(@NotNull String kindName) { for (SourceRootKind kind : getSupportedFileKinds1()) { if (kind.getName().equals(kindName)) { return kind; } } throw new FileKindIsNotAllowedException(kindName); } /** * @deprecated <code>String</code> is not the best choice. Consider using {@link #getSupportedFileKinds1()} * @see SourcePaths */ @Deprecated @Immutable public final Collection<String> getSupportedFileKinds() { List<String> legacyFileKinds = mySupportedFileKinds; if (legacyFileKinds != null) { return legacyFileKinds; } else { return getSupportedFileKinds1().stream() .map(SourceRootKind::getName) .collect(Collectors.toList()); } } /** * @deprecated use {@link #getSourceRoots(SourceRootKind)} instead */ @NotNull @Deprecated public final Collection<String> getFiles(@NotNull String kind) { List<SourceRoot> roots = getSourceRoots(resolveKindByName(kind)); return unmodifiableList(roots.stream() .map(SourceRoot::getAbsolutePath) // unfortunately I am sure that plenty clients rely on the absolute path here. .map(IFile::getPath) .collect(Collectors.toList())); } /** * @deprecated use {@link #getSourceRoots(SourceRootKind)} + {@link List#contains}. */ @Deprecated @ToRemove(version = 0) public final boolean containsFile(String kind, String file) { Collection<String> sourceRoots = getFiles(kind); return sourceRoots.contains(file); } /** * @deprecated use {@link #addSourceRoot(SourceRootKind, SourceRoot)} instead */ @Deprecated @ToRemove(version = 3.5) public final void addFile(String kind, String filePath) { SourceRootKind sourceRootKind = resolveKindByName(kind); addSourceRoot(sourceRootKind, new DefaultSourceRoot(filePath, myContentDir)); } /** * @deprecated use {@link #removeSourceRoot(SourceRoot)} instead */ @Deprecated @ToRemove(version = 3.5) public final void deleteFile(String kind, String file) { checkNotRegistered(); if (!containsFile(kind, file)) { throw new FileKindIsNotAllowedException(kind, file); } removeSourceRoot(new DefaultSourceRoot(file, myContentDir)); } @Override public final String getPresentation() { IFile contentDirectory = getContentDirectory(); return contentDirectory == null ? "no content dir" : contentDirectory.getPath(); } @Override public void save(@NotNull Memento memento) { if (myContentDir != null) { memento.put(CONTENT_PATH, myContentDir.getPath()); } memento.put("type", getType()); for (SourceRootKind kind : getSupportedFileKinds1()) { for (SourceRoot root : getSourceRoots(kind)) { Memento modelRootMemento = memento.createChild(kind.getName()); String contentRootPath = root.getAbsolutePath().getPath(); // must go away as soon as we allow relative paths if (FileUtil.isAncestor(myContentDir.getPath(), contentRootPath)) { String relPath = relativize(contentRootPath, myContentDir); if (relPath.isEmpty()) { relPath = MPSExtentions.DOT; } modelRootMemento.put(LOCATION, relPath); } else { modelRootMemento.put(PATH, contentRootPath); } } } } @Override public void load(@NotNull Memento memento) { checkNotRegistered(); mySourcePathStorage.clearAll(); // AP: I'd rather force a single invocation of the #load method if (memento instanceof MementoWithFS) { myFileSystem = ((MementoWithFS) memento).getFileSystem(); } String path = memento.get(CONTENT_PATH); myContentDir = (path != null) ? myFileSystem.getFile(path) : null; for (SourceRootKind kind : getSupportedFileKinds1()) { for (Memento root : memento.getChildren(kind.getName())) { String relPath = root.get(LOCATION); if (relPath != null) { addSourceRoot(kind, new DefaultSourceRoot(relPath, myContentDir)); // relative } else { addSourceRoot(kind, new DefaultSourceRoot(root.get(PATH), myContentDir)); // absolute } } } } @Override public void attach() { super.attach(); attachPathListenerForEachSourceRoot(); } private void attachPathListenerForEachSourceRoot() { // fixme extract class getSupportedFileKinds1().stream().filter(kind -> !kind.isExcluded()).forEach(kind -> { for (SourceRoot sourceRoot : getSourceRoots(kind)) { IFile file = sourceRoot.getAbsolutePath(); PathListener listener = new PathListener(file); myListeners.add(listener); myFileSystem.addListener(listener); } }); } @Override public void dispose() { for (PathListener listener : myListeners) { myFileSystem.removeListener(listener); } myListeners.clear(); super.dispose(); } @Override public final IFile getFileToListen() { throw new UnsupportedOperationException(); } @Override public Iterable<FileSystemListener> getListenerDependencies() { if (getModule() instanceof FileSystemListener) { return Collections.singleton((FileSystemListener) getModule()); } return null; } @Override public void update(ProgressMonitor monitor, @NotNull FileSystemEvent event) { if (!event.getCreated().isEmpty() || !event.getRemoved().isEmpty()) { update(); } } /** * Sets the same content root to the target model root * Adds the corresponding files to the target model root * If the content root is out of the module directory location then * the exception is thrown (since we have no idea which location for the copy we need to choose) * * @see #setContentRoot(String) * @see #addFile */ protected final void copyContentRootAndFiles(@NotNull FileBasedModelRoot targetModelRoot) throws CopyNotSupportedException { AbstractModule source = (AbstractModule) getModule(); AbstractModule target = (AbstractModule) targetModelRoot.getModule(); if (source == null) { throw new CopyNotSupportedException("The module of the source model root is null " + this); } if (target == null) { throw new CopyNotSupportedException("The module of the target model root is null " + targetModelRoot); } if (getContentDirectory() != null) { IFile targetContentDir; if (isUnderModuleSourceDir(source, getContentDirectory())) { String relFromModuleDirToContentDir = relativize(getContentDirectory().getPath(), source.getModuleSourceDir()); targetContentDir = target.getModuleSourceDir().getDescendant(relFromModuleDirToContentDir); } else { throw new CopyNotSupportedException("The model root is not located under the module source directory " + this); } targetModelRoot.setContentDirectory(targetContentDir); for (SourceRootKind kind : getSupportedFileKinds1()) { for (SourceRoot sourceRoot : getSourceRoots(kind)) { String relativePath = relativize(sourceRoot.getAbsolutePath().getPath(), getContentDirectory()); IFile descendant = targetContentDir.getDescendant(relativePath); String targetSourceRoot = descendant.getPath(); targetModelRoot.addSourceRoot(kind, new DefaultSourceRoot(targetSourceRoot, targetContentDir)); } } } } private static boolean isUnderModuleSourceDir(@NotNull AbstractModule module, @NotNull IFile path) { IFile moduleSourceDir = module.getModuleSourceDir(); return moduleSourceDir != null && FileUtil.isAncestor(moduleSourceDir.getPath(), path.getPath()); } @Override public final boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } FileBasedModelRoot that = (FileBasedModelRoot) o; return Objects.equals(myContentDir, that.myContentDir) && Objects.equals(mySourcePathStorage, that.mySourcePathStorage) && Objects.equals(myFileSystem, that.myFileSystem); } @Override public final int hashCode() { return Objects.hash(myContentDir, mySourcePathStorage); } @NotNull public static String relativize(@NotNull String fullPath, @NotNull IFile contentHome) { String contentHomePath = independentAndAbsolute(contentHome.getPath()); fullPath = independentAndAbsolute(fullPath); if (fullPath.equals(contentHomePath)) { return MPSExtentions.DOT; } return FileUtil.getRelativePath(fullPath, contentHomePath, Path.UNIX_SEPARATOR); } @NotNull private static String independentAndAbsolute(@NotNull String path) { return getUnixPath(getAbsolutePath(path)); } private final class PathListener implements FileSystemListener { private final IFile myPath; private PathListener(@NotNull IFile path) { myPath = path; } @NotNull @Override public IFile getFileToListen() { return myPath; } @Override public Iterable<FileSystemListener> getListenerDependencies() { return FileBasedModelRoot.this.getListenerDependencies(); } @Override public void update(ProgressMonitor monitor, @NotNull FileSystemEvent event) { event.notify(FileBasedModelRoot.this); } @Override public String toString() { return "[PathListener: path: " + myPath + "; modelRoot: " + FileBasedModelRoot.this + "]"; } } }