/* * RapidMiner * * Copyright (C) 2001-2011 by Rapid-I and the contributors * * Complete list of developers available at our web site: * * http://rapid-i.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.awt.Component; import java.awt.Dialog; import java.awt.Point; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.File; import java.io.IOException; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.logging.Level; import javax.swing.Action; import javax.swing.JComponent; import javax.swing.JPopupMenu; import javax.swing.JTree; import javax.swing.TransferHandler; import javax.swing.event.EventListenerList; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeExpansionListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import com.rapidminer.RepositoryProcessLocation; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.actions.OpenAction; import com.rapidminer.gui.dnd.TransferableOperator; import com.rapidminer.gui.tools.ProgressThread; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.gui.tools.components.ToolTipWindow; import com.rapidminer.gui.tools.components.ToolTipWindow.TipProvider; import com.rapidminer.gui.tools.dialogs.wizards.dataimport.DataImportWizard; import com.rapidminer.repository.DataEntry; import com.rapidminer.repository.Entry; import com.rapidminer.repository.Folder; import com.rapidminer.repository.ProcessEntry; import com.rapidminer.repository.RepositoryActionCondition; import com.rapidminer.repository.RepositoryActionConditionImplConfigRepository; import com.rapidminer.repository.RepositoryActionConditionImplStandard; import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; import com.rapidminer.repository.RepositoryManager; import com.rapidminer.repository.gui.actions.AbstractRepositoryAction; import com.rapidminer.repository.gui.actions.ConfigureRepositoryAction; import com.rapidminer.repository.gui.actions.CopyEntryRepositoryAction; import com.rapidminer.repository.gui.actions.CopyLocationAction; import com.rapidminer.repository.gui.actions.CreateFolderAction; import com.rapidminer.repository.gui.actions.DeleteAction; import com.rapidminer.repository.gui.actions.OpenEntryAction; import com.rapidminer.repository.gui.actions.PasteEntryRepositoryAction; import com.rapidminer.repository.gui.actions.RefreshAction; import com.rapidminer.repository.gui.actions.RenameAction; import com.rapidminer.repository.gui.actions.StoreProcessAction; import com.rapidminer.tools.LogService; /** * A tree displaying repository contents. * <p> * To add new actions to the popup menu, call {@link #addRepositoryAction(Class, RepositoryActionCondition, Class, boolean, boolean)} or {@link #addRepositoryAction(Class, RepositoryActionCondition, boolean, boolean)}. * Be sure to follow its instructions carefully. * * @author Simon Fischer, Tobias Malbrecht, Marco Boeck */ public class RepositoryTree extends JTree { /** * Holds the RepositoryAction entries. * */ private static class RepositoryActionEntry { private Class<? extends AbstractRepositoryAction> actionClass; private RepositoryActionCondition condition; private boolean hasSeparatorBefore; private boolean hasSeparatorAfter; public RepositoryActionEntry(Class<? extends AbstractRepositoryAction> actionClass, RepositoryActionCondition condition, boolean hasSeparatorBefore, boolean hasSeparatorAfter) { this.actionClass = actionClass; this.condition = condition; this.hasSeparatorAfter = hasSeparatorAfter; this.hasSeparatorBefore = hasSeparatorBefore; } public boolean hasSeperatorBefore() { return hasSeparatorBefore; } public boolean hasSeperatorAfter() { return hasSeparatorAfter; } public RepositoryActionCondition getRepositoryActionCondition() { return condition; } public Class<? extends AbstractRepositoryAction> getRepositoryActionClass() { return actionClass; } } public final AbstractRepositoryAction<Entry> RENAME_ACTION = new RenameAction(this); public final AbstractRepositoryAction<Entry> DELETE_ACTION = new DeleteAction(this); public final AbstractRepositoryAction<DataEntry> OPEN_ACTION = new OpenEntryAction(this); public final AbstractRepositoryAction<Folder> REFRESH_ACTION = new RefreshAction(this); public final AbstractRepositoryAction<Folder> CREATE_FOLDER_ACTION = new CreateFolderAction(this); private List<AbstractRepositoryAction> listToEnable = new LinkedList<AbstractRepositoryAction>(); private EventListenerList listenerList = new EventListenerList(); private static final long serialVersionUID = -6613576606220873341L; private static final List<RepositoryActionEntry> REPOSITORY_ACTIONS = new LinkedList<RepositoryTree.RepositoryActionEntry>(); static { addRepositoryAction(ConfigureRepositoryAction.class, new RepositoryActionConditionImplConfigRepository(), false, true); addRepositoryAction(OpenEntryAction.class, new RepositoryActionConditionImplStandard(new Class<?>[]{ DataEntry.class }, new Class<?>[]{}), false, false); addRepositoryAction(StoreProcessAction.class, new RepositoryActionConditionImplStandard(new Class<?>[]{ ProcessEntry.class, Folder.class }, new Class<?>[]{}), false, false); addRepositoryAction(RenameAction.class, new RepositoryActionConditionImplStandard(new Class<?>[]{ Entry.class }, new Class<?>[]{}), false, false); addRepositoryAction(CreateFolderAction.class, new RepositoryActionConditionImplStandard(new Class<?>[]{ Folder.class }, new Class<?>[]{}), false, false); addRepositoryAction(CopyEntryRepositoryAction.class, new RepositoryActionConditionImplStandard(new Class<?>[]{}, new Class<?>[]{}), true, false); addRepositoryAction(PasteEntryRepositoryAction.class, new RepositoryActionConditionImplStandard(new Class<?>[]{}, new Class<?>[]{}), false, false); addRepositoryAction(CopyLocationAction.class, new RepositoryActionConditionImplStandard(new Class<?>[]{}, new Class<?>[]{}), false, false); addRepositoryAction(DeleteAction.class, new RepositoryActionConditionImplStandard(new Class<?>[]{ Entry.class }, new Class<?>[]{}), false, false); addRepositoryAction(RefreshAction.class, new RepositoryActionConditionImplStandard(new Class<?>[]{ Folder.class }, new Class<?>[]{}), true, false); } public RepositoryTree() { this(null); } public RepositoryTree(Dialog owner) { this(owner, false); } public RepositoryTree(Dialog owner, boolean onlyFolders) { super(new RepositoryTreeModel(RepositoryManager.getInstance(null), onlyFolders)); // these actions are a) needed for the action map or b) needed by other classes for toolbars etc listToEnable.add(DELETE_ACTION); listToEnable.add(RENAME_ACTION); listToEnable.add(REFRESH_ACTION); listToEnable.add(OPEN_ACTION); listToEnable.add(CREATE_FOLDER_ACTION); RENAME_ACTION.addToActionMap(this, WHEN_FOCUSED); DELETE_ACTION.addToActionMap(this, WHEN_FOCUSED); REFRESH_ACTION.addToActionMap(this, WHEN_FOCUSED); setRootVisible(false); setShowsRootHandles(true); setCellRenderer(new RepositoryTreeCellRenderer()); getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getButton()== MouseEvent.BUTTON3){ int row = getRowForLocation(e.getX(),e.getY()); setSelectionInterval(row, row); if (e.isPopupTrigger()) { showPopup(e); } } } @Override public void mousePressed(MouseEvent e) { if (e.getButton()== MouseEvent.BUTTON3){ int row = getRowForLocation(e.getX(),e.getY()); setSelectionInterval(row, row); if (e.isPopupTrigger()) { showPopup(e); } } } @Override public void mouseReleased(MouseEvent e) { if (e.getButton()== MouseEvent.BUTTON3){ int row = getRowForLocation(e.getX(),e.getY()); setSelectionInterval(row, row); if (e.isPopupTrigger()) { showPopup(e); } } } }); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { TreePath path = getSelectionPath(); if (path == null) { return; } fireLocationSelected((Entry) path.getLastPathComponent()); } } }); addKeyListener(new KeyListener() { @Override public void keyTyped(KeyEvent e) { } /** * Opens entries on enter pressed; collapses/expands folders */ @Override public void keyReleased(KeyEvent e) { if (e.getModifiers() == 0) { switch (e.getKeyCode()) { case KeyEvent.VK_ENTER: case KeyEvent.VK_SPACE: TreePath path = getSelectionPath(); if (path == null) { return; } Entry entry = (Entry)path.getLastPathComponent(); if (entry instanceof Folder) { if (isExpanded(path)) { collapsePath(path); } else { expandPath(path); } } else { fireLocationSelected((Entry) path.getLastPathComponent()); } e.consume(); break; } } } @Override public void keyPressed(KeyEvent e) { } }); setDragEnabled(true); setTransferHandler(new TransferHandler() { private static final long serialVersionUID = 1L; @Override public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { List<DataFlavor> flavors = Arrays.asList(transferFlavors); boolean contains = flavors.contains(DataFlavor.javaFileListFlavor); contains |= flavors.contains(TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR); return contains; } /** Imports data files using a Wizard. */ @Override public boolean importData(final TransferSupport ts) { // determine where to insert final Entry droppedOn; if (ts.isDrop()) { Point dropPoint = ts.getDropLocation().getDropPoint(); TreePath path = getPathForLocation((int)dropPoint.getX(), (int)dropPoint.getY()); if (path == null) { return false; } droppedOn = (Entry) path.getLastPathComponent(); } else { droppedOn = getSelectedEntry(); } if (droppedOn == null) { return false; } try { List<DataFlavor> flavors = Arrays.asList(ts.getDataFlavors()); if (flavors.contains(TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR)) { final RepositoryLocation loc = (RepositoryLocation) ts.getTransferable().getTransferData(TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR); if (droppedOn instanceof Folder) { new ProgressThread("copy_repository_entry", true) { public void run() { try { if (ts.isDrop() && (ts.getDropAction() == MOVE)) { RepositoryManager.getInstance(null).move(loc, (Folder)droppedOn, getProgressListener()); } else { RepositoryManager.getInstance(null).copy(loc, (Folder)droppedOn, getProgressListener()); } } catch (RepositoryException e) { SwingTools.showSimpleErrorMessage("error_in_copy_repository_entry", e, loc.toString(), e.getMessage()); } } }.start(); return true; } else { return false; } } else if (flavors.contains(DataFlavor.javaFileListFlavor)) { List files = (List)ts.getTransferable().getTransferData(DataFlavor.javaFileListFlavor); File file = (File) files.get(0); DataImportWizard.importData(file, droppedOn.getLocation()); return true; } else { return false; } } catch (UnsupportedFlavorException e) { LogService.getRoot().log(Level.WARNING, "Cannot accept drop flavor: "+e, e); return false; } catch (IOException e) { LogService.getRoot().log(Level.WARNING, "Error during drop: "+e, e); return false; } } @Override public int getSourceActions(JComponent c) { return COPY_OR_MOVE; } @Override protected Transferable createTransferable(JComponent c) { TreePath path = getSelectionPath(); if (path != null) { Entry e = (Entry)path.getLastPathComponent(); final RepositoryLocation location = e.getLocation(); return new Transferable() { @Override public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { if (flavor.equals(DataFlavor.stringFlavor)) { return location.getAbsoluteLocation(); } else if (TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR.equals(flavor)) { return location; } else { throw new IllegalArgumentException("Flavor not supported: "+flavor); } } @Override public DataFlavor[] getTransferDataFlavors() { return new DataFlavor[] { TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR, DataFlavor.stringFlavor }; } @Override public boolean isDataFlavorSupported(DataFlavor flavor) { return TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR.equals(flavor) || DataFlavor.stringFlavor.equals(flavor); } }; } else { return null; } } }); // setTransferHandler(new OperatorTransferHandler() { // private static final long serialVersionUID = 1L; // @Override // protected List<Operator> getDraggedOperators() { // TreePath path = getSelectionPath(); // if (path != null) { // Entry e = (Entry)path.getLastPathComponent(); // if (e instanceof IOObjectEntry) { // RepositorySource source; // try { // source = OperatorService.createOperator(RepositorySource.class); // source.setParameter(RepositorySource.PARAMETER_REPOSITORY_ENTRY, e.getLocation().getAbsoluteLocation()); // return Collections.<Operator>singletonList(source); // } catch (OperatorCreationException e1) { // LogService.getRoot().log(Level.WARNING, "Cannot create RepositorySource: "+e, e); // return null; // } // } else { // return null; // } // } else { // return null; // } // } // }); getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() { @Override public void valueChanged(TreeSelectionEvent e) { enableActions(); } }); addTreeExpansionListener(new TreeExpansionListener() { @Override public void treeExpanded(TreeExpansionEvent event) { // select the last expanded/collapsed path selectionModel.setSelectionPath(event.getPath()); } @Override public void treeCollapsed(TreeExpansionEvent event) { // select the last expanded/collapsed path treeExpanded(event); } }); enableActions(); new ToolTipWindow(owner, new TipProvider() { @Override public String getTip(Object o) { if (o instanceof Entry) { return ToolTipProviderHelper.getTip((Entry) o); } else { return null; } } @Override public Object getIdUnder(Point point) { TreePath path = getPathForLocation((int)point.getX(), (int)point.getY()); if (path != null) { return path.getLastPathComponent(); } else { return null; } } @Override public Component getCustomComponent(Object o) { if (o instanceof Entry) { return ToolTipProviderHelper.getCustomComponent((Entry) o); } else { return null; } } }, this); } public void enableActions() { for (AbstractRepositoryAction action : listToEnable) { action.enable(); } } public void addRepositorySelectionListener(RepositorySelectionListener listener) { listenerList.add(RepositorySelectionListener.class, listener); } public void removeRepositorySelectionListener(RepositorySelectionListener listener) { listenerList.remove(RepositorySelectionListener.class, listener); } private void fireLocationSelected(Entry entry) { RepositorySelectionEvent event = null; for (RepositorySelectionListener l : listenerList.getListeners(RepositorySelectionListener.class)) { if (event == null) { event = new RepositorySelectionEvent(entry); } l.repositoryLocationSelected(event); } } /** Selects as much as possible of the selected path to the given location. * Returns true if the given location references a folder. */ boolean expandIfExists(RepositoryLocation relativeTo, String location) { RepositoryLocation loc; boolean full = true; try { loc = new RepositoryLocation(relativeTo, location); } catch (Exception e) { // do nothing return false; } Entry entry = null; while (true) { try { entry = loc.locateEntry(); if (entry != null) { break; } } catch (RepositoryException e) { return false; } loc = loc.parent(); if (loc == null) { return false; } full = false; } if (entry != null) { RepositoryTreeModel model = (RepositoryTreeModel) getModel(); TreePath pathTo = model.getPathTo(entry); expandPath(pathTo); setSelectionPath(pathTo); if (entry instanceof Folder) { return full; } } return false; //loc = loc.parent(); } private void showPopup(MouseEvent e) { TreePath path = getSelectionPath(); if (path == null) { return; } Object selected = path.getLastPathComponent(); JPopupMenu menu = new JPopupMenu(); // can support multiple selections, not needed right now List<Entry> entryList = new ArrayList<Entry>(1); if (selected instanceof Entry) { entryList.add((Entry)selected); } List<Action> actionList = createContextMenuActions(this, entryList); // go through ordered list of actions and add them for (Action action : actionList) { if (action == null) { menu.addSeparator(); } else { menu.add(action); } } // append custom actions if there are any if (selected instanceof Entry) { Collection<Action> customActions = ((Entry) selected).getCustomActions(); if ((customActions != null) && !customActions.isEmpty()) { menu.addSeparator(); for (Action a : customActions) { menu.add(a); } } } menu.show(this, e.getX(), e.getY()); } /** Opens the process held by the given entry (in the background) and opens it. */ public static void openProcess(final ProcessEntry processEntry) { ProgressThread openProgressThread = new ProgressThread("open_process") { public void run() { RepositoryProcessLocation processLocation = new RepositoryProcessLocation(processEntry.getLocation()); if (RapidMinerGUI.getMainFrame().close()){ OpenAction.open(processLocation, false); } /* PRE FIX OF BUG 308: When opening process with double click all changes are discarded * * try { RepositoryProcessLocation processLocation = new RepositoryProcessLocation(processEntry.getLocation()); String xml = processEntry.retrieveXML(); try { final Process process = new Process(xml); process.setProcessLocation(processLocation); SwingUtilities.invokeLater(new Runnable() { public void run() { RapidMinerGUI.getMainFrame().setOpenedProcess(process, true, processEntry.getLocation().toString()); } }); } catch (Exception e) { RapidMinerGUI.getMainFrame().handleBrokenProxessXML(processLocation, xml, e); } } catch (Exception e1) { SwingTools.showSimpleErrorMessage("cannot_fetch_data_from_repository", e1); }*/ } }; openProgressThread.start(); } public Entry getSelectedEntry() { TreePath path = getSelectionPath(); if (path == null) { return null; } Object selected = path.getLastPathComponent(); if (selected instanceof Entry) { return (Entry) selected; } else { return null; } } public Collection<AbstractRepositoryAction<?>> getAllActions() { List<AbstractRepositoryAction<?>> listOfAbstractRepositoryActions = new LinkedList<AbstractRepositoryAction<?>>(); for (Action action : createContextMenuActions(this, new LinkedList<Entry>())) { if (action instanceof AbstractRepositoryAction<?>) { listOfAbstractRepositoryActions.add((AbstractRepositoryAction<?>)action); } } return listOfAbstractRepositoryActions; } /** * Appends the given {@link AbstractRepositoryAction} extending class to the popup menu actions. * <p>The class <b>MUST</b> have one public constructor taking only a RepositoryTree. * </br>Example: public MyNewRepoAction(RepositoryTree tree) { ... } * </br>Otherwise creating the action via reflection will fail. * * @param actionClass the class extending {@link AbstractRepositoryAction} * @param condition the {@link RepositoryActionCondition} which determines on which selected entries the action will be visible. * @param hasSeparatorBefore if true, a separator will be added before the action * @param hasSeparatorAfter if true, a separator will be added after the action * @return true if the action was successfully added; false otherwise */ public static void addRepositoryAction(Class<? extends AbstractRepositoryAction> actionClass, RepositoryActionCondition condition, boolean hasSeparatorBefore, boolean hasSeparatorAfter) { addRepositoryAction(actionClass, condition, null, hasSeparatorBefore, hasSeparatorAfter); } /** * Adds the given {@link AbstractRepositoryAction} extending class to the popup menu actions at the given index. * <p>The class <b>MUST</b> have one public constructor taking only a RepositoryTree. * </br>Example: public MyNewRepoAction(RepositoryTree tree) { ... } * </br>Otherwise creating the action via reflection will fail. * * @param actionClass the class extending {@link AbstractRepositoryAction} * @param condition the {@link RepositoryActionCondition} which determines on which selected entries the action will be visible. * @param insertAfterThisAction the class of the action after which the new action should be inserted. Set to {@code null} to append the action at the end. * @param hasSeparatorBefore if true, a separator will be added before the action * @param hasSeparatorAfter if true, a separator will be added after the action * @return true if the action was successfully added; false otherwise */ public static void addRepositoryAction(Class<? extends AbstractRepositoryAction> actionClass, RepositoryActionCondition condition, Class<? extends Action> insertAfterThisAction, boolean hasSeparatorBefore, boolean hasSeparatorAfter) { if (actionClass == null || condition == null) { throw new IllegalArgumentException("actionClass and condition must not be null!"); } RepositoryActionEntry newEntry = new RepositoryActionEntry(actionClass, condition, hasSeparatorBefore, hasSeparatorAfter); if (insertAfterThisAction == null) { REPOSITORY_ACTIONS.add(newEntry); } else { // searching for class to insert after boolean inserted = false; int i = 0; for (RepositoryActionEntry entry : REPOSITORY_ACTIONS) { Class<? extends Action> existingAction = entry.getRepositoryActionClass(); if (existingAction.equals(insertAfterThisAction)) { REPOSITORY_ACTIONS.add(i + 1, newEntry); inserted = true; break; } i++; } // if reference couldn't be found: just add as last if(!inserted) REPOSITORY_ACTIONS.add(newEntry); } } /** * Removes the given action from the popup menu actions. * @param actionClass the class of the {@link AbstractRepositoryAction} to remove */ public static void removeRepositoryAction(Class<? extends AbstractRepositoryAction> actionClass) { Iterator<RepositoryActionEntry> iterator = REPOSITORY_ACTIONS.iterator(); while (iterator.hasNext()) { if (iterator.next().getRepositoryActionClass().equals(actionClass)) { iterator.remove(); } } } /** * This method returns a list of actions shown in the context menu if the given {@link RepositoryActionCondition} is true. * Contains {@code null} elements for each separator. * This method is called by each {@link RepositoryTree} instance during construction time and * creates instances via reflection of all registered acionts. * See {@link #addRepositoryAction(Class, RepositoryActionCondition, Class, boolean, boolean)} to add actions. */ private static List<Action> createContextMenuActions(RepositoryTree repositoryTree, List<Entry> entryList) { List<Action> listOfActions = new LinkedList<Action>(); boolean lastWasSeparator = true; for (RepositoryActionEntry entry: REPOSITORY_ACTIONS) { try { if (entry.getRepositoryActionCondition().evaluateCondition(entryList)) { if (!lastWasSeparator && entry.hasSeperatorBefore()) { // add null element which means a separator will be added in the menu listOfActions.add(null); } Constructor constructor = entry.getRepositoryActionClass().getConstructor(new Class[]{ RepositoryTree.class }); AbstractRepositoryAction createdAction = (AbstractRepositoryAction)constructor.newInstance(repositoryTree); createdAction.enable(); listOfActions.add(createdAction); if (entry.hasSeperatorAfter()) { listOfActions.add(null); lastWasSeparator = true; } else { lastWasSeparator = false; } } } catch (Exception e) { LogService.getGlobal().log("could not create repository action: " + entry.getRepositoryActionClass(), LogService.ERROR); } } return listOfActions; } }