/**
* 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.gui.processeditor;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import com.rapidminer.RapidMiner;
import com.rapidminer.gui.tools.CamelCaseFilter;
import com.rapidminer.operator.OperatorDescription;
import com.rapidminer.operator.ProcessRootOperator;
import com.rapidminer.tools.GroupTree;
import com.rapidminer.tools.OperatorService;
import com.rapidminer.tools.OperatorService.OperatorServiceListener;
import com.rapidminer.tools.documentation.OperatorDocBundle;
import com.rapidminer.tools.usagestats.ActionStatisticsCollector;
/**
* This is the model for the group selection tree in the new operator editor panel.
*
* @author Ingo Mierswa, Tobias Malbrecht, Sebastian Land
*/
public class NewOperatorGroupTreeModel implements TreeModel, OperatorServiceListener {
/** Regular expression used as delimiter for search terms. */
private static final String SEARCH_TERM_DELIMITER = "\\s+";
/** Consider only the first {@value} search terms. */
private static final int MAX_SEARCH_TERMS = 5;
/** Compares operator descriptions based on their usage statistics. */
private static final class UsageStatsComparator implements Comparator<OperatorDescription>, Serializable {
private static final long serialVersionUID = 1L;
@Override
public int compare(OperatorDescription op1, OperatorDescription op2) {
int usageCount1 = (int) ActionStatisticsCollector.getInstance().getCount(ActionStatisticsCollector.TYPE_OPERATOR,
op1.getKey(), ActionStatisticsCollector.OPERATOR_EVENT_EXECUTION);
int usageCount2 = (int) ActionStatisticsCollector.getInstance().getCount(ActionStatisticsCollector.TYPE_OPERATOR,
op2.getKey(), ActionStatisticsCollector.OPERATOR_EVENT_EXECUTION);
return usageCount2 - usageCount1;
}
}
private static final class OperatorPriorityComparator implements Comparator<OperatorDescription> {
@Override
public int compare(OperatorDescription o1, OperatorDescription o2) {
try {
return Math.negateExact(Integer.compare(o1.getPriority(), o2.getPriority()));
} catch (ArithmeticException e) {
// no logging for idiotic error
return 0;
}
}
}
private final GroupTree completeTree;
private GroupTree displayedTree;
private boolean filterDeprecated = true;
private String filter = null;
/** The list of all tree model listeners. */
private final List<TreeModelListener> treeModelListeners = new LinkedList<>();
private boolean sortByUsage = false;
/** sorts by <priority> key of operators */
private Comparator<OperatorDescription> priorityComparator = new OperatorPriorityComparator();
/** sorts by local usage count of operators */
private Comparator<OperatorDescription> usageComparator = new UsageStatsComparator();
public NewOperatorGroupTreeModel() {
this.completeTree = OperatorService.getGroups();
OperatorService.addOperatorServiceListener(this);
removeHidden(completeTree);
this.displayedTree = this.completeTree;
this.filterDeprecated = true;
updateTree();
}
public void setFilterDeprecated(boolean filterDeprecated) {
this.filterDeprecated = filterDeprecated;
updateTree();
}
@Override
public void addTreeModelListener(TreeModelListener l) {
treeModelListeners.add(l);
}
public boolean contains(Object o) {
return contains(this.getRoot(), o);
}
private boolean contains(Object start, Object o) {
if (o.equals(start)) {
return true;
} else {
for (int i = 0; i < getChildCount(start); i++) {
if (contains(getChild(start, i), o)) {
return true;
}
}
}
return false;
}
@Override
public Object getChild(Object parent, int index) {
if (parent instanceof GroupTree) {
GroupTree tree = (GroupTree) parent;
int numSubGroups = tree.getSubGroups().size();
if (index < numSubGroups) {
return tree.getSubGroup(index);
} else {
return tree.getOperatorDescriptions().get(index - numSubGroups);
}
} else {
return null;
}
}
@Override
public int getChildCount(Object parent) {
if (parent instanceof GroupTree) {
GroupTree tree = (GroupTree) parent;
return tree.getSubGroups().size() + tree.getOperatorDescriptions().size();
} else {
return 0;
}
}
@Override
public int getIndexOfChild(Object parent, Object child) {
GroupTree tree = (GroupTree) parent;
if (child instanceof GroupTree) {
return tree.getIndexOfSubGroup((GroupTree) child);
} else {
return tree.getOperatorDescriptions().indexOf(child) + tree.getSubGroups().size();
}
}
@Override
public Object getRoot() {
return displayedTree;
}
@Override
public boolean isLeaf(Object node) {
return !(node instanceof GroupTree);
}
@Override
public void removeTreeModelListener(TreeModelListener l) {
treeModelListeners.remove(l);
}
/** Will be invoked after editing changes of nodes. */
@Override
public void valueForPathChanged(TreePath path, Object node) {
fireTreeChanged(node, path);
}
private void fireTreeChanged(Object source, TreePath path) {
Iterator<TreeModelListener> i = treeModelListeners.iterator();
while (i.hasNext()) {
i.next().treeStructureChanged(new TreeModelEvent(source, path));
}
}
private void fireCompleteTreeChanged(Object source) {
Iterator<TreeModelListener> i = treeModelListeners.iterator();
while (i.hasNext()) {
i.next().treeStructureChanged(new TreeModelEvent(this, new TreePath(getRoot())));
}
}
public List<TreePath> applyFilter(String filter) {
this.filter = filter;
return updateTree();
}
public List<TreePath> updateTree() {
// int hits = Integer.MAX_VALUE;
GroupTree filteredTree = this.completeTree.clone();
if (!"true".equals(System.getProperty(RapidMiner.PROPERTY_DEVELOPER_MODE))) {
removeDeprecatedGroup(filteredTree);
}
List<TreePath> expandedPaths = new LinkedList<>();
if (filter != null && filter.trim().length() > 0) {
String[] terms = filter.trim().split(SEARCH_TERM_DELIMITER, MAX_SEARCH_TERMS);
if (terms.length > 1) {
Arrays.setAll(terms, i -> terms[i].toLowerCase());
removeFilteredInstances(terms, filteredTree, expandedPaths, new TreePath(getRoot()));
} else {
CamelCaseFilter ccFilter = new CamelCaseFilter(filter);
removeFilteredInstances(ccFilter, filteredTree, expandedPaths, new TreePath(getRoot()));
}
}
if (filterDeprecated) {
removeDeprecated(filteredTree);
}
this.displayedTree = filteredTree;
filteredTree.sort(priorityComparator);
if (sortByUsage) {
filteredTree.sort(usageComparator);
}
fireCompleteTreeChanged(this);
return expandedPaths;
}
public GroupTree getNonDeprecatedGroupTree(GroupTree tree) {
GroupTree filteredTree = tree.clone();
removeDeprecated(filteredTree);
return filteredTree;
}
private void removeHidden(GroupTree tree) {
Iterator<? extends GroupTree> g = tree.getSubGroups().iterator();
while (g.hasNext()) {
GroupTree child = g.next();
removeHidden(child);
if (child.getAllOperatorDescriptions().size() == 0) {
g.remove();
}
}
Iterator<OperatorDescription> o = tree.getOperatorDescriptions().iterator();
while (o.hasNext()) {
OperatorDescription description = o.next();
if (description.getOperatorClass().equals(ProcessRootOperator.class)) {
o.remove();
}
}
}
private void removeDeprecatedGroup(GroupTree tree) {
Iterator<? extends GroupTree> g = tree.getSubGroups().iterator();
while (g.hasNext()) {
GroupTree child = g.next();
if (child.getKey().equals("deprecated")) {
g.remove();
} else {
removeDeprecatedGroup(child);
}
}
}
private int removeDeprecated(GroupTree tree) {
int hits = 0;
Iterator<? extends GroupTree> g = tree.getSubGroups().iterator();
while (g.hasNext()) {
GroupTree child = g.next();
hits += removeDeprecated(child);
if (child.getAllOperatorDescriptions().size() == 0) {
g.remove();
}
}
Iterator<OperatorDescription> o = tree.getOperatorDescriptions().iterator();
while (o.hasNext()) {
OperatorDescription description = o.next();
if (description.isDeprecated()) {
o.remove();
} else {
hits++;
}
}
return hits;
}
/**
* Recursively deletes nodes from the filteredTree when neither the name of the node nor the
* name of one of its children or parents matches the filter. Stores paths to nodes that match
* the filter (not containing the node itself) in expandedPaths.
*
* @param filter
* filter for which names to take
* @param filteredTree
* the tree to filter
* @param expandedPaths
* list of paths that should be expanded when the tree is displayed
* @param path
* the current path
* @return number of hits below the current path
*/
private int removeFilteredInstances(CamelCaseFilter filter, GroupTree filteredTree, List<TreePath> expandedPaths,
TreePath path) {
int hits = 0;
Iterator<? extends GroupTree> g = filteredTree.getSubGroups().iterator();
while (g.hasNext()) {
GroupTree child = g.next();
boolean matches = filter.matches(child.getName());
if (matches) {
expandedPaths.add(path);
}
hits += removeFilteredInstances(filter, child, expandedPaths, path.pathByAddingChild(child));
if (child.getAllOperatorDescriptions().size() == 0 && !matches) {
g.remove();
}
}
// remove non matching operator descriptions if the group does not match, keep all in
// matching group, count hits even if matching
boolean groupMatches = filter.matches(filteredTree.getName());
Iterator<OperatorDescription> o = filteredTree.getOperatorDescriptions().iterator();
while (o.hasNext()) {
OperatorDescription description = o.next();
boolean matches = filter.matches(description.getName()) || filter.matches(description.getShortName());
if (!matches) {
for (String tag : description.getTags()) {
matches = filter.matches(tag);
if (matches) {
break;
}
}
}
if (!filterDeprecated) {
for (String replaces : description.getReplacedKeys()) {
matches |= filter.matches(replaces);
}
}
if (!matches && !groupMatches) {
o.remove();
} else {
hits++;
}
}
if (hits > 0) {
expandedPaths.add(path);
}
return hits;
}
public void setSortByUsage(boolean sort) {
if (sort != this.sortByUsage) {
this.sortByUsage = sort;
updateTree();
}
}
private int removeFilteredInstances(String[] terms, GroupTree filteredTree, List<TreePath> expandedPaths,
TreePath path) {
int hits = 0;
Iterator<? extends GroupTree> g = filteredTree.getSubGroups().iterator();
while (g.hasNext()) {
GroupTree child = g.next();
String lowerCaseName = child.getName().toLowerCase();
boolean matches = true;
for (String term : terms) {
if (!lowerCaseName.contains(term)) {
matches = false;
break;
}
}
if (matches) {
expandedPaths.add(path);
}
hits += removeFilteredInstances(terms, child, expandedPaths, path.pathByAddingChild(child));
if (child.getAllOperatorDescriptions().size() == 0 && !matches) {
g.remove();
}
}
// remove non matching operator descriptions if the group does not match, keep all in
// matching group, count hits even if matching
boolean groupMatches = true;
String lowerCaseName = filteredTree.getName().toLowerCase();
for (String term : terms) {
if (!lowerCaseName.contains(term)) {
groupMatches = false;
break;
}
}
Iterator<OperatorDescription> o = filteredTree.getOperatorDescriptions().iterator();
while (o.hasNext()) {
OperatorDescription description = o.next();
boolean matches = true;
for (String term : terms) {
// check names
if (description.getName().toLowerCase().contains(term)) {
continue;
}
if (description.getShortName().toLowerCase().contains(term)) {
continue;
}
// check tags
boolean foundTag = false;
for (String tag : description.getTags()) {
if (tag.toLowerCase().contains(term)) {
foundTag = true;
break;
}
}
if (foundTag) {
continue;
}
// replaced keys
boolean foundReplacedKey = false;
if (!filterDeprecated) {
for (String replaces : description.getReplacedKeys()) {
replaces.toLowerCase().contains(term);
foundReplacedKey = true;
break;
}
}
if (foundReplacedKey) {
continue;
}
// term not found
matches = false;
break;
}
if (!matches && !groupMatches) {
o.remove();
} else {
hits++;
}
}
if (hits > 0) {
expandedPaths.add(path);
}
return hits;
}
@Override
public void operatorRegistered(OperatorDescription description, OperatorDocBundle bundle) {
updateTree();
}
@Override
public void operatorUnregistered(OperatorDescription description) {
updateTree();
}
}