/* * 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.extapi.persistence.datasource.PreinstalledDataSourceTypes; import org.jetbrains.mps.openapi.persistence.datasource.DataSourceType; import jetbrains.mps.vfs.CachingFile; import jetbrains.mps.vfs.CachingFileSystem; import jetbrains.mps.vfs.FileSystemEvent; import jetbrains.mps.vfs.FileSystemExtPoint; import jetbrains.mps.vfs.FileSystemListener; import jetbrains.mps.vfs.IFile; import jetbrains.mps.vfs.DefaultCachingContext; import jetbrains.mps.vfs.openapi.FileSystem; import jetbrains.mps.vfs.path.Path; import org.jetbrains.annotations.NotNull; import org.jetbrains.mps.openapi.persistence.DataSource; import org.jetbrains.mps.openapi.persistence.DataSourceListener; import org.jetbrains.mps.openapi.persistence.ModelFactory; import org.jetbrains.mps.openapi.persistence.ModelRoot; import org.jetbrains.mps.openapi.util.ProgressMonitor; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; /** * Must be replaced with the FileDataSource everywhere. * Additional functionality (like #isIncluded) must be extracted or removed. * Remember: it is supposed to be just a simple notion of location with file system for {@link ModelFactory} * to load/save/create models there. * * @author apyshkin * evgeny, 11/3/12 */ public class FolderSetDataSource extends DataSourceBase implements DataSource, FileSystemListener, FileSystemBasedDataSource { private final ReadWriteLock myLock = new ReentrantReadWriteLock(); private final List<DataSourceListener> myListeners = new ArrayList<DataSourceListener>(4); private final Map<String, PathListener> myPaths = new LinkedHashMap<String, PathListener>(8); private final Set<FileSystemListener> myListenerDependencies = new HashSet<FileSystemListener>(8); public FolderSetDataSource() { } /** * @param modelRoot (optional) containing model root, which should be notified before the source during the update */ public void addPath(@NotNull IFile path, ModelRoot modelRoot) { myLock.writeLock().lock(); try { if (myPaths.containsKey(path.getPath())) { return; } if (modelRoot instanceof FileSystemListener) { myListenerDependencies.add((FileSystemListener) modelRoot); } else if (modelRoot != null && modelRoot.getModule() instanceof FileSystemListener) { myListenerDependencies.add((FileSystemListener) modelRoot.getModule()); } PathListener listener = new PathListener(path, this); myPaths.put(path.getPath(), listener); if (!(myListeners.isEmpty())) { path.getFileSystem().addListener(listener); } } finally { myLock.writeLock().unlock(); } } public Collection<String> getPaths() { myLock.readLock().lock(); try { return new ArrayList<>(myPaths.keySet()); } finally { myLock.readLock().unlock(); } } private Collection<IFile> getFiles() { myLock.readLock().lock(); try { Collection<IFile> rv = new ArrayList<IFile>(myPaths.size()); for (PathListener l : myPaths.values()) { rv.add(l.myFile); } return rv; } finally { myLock.readLock().unlock(); } } @Override public void refresh() { FileSystem fs = getFS(); if (fs instanceof CachingFileSystem) { Set<CachingFile> collect = getFiles().stream().filter(file -> file instanceof CachingFile).map(file -> (CachingFile) file).collect(Collectors.toSet()); ((CachingFileSystem) fs).refresh(new DefaultCachingContext(true, false), collect); } } @Override public long getTimestamp() { long max = -1; Collection<IFile> paths = getFiles(); for (IFile path : paths) { String fsPath = path.getPath(); //at least some programs don't change timestamp of a directory inside jar file after deleting a file in it if (fsPath.contains(Path.ARCHIVE_SEPARATOR)){ IFile jarFile = path.getFileSystem().getFile(fsPath.substring(0, fsPath.lastIndexOf(Path.ARCHIVE_SEPARATOR))); if (jarFile != null){ max = Math.max(max, jarFile.lastModified()); continue; // no need to go deep into jar contents } } long ts = getTimestampRecursive(path); max = Math.max(max, ts); } return max; } @Override public boolean isReadOnly() { return false; } @Override public void delete() { Collection<IFile> toDelete = getFiles(); for (IFile f : toDelete) { f.delete(); } } private FileSystem getFS() { List<IFile> toRefresh = new ArrayList<>(getFiles()); if (toRefresh.isEmpty()) return FileSystemExtPoint.getFS(); return toRefresh.get(0).getFileSystem(); } @NotNull @Override public String getLocation() { return "Folders(" + Arrays.toString(getPaths().toArray()) + ")"; } @Override public void addListener(@NotNull DataSourceListener listener) { myLock.writeLock().lock(); try { if (myListeners.isEmpty()) { for (PathListener pathListener : myPaths.values()) { getFS().addListener(pathListener); } } myListeners.add(listener); } finally { myLock.writeLock().unlock(); } } @Override public void removeListener(@NotNull DataSourceListener listener) { myLock.writeLock().lock(); try { myListeners.remove(listener); if (myListeners.isEmpty()) { for (PathListener pathListener : myPaths.values()) { getFS().removeListener(pathListener); } } } finally { myLock.writeLock().unlock(); } } private List<DataSourceListener> getDataSourceListeners() { List<DataSourceListener> listeners; myLock.readLock().lock(); try { listeners = new ArrayList<DataSourceListener>(myListeners); } finally { myLock.readLock().unlock(); } return listeners; } @Override public IFile getFileToListen() { throw new UnsupportedOperationException(); } @Override public Iterable<FileSystemListener> getListenerDependencies() { myLock.readLock().lock(); try { return new ArrayList<FileSystemListener>(myListenerDependencies); } finally { myLock.readLock().unlock(); } } @Override public void update(ProgressMonitor monitor, @NotNull FileSystemEvent event) { fireChanged(monitor); } private void fireChanged(ProgressMonitor monitor) { List<DataSourceListener> listeners = getDataSourceListeners(); monitor.start("Reloading", listeners.size()); try { for (DataSourceListener l : listeners) { l.changed(this); monitor.advance(1); } } finally { monitor.done(); } } private static long getTimestampRecursive(IFile path) { long max = path.lastModified(); if (path.isDirectory()) { for (IFile child : path.getChildren()) { long timestamp = getTimestampRecursive(child); if (timestamp > max) { max = timestamp; } } } return max; } @NotNull @Override public Collection<IFile> getAffectedFiles() { return getFiles(); } @NotNull @Override public DataSourceType getType() { return PreinstalledDataSourceTypes.FOLDER_SET; } private static class PathListener implements FileSystemListener { private final IFile myFile; private final FileSystemListener myDelegate; private PathListener(@NotNull IFile path, FileSystemListener delegate) { myFile = path; myDelegate = delegate; } @NotNull @Override public IFile getFileToListen() { return myFile; } @Override public Iterable<FileSystemListener> getListenerDependencies() { return myDelegate.getListenerDependencies(); } @Override public void update(ProgressMonitor monitor, @NotNull FileSystemEvent event) { event.notify(myDelegate); } } }