/*
* Copyright 2003-2012 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.util.annotation.ToRemove;
import jetbrains.mps.vfs.FileSystemEvent;
import jetbrains.mps.vfs.FileSystemListener;
import jetbrains.mps.vfs.IFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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.persistence.MultiStreamDataSource;
import org.jetbrains.mps.openapi.persistence.MultiStreamDataSourceListener;
import org.jetbrains.mps.openapi.util.ProgressMonitor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 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/4/12
*/
@ToRemove(version = 4.0)
public class FolderDataSource extends DataSourceBase implements MultiStreamDataSource, FileSystemListener, FileSystemBasedDataSource {
private final Object LOCK = new Object();
private List<DataSourceListener> myListeners = new ArrayList<DataSourceListener>();
@NotNull
private final IFile myFolder;
private final ModelRoot myModelRoot;
private long myLastAddRemove = -1;
public FolderDataSource(@NotNull IFile folder) {
this(folder, null);
}
/**
* @param modelRoot (optional) containing model root, which should be notified before the source during the update
*/
@ToRemove(version = 3.5)
@Deprecated
protected FolderDataSource(@NotNull IFile folder, @Nullable ModelRoot modelRoot) {
if (folder.exists() && !folder.isDirectory()) {
throw new IllegalArgumentException("Could not create FolderDataSource with regular file: " + folder);
}
this.myFolder = folder;
this.myModelRoot = modelRoot;
}
/**
* Returns true iff the file potentially contains some model data
*
* @return whether file is an actual source file
*/
public boolean isIncluded(@NotNull IFile file) {
return myFolder.equals(file.getParent());
}
protected Iterable<IFile> getStreams() {
return myFolder.getChildren();
}
protected String getStreamName(IFile file) {
return file.getName();
}
public IFile getFile(String streamName) {
return myFolder.getDescendant(streamName);
}
@NotNull
public IFile getFolder() {
return myFolder;
}
@Override
public boolean isReadOnly() {
return myFolder.isPackaged(); // !!! legacy
}
@NotNull
@Override
public String getLocation() {
return myFolder.toString();
}
@NotNull
@Override
public Iterable<String> getAvailableStreams() {
Set<String> names = new HashSet<String>();
for (IFile file : getStreams()) {
if (isIncluded(file)) {
names.add(getStreamName(file));
}
}
return names;
}
@NotNull
@Override
public InputStream openInputStream(String name) throws IOException {
IFile file = getFile(name);
if (file == null) {
throw new IOException("stream is not available");
}
return file.openInputStream();
}
@NotNull
@Override
public OutputStream openOutputStream(String name) throws IOException {
IFile file = getFile(name);
if (file == null) {
throw new IOException("stream is not available");
}
return file.openOutputStream();
}
@Override
public boolean delete(String name) {
IFile file = getFile(name);
return file != null && file.delete();
}
@Override
public long getTimestamp() {
long max = myLastAddRemove;
for (IFile file : getStreams()) {
if (!isIncluded(file)) {
continue;
}
long timestamp = file.lastModified();
if (timestamp > max) {
max = timestamp;
}
}
return max;
}
@Override
public void addListener(@NotNull DataSourceListener listener) {
synchronized (LOCK) {
if (myListeners.isEmpty()) {
myFolder.getFileSystem().addListener(this);
}
myListeners.add(listener);
}
}
@Override
public void removeListener(@NotNull DataSourceListener listener) {
synchronized (LOCK) {
myListeners.remove(listener);
if (myListeners.isEmpty()) {
myFolder.getFileSystem().removeListener(this);
}
}
}
@NotNull
@Override
public IFile getFileToListen() {
return myFolder;
}
@Override
public void delete() {
if (isReadOnly()) {
return;
}
ArrayList<IFile> toDelete = new ArrayList<IFile>();
for (IFile file : getStreams()) {
if (isIncluded(file)) {
toDelete.add(file);
}
}
for (IFile file : toDelete) {
file.delete();
}
myLastAddRemove = -1;
}
@Override
public Iterable<FileSystemListener> getListenerDependencies() {
if (myModelRoot instanceof FileSystemListener) {
return Collections.singleton((FileSystemListener) myModelRoot);
}
if (myModelRoot != null && myModelRoot.getModule() instanceof FileSystemListener) {
return Collections.singleton((FileSystemListener) myModelRoot.getModule());
}
return null;
}
@Override
public void update(ProgressMonitor monitor, @NotNull FileSystemEvent event) {
Set<String> affectedStreams = new HashSet<String>();
for (IFile file : event.getChanged()) {
if (isIncluded(file)) {
affectedStreams.add(getStreamName(file));
break;
}
}
for (IFile file : event.getCreated()) {
if (isIncluded(file)) {
affectedStreams.add(getStreamName(file));
myLastAddRemove = new Date().getTime();
break;
}
}
for (IFile file : event.getRemoved()) {
if (isIncluded(file)) {
affectedStreams.add(getStreamName(file));
myLastAddRemove = new Date().getTime();
break;
}
}
fireChanged(monitor, affectedStreams);
}
private void fireChanged(ProgressMonitor monitor, Iterable<String> streams) {
List<DataSourceListener> listeners;
synchronized (LOCK) {
listeners = new ArrayList<DataSourceListener>(myListeners);
}
monitor.start("Reloading", listeners.size());
try {
for (DataSourceListener l : listeners) {
if (l instanceof MultiStreamDataSourceListener) {
((MultiStreamDataSourceListener) l).changed(this, streams);
} else {
l.changed(this);
}
monitor.advance(1);
}
} finally {
monitor.done();
}
}
@NotNull
@Override
public Collection<IFile> getAffectedFiles() {
return Collections.singleton(myFolder);
}
@NotNull
@Override
public DataSourceType getType() {
return PreinstalledDataSourceTypes.FOLDER;
}
}