/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.repository.gui;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.event.EventListenerList;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.repository.DataEntry;
import com.rapidminer.repository.Entry;
import com.rapidminer.repository.Folder;
import com.rapidminer.repository.Repository;
import com.rapidminer.repository.RepositoryException;
import com.rapidminer.repository.RepositoryListener;
import com.rapidminer.repository.RepositoryLocation;
import com.rapidminer.repository.RepositoryManager;
import com.rapidminer.repository.RepositorySortingMethod;
import com.rapidminer.repository.RepositoryTools;
import com.rapidminer.tools.I18N;
import com.rapidminer.tools.LogService;
import com.rapidminer.tools.Observable;
import com.rapidminer.tools.Observer;
/**
* Model representing {@link Entry}s as a tree.
*
* @author Simon Fischer , Denis Schernov, Marcel Seifert
*
*/
public class RepositoryTreeModel implements TreeModel {
// Since we want the repository entries to be sorted alphanumeric we have to manipulate the
// methods getChild and getChildIndex. If we wouldn't cache the sorted entries we would have to
// sort the entries every time one of these methods is called.
// We're using a HashMap to cache the location of folders as keys while storing their subfolders
// and entries as their values in a sorted list.
private final HashMap<RepositoryLocation, List<Entry>> sortedRepositoryEntriesHashMap = new HashMap<>();
private static final String PENDING_FOLDER_NAME = "Pending...";
private final RepositoryManager root;
private final EventListenerList listeners = new EventListenerList();
private JTree parentTree = null;
private final RepositoryListener repositoryListener = new RepositoryListener() {
private TreeModelEvent makeChangeEvent(Entry entry) {
TreePath path = getPathTo(entry.getContainingFolder());
int index;
if (entry instanceof Repository) {
index = RepositoryManager.getInstance(null).getRepositories().indexOf(entry);
} else {
index = getIndexOfChild(entry.getContainingFolder(), entry);
}
return new TreeModelEvent(RepositoryTreeModel.this, path, new int[] { index }, new Object[] { entry });
}
@Override
public void entryAdded(final Entry newEntry, final Folder parent) {
// If there is already a sorted list of the entries of the parentfolder, the key and the
// list will be deleted so it will be recached with the new entry in the
// updateCachedRepositoryEntries method.
if (sortedRepositoryEntriesHashMap.containsKey(parent.getLocation())) {
sortedRepositoryEntriesHashMap.remove(parent.getLocation());
}
// fire event
final TreeModelEvent e = makeChangeEvent(newEntry);
SwingTools.invokeAndWait(new Runnable() {
@Override
public void run() {
for (TreeModelListener l : listeners.getListeners(TreeModelListener.class)) {
l.treeNodesInserted(e);
}
}
});
}
@Override
public void entryRemoved(final Entry removedEntry, final Folder parent, final int index) {
// If there is already a sorted list of the entries of the parentfolder, the key and the
// list will be deleted so it will be recached without the deleted entry in the
// updateCachedRepositoryEntries method.
if (sortedRepositoryEntriesHashMap.containsKey(parent.getLocation())) {
sortedRepositoryEntriesHashMap.remove(parent.getLocation());
}
// Save path of parent
final RepositoryTreeUtil treeUtil = new RepositoryTreeUtil();
TreePath parentPath = getPathTo(parent);
treeUtil.saveSelectionPath(parentPath);
// Fire event
final TreeModelEvent p = makeChangeEvent(removedEntry);
final TreeModelEvent e = new TreeModelEvent(RepositoryTreeModel.this, parentPath, new int[] { index },
new Object[] { removedEntry });
SwingTools.invokeAndWait(new Runnable() {
@Override
public void run() {
for (TreeModelListener l : listeners.getListeners(TreeModelListener.class)) {
l.treeNodesRemoved(e);
l.treeStructureChanged(p);
}
}
});
// Restore selected path / expansion state of parent
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
treeUtil.restoreSelectionPaths(parentTree);
}
});
}
@Override
public void entryChanged(final Entry entry) {
// If there is already a sorted list of the entries of the parentfolder, the key and the
// list will be deleted so it will be recached with the changed entry in the
// updateCachedRepositoryEntries method.
Folder parent = entry.getContainingFolder();
if (parent != null) {
if (sortedRepositoryEntriesHashMap.containsKey(parent.getLocation())) {
sortedRepositoryEntriesHashMap.remove(parent.getLocation());
}
}
// fire event
final TreeModelEvent p = entry != null ? makeChangeEvent(entry) : null;
final TreeModelEvent e = makeChangeEvent(entry);
final RepositoryTreeUtil treeUtil = new RepositoryTreeUtil();
if (parentTree != null) {
treeUtil.saveExpansionState(parentTree);
}
SwingTools.invokeAndWait(new Runnable() {
@Override
public void run() {
for (TreeModelListener l : listeners.getListeners(TreeModelListener.class)) {
l.treeNodesChanged(e);
if (p != null) {
l.treeStructureChanged(p);
}
}
}
});
if (parentTree != null) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
treeUtil.restoreExpansionState(parentTree);
}
});
}
}
@Override
public void folderRefreshed(final Folder folder) {
final RepositoryTreeUtil treeUtil = new RepositoryTreeUtil();
final TreeModelEvent e = makeChangeEvent(folder);
SwingTools.invokeAndWait(new Runnable() {
@Override
public void run() {
if (parentTree != null) {
treeUtil.saveExpansionState(parentTree);
}
for (TreeModelListener l : listeners.getListeners(TreeModelListener.class)) {
l.treeStructureChanged(e);
}
treeUtil.locateExpandedEntries();
}
});
if (parentTree != null) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
treeUtil.restoreExpansionState(parentTree);
}
});
}
sortedRepositoryEntriesHashMap.clear();
}
};
private boolean onlyFolders = false;
private boolean onlyWriteableRepositories = false;
RepositorySortingMethod sortingMethod = RepositorySortingMethod.NAME_ASC;
public RepositoryTreeModel(final RepositoryManager root) {
this(root, false, false);
}
public RepositoryTreeModel(final RepositoryManager root, final boolean onlyFolders,
final boolean onlyWritableRepositories) {
this.root = root;
this.onlyFolders = onlyFolders;
this.onlyWriteableRepositories = onlyWritableRepositories;
for (Repository repository : root.getRepositories()) {
repository.addRepositoryListener(repositoryListener);
}
root.addObserver(new Observer<Repository>() {
@Override
public void update(Observable<Repository> observable, Repository arg) {
for (Repository repository : root.getRepositories()) {
repository.removeRepositoryListener(repositoryListener);
repository.addRepositoryListener(repositoryListener);
}
final TreeModelEvent e = new TreeModelEvent(this, new TreePath(root));
if (SwingUtilities.isEventDispatchThread()) {
for (TreeModelListener l : listeners.getListeners(TreeModelListener.class)) {
l.treeStructureChanged(e);
}
} else {
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
for (TreeModelListener l : listeners.getListeners(TreeModelListener.class)) {
l.treeStructureChanged(e);
}
}
});
} catch (InvocationTargetException | InterruptedException ex) {
LogService.getRoot().log(Level.WARNING, "com.rapidminer.repository.gui.edt_event", ex);
}
}
}
}, true);
}
TreePath getPathTo(Entry entry) {
return RepositoryTreeModel.getPathTo(entry, root);
}
/**
* Gets a tree path based on an entry.
*
* @param entry
* The entry for which a path should be determined
* @param repositoryManager
* The manager is used as the root of a tree path
* @return A tree path.
*/
public static TreePath getPathTo(Entry entry, RepositoryManager repositoryManager) {
if (entry == null) {
return new TreePath(repositoryManager);
} else if (entry.getContainingFolder() == null) {
return new TreePath(repositoryManager).pathByAddingChild(entry);
} else {
return getPathTo(entry.getContainingFolder(), repositoryManager).pathByAddingChild(entry);
}
}
@Override
public void addTreeModelListener(TreeModelListener l) {
listeners.add(TreeModelListener.class, l);
}
@Override
public void removeTreeModelListener(TreeModelListener l) {
listeners.remove(TreeModelListener.class, l);
}
public void setParentTree(JTree tree) {
parentTree = tree;
}
/**
* Returns the Object which is contained by the parent and has the wanted index while the child
* will be determinate by a sorted list of the children of the parent. If there is no data known
* an empty string will be returned and if the folder is blocking the data for now
* PENDING_FOLDER_NAME will be returned.
*
* @param parent
* The parent {@link Repository}, {@link Folder}, {@link Entry} etc. of the child.
* @param index
* Refers to the index of the child you want to get.
* @return The child with the parent at the index.
*/
@Override
public Object getChild(Object parent, int index) {
if (parent instanceof RepositoryManager) {
if (onlyWriteableRepositories) {
return getWritableRepositories((RepositoryManager) parent).get(index);
}
return ((RepositoryManager) parent).getRepositories().get(index);
} else if (parent instanceof Folder) {
Folder folder = (Folder) parent;
if (folder.willBlock()) {
unblock(folder);
return PENDING_FOLDER_NAME;
} else {
try {
if (!sortedRepositoryEntriesHashMap.keySet().contains(folder.getLocation())) {
updateCachedRepositoryEntries(folder);
}
int numFolders = folder.getSubfolders().size();
if (index < numFolders) {
if (!sortedRepositoryEntriesHashMap.keySet().contains(folder.getLocation())
|| sortedRepositoryEntriesHashMap.get(folder.getLocation()).size() <= index) {
updateCachedRepositoryEntries(folder);
}
return sortedRepositoryEntriesHashMap.get(folder.getLocation()).get(index);
} else if (onlyFolders) {
return null;
} else {
if (folder.getDataEntries().size() > index - numFolders) {
if (!sortedRepositoryEntriesHashMap.keySet().contains(folder.getLocation())
|| sortedRepositoryEntriesHashMap.get(folder.getLocation()).size() <= index) {
updateCachedRepositoryEntries(folder);
}
return sortedRepositoryEntriesHashMap.get(folder.getLocation()).get(index);
} else {
// In this case and at this state, no data entry is known.
// Returning null would cause a NPE.
// This solution prevents from an IndexOutOfBoundsException inside EDT.
return "";
}
}
} catch (RepositoryException e) {
LogService.getRoot().log(Level.WARNING,
I18N.getMessage(LogService.getRoot().getResourceBundle(),
"com.rapidminer.repository.gui.RepositoryTreeModel.getting_children_of_folder_error",
folder.getName(), e),
e);
return null;
}
}
} else {
return null;
}
}
private final Set<Folder> pendingFolders = new HashSet<>();
private final Set<Folder> brokenFolders = new HashSet<>();
/**
* Asynchronously fetches data from the folder so it will no longer block and then notifies
* listeners on the EDT.
*/
private void unblock(final Folder folder) {
if (pendingFolders.contains(folder)) {
return;
}
pendingFolders.add(folder);
new Thread("wait-for-" + folder.getName()) {
@Override
public void run() {
final List<Entry> children = new ArrayList<>();
final AtomicBoolean folderBroken = new AtomicBoolean(false);
try {
List<Folder> subfolders = folder.getSubfolders();
children.addAll(subfolders); // this may take some time
children.addAll(folder.getDataEntries()); // this may take some time
} catch (Exception e) {
// this occurs for example if the remote repository is unreachable
folderBroken.set(true);
brokenFolders.add(folder);
SwingTools.showSimpleErrorMessage("error_fetching_folder_contents_from_repository", e);
} finally {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
try {
TreeModelEvent removeEvent = new TreeModelEvent(RepositoryTreeModel.this, getPathTo(folder),
new int[] { 0 }, new Object[] { PENDING_FOLDER_NAME });
for (TreeModelListener l : listeners.getListeners(TreeModelListener.class)) {
l.treeNodesRemoved(removeEvent);
}
int index[] = new int[children.size()];
for (int i = 0; i < index.length; i++) {
index[i] = i;
}
Object[] childArray = children.toArray();
if (childArray.length > 0) {
TreeModelEvent insertEvent = new TreeModelEvent(RepositoryTreeModel.this,
getPathTo(folder), index, childArray);
for (TreeModelListener l : listeners.getListeners(TreeModelListener.class)) {
l.treeNodesInserted(insertEvent);
}
}
} finally {
if (!folderBroken.get()) {
pendingFolders.remove(folder);
brokenFolders.remove(folder);
}
}
}
});
}
}
}.start();
}
@Override
public int getChildCount(Object parent) {
if (parent instanceof RepositoryManager) {
if (onlyWriteableRepositories) {
return getWritableRepositories((RepositoryManager) parent).size();
}
return ((RepositoryManager) parent).getRepositories().size();
} else if (parent instanceof Folder) {
Folder folder = (Folder) parent;
if (folder.willBlock()) {
unblock(folder);
// folder is broken and has no children
if (brokenFolders.contains(folder)) {
return 0;
}
return 1; // "Pending...."
} else {
try {
if (onlyFolders) {
return folder.getSubfolders().size();
} else {
return folder.getSubfolders().size() + folder.getDataEntries().size();
}
} catch (RepositoryException e) {
LogService.getRoot().log(Level.WARNING,
I18N.getMessage(LogService.getRoot().getResourceBundle(),
"com.rapidminer.repository.gui.RepositoryTreeModel.getting_children_count_of_folder_error",
folder.getName(), e),
e);
return 0;
}
}
} else {
return 0;
}
}
/**
* This method will return the index of the child which is contained by parent.
*
* @param parent
* Parent object (directory) of the child
* @param child
* Child object which index will be returned
* @return The index of the child which is in the parent directory.
*/
@Override
public int getIndexOfChild(Object parent, Object child) {
if (parent instanceof RepositoryManager) {
if (onlyWriteableRepositories) {
return getWritableRepositories((RepositoryManager) parent).indexOf(child);
}
return ((RepositoryManager) parent).getRepositories().indexOf(child);
} else if (parent instanceof Folder) {
// don't return -1 for index of pending "folder" (for blocking folder requests)
if (PENDING_FOLDER_NAME.equals(child)) {
return 0;
}
Folder folder = (Folder) parent;
try {
if (!sortedRepositoryEntriesHashMap.keySet().contains(folder.getLocation())) {
updateCachedRepositoryEntries(folder);
}
if (child instanceof Folder || child instanceof Entry) {
if (folder.getSubfolders().contains(child) || folder.getDataEntries().contains(child)) {
if (!sortedRepositoryEntriesHashMap.keySet().contains(folder.getLocation())
|| !sortedRepositoryEntriesHashMap.get(folder.getLocation()).contains(child)) {
updateCachedRepositoryEntries(folder);
}
return sortedRepositoryEntriesHashMap.get(folder.getLocation()).indexOf(child);
}
} else {
return -1;
}
} catch (RepositoryException e) {
LogService.getRoot().log(Level.WARNING,
I18N.getMessage(LogService.getRoot().getResourceBundle(),
"com.rapidminer.repository.gui.RepositoryTreeModel.getting_child_index_of_folder_error",
folder.getName(), e),
e);
return -1;
}
} else {
return -1;
}
return 0;
}
/**
* This method will update the cached HashMap depending on the key which is the parameter parent
* more specifically its path.
*
* @param parent
* determinate which part of the cached HashMap has to be updated
* @throws RepositoryException
*/
private void updateCachedRepositoryEntries(Folder parent) throws RepositoryException {
sortedRepositoryEntriesHashMap.remove(parent.getLocation());
List<Folder> sortedFolders = new ArrayList<>(parent.getSubfolders());
List<DataEntry> sortedEntries = new ArrayList<>(parent.getDataEntries());
// sort entries depending on sorting method
if (sortingMethod == RepositorySortingMethod.LAST_MODIFIED_DATE_DESC) {
Collections.sort(sortedEntries, RepositoryTools.ENTRY_COMPARATOR_LAST_MODIFIED);
Collections.sort(sortedFolders, RepositoryTools.ENTRY_COMPARATOR_LAST_MODIFIED);
} else {
Collections.sort(sortedEntries, RepositoryTools.ENTRY_COMPARATOR);
Collections.sort(sortedFolders, RepositoryTools.ENTRY_COMPARATOR);
}
List<Entry> newList = new ArrayList<>(sortedFolders);
newList.addAll(sortedEntries);
sortedRepositoryEntriesHashMap.put(parent.getLocation(), newList);
}
@Override
public Object getRoot() {
return root;
}
@Override
public boolean isLeaf(Object node) {
return !(node instanceof Folder) && !(node instanceof RepositoryManager);
}
@Override
public void valueForPathChanged(TreePath path, Object newValue) {
try {
((Entry) path.getLastPathComponent()).rename(newValue.toString());
} catch (Exception e) {
SwingTools.showSimpleErrorMessage("error_rename", e, e.toString());
}
}
private List<Repository> getWritableRepositories(RepositoryManager manager) {
List<Repository> repositories = manager.getRepositories();
List<Repository> writeableRepositories = new ArrayList<>();
for (Repository repository : repositories) {
if (!repository.isReadOnly()) {
writeableRepositories.add(repository);
}
}
return writeableRepositories;
}
/**
* Sets the {@link RepositorySortingMethod} with which this {@link RepositoryTreeModel} is
* sorted
*
* @param method
* The {@link RepositorySortingMethod}
* @since 7.4
*/
void setSortingMethod(RepositorySortingMethod method) {
sortingMethod = method;
sortedRepositoryEntriesHashMap.clear();
// Save expansion state and notify listeners to prevent GUI misbehavior
final RepositoryTreeUtil treeUtil = new RepositoryTreeUtil();
if (parentTree != null) {
treeUtil.saveExpansionState(parentTree);
}
TreeModelEvent e = new TreeModelEvent(RepositoryTreeModel.this, getPathTo(null), new int[] { 0 },
new Object[] { null });
for (TreeModelListener l : listeners.getListeners(TreeModelListener.class)) {
l.treeStructureChanged(e);
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
treeUtil.restoreSelectionPaths(parentTree);
}
});
}
/**
* Gets the {@link RepositorySortingMethod} with which this {@link RepositoryTreeModel} is
* sorted
*
* @since 7.4
*/
RepositorySortingMethod getSortingMethod() {
return sortingMethod;
}
}