/* * RapidMiner * * Copyright (C) 2001-2014 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.process; import java.lang.reflect.InvocationTargetException; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.logging.Level; 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 javax.xml.datatype.XMLGregorianCalendar; import com.rapid_i.repository.wsimport.ProcessResponse; import com.rapid_i.repository.wsimport.ProcessStackTrace; import com.rapid_i.repository.wsimport.ProcessStackTraceElement; import com.rapidminer.io.process.XMLTools; import com.rapidminer.repository.RemoteProcessState; import com.rapidminer.repository.Repository; import com.rapidminer.repository.RepositoryManager; import com.rapidminer.repository.remote.ProcessServiceFacade; import com.rapidminer.repository.remote.RemoteRepository; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tools.Observable; import com.rapidminer.tools.Observer; import com.rapidminer.tools.container.Pair; /** * The TreeModel for the log of remotely executed processes. * * @author Simon Fischer, Nils Woehler */ public class RemoteProcessesTreeModel implements TreeModel { private static final long UPDATE_PERIOD = 2500; /** Object returned by {@link #getChild(Object, int)} if the list is empty or an error has occurred. */ public static final Object EMPTY_PROCESS_LIST = new Object(); public static final Object PENDING_PROCESS_LIST = new Object(); public static enum ProcessListState { PENDING, READY, ERROR, CANCELED } class ProcessList { private List<Integer> knownIds = new LinkedList<Integer>(); private Map<Integer, ProcessResponse> processResponses = new HashMap<Integer, ProcessResponse>(); private ProcessListState state = ProcessListState.PENDING; private int maxID = 0; public int add(ProcessResponse pr) { int newIndex = -1; if (!processResponses.containsKey(pr.getId())) { newIndex = knownIds.size(); if (pr.getId() > maxID) { maxID = pr.getId(); newIndex = 0; } knownIds.add(pr.getId()); Collections.sort(knownIds); Collections.reverse(knownIds); } processResponses.put(pr.getId(), pr); state = ProcessListState.READY; return newIndex; } public ProcessListState getState() { return state; } public void setState(ProcessListState state) { this.state = state; } public ProcessResponse getByIndex(int index) { return processResponses.get(knownIds.get(index)); } public ProcessResponse getById(int id) { return processResponses.get(id); } public int size() { return knownIds.size(); } public int indexOf(ProcessResponse child) { int index = 0; for (Integer id : knownIds) { ProcessResponse pr = processResponses.get(id); if ((pr != null) && (pr.getId() == child.getId())) { return index; } index++; } return -1; } private void trim(Set<Integer> processIds, RemoteRepository repos) { boolean wasEmpty = knownIds.isEmpty(); List<Integer> removedIndices = new LinkedList<Integer>(); List<Object> removedObjects = new LinkedList<Object>(); Iterator<Integer> i = knownIds.iterator(); int index = 0; while (i.hasNext()) { Integer id = i.next(); if (!processIds.contains(id)) { i.remove(); ProcessResponse process = processResponses.remove(id); removedIndices.add(index); removedObjects.add(process); } index++; } if (!wasEmpty && knownIds.isEmpty()) { // list was not empty before, but now it is empty, so we fire structure changed. Model will return only EMPTY_PROCESS_LIST fireStructureChanged(new TreeModelEvent(this, new Object[] { root, repos })); } else if (!removedIndices.isEmpty()) { int[] indices = new int[removedIndices.size()]; for (int j = 0; j < removedIndices.size(); j++) { indices[j] = removedIndices.get(j); } fireDelete(new TreeModelEvent(this, new Object[] { root, repos }, indices, removedObjects.toArray())); } else { return; } } } private final class UpdateTask extends TimerTask { @Override public void run() { Iterator<RemoteRepository> iterator = getRepositoryIterator(); // iterate over all current repositories while (iterator.hasNext()) { final RemoteRepository repos = iterator.next(); // check if the current repository is observed if (observedRepositories.contains(repos)) { // get process list from repository final ProcessList processList = getProcessList(repos); try { // if password input has been canceled if (repos.isPasswordInputCanceled()) { // set it into canceled state and log action if (processList.getState() != ProcessListState.CANCELED) { LogService.getRoot().log(Level.INFO, I18N.getMessage(LogService.getRoot().getResourceBundle(), "com.rapidminer.repository.gui.process.RemoteProcessesTreeModel.skipping_user_canceled_auth", repos.getName())); SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { setIntoState(repos, ProcessListState.CANCELED); } }); } continue; } ProcessServiceFacade processService = repos.getProcessService(); if (processService == null) { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { setIntoState(repos, ProcessListState.ERROR); } }); continue; } final Collection<Integer> processIds = processService.getRunningProcesses(since); // First, delete removed ids SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { if (processIds != null) { if (processIds.isEmpty()) { processList.setState(ProcessListState.READY); fireStructureChanged(new TreeModelEvent(this, new Object[] { root, repos })); } else { if (processList.getState() != ProcessListState.ERROR) { processList.setState(ProcessListState.READY); } } processList.trim(new HashSet<Integer>(processIds), repos); } } }); // Now, update model for new / existing IDs for (Integer processId : processIds) { ProcessResponse oldProcess = processList.getById(processId); // we update if we don't know the id yet or if the process is not complete if (oldProcess == null) { final ProcessResponse newResponse = processService.getRunningProcessesInfo(processId); SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { // If was empty before, fire event to remove EMPTY_PROCESS_LIST if (processList.size() == 0) { fireDelete(new TreeModelEvent(this, new Object[] { root, repos }, new int[] { 0 }, new Object[] { EMPTY_PROCESS_LIST })); } int newIndex = processList.add(newResponse); fireAdd(new TreeModelEvent(this, new Object[] { root, repos }, new int[] { newIndex }, new Object[] { newResponse })); } }); } else if (!RemoteProcessState.valueOf(oldProcess.getState()).isTerminated()) { final ProcessResponse updatedResponse = processService.getRunningProcessesInfo(processId); SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { processList.add(updatedResponse); fireStructureChanged(new TreeModelEvent(this, new Object[] { root, repos, updatedResponse })); } }); } else { // If process is terminated, there is not need to update. // The process is already in the list since it is copied } } } catch (Exception ex) { if (processList.getState() != ProcessListState.ERROR) { LogService.getRoot().log(Level.WARNING, I18N.getMessage(LogService.getRoot().getResourceBundle(), "com.rapidminer.repository.gui.process.RemoteProcessesTreeModel.fetching_remote_process_list_error", ex), ex); try { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { setIntoState(repos, ProcessListState.ERROR); } }); } catch (InvocationTargetException e) { LogService.getRoot().log(Level.WARNING, I18N.getMessage(LogService.getRoot().getResourceBundle(), "com.rapidminer.repository.gui.process.RemoteProcessesTreeModel.fetching_remote_process_list_error", e), e); } catch (InterruptedException e) { LogService.getRoot().log(Level.WARNING, I18N.getMessage(LogService.getRoot().getResourceBundle(), "com.rapidminer.repository.gui.process.RemoteProcessesTreeModel.fetching_remote_process_list_error", e), e); } } } } } } } private Observer<Repository> repositoryManagerObserver = new Observer<Repository>() { @Override public void update(Observable<Repository> observable, final Repository arg) { // repository has been removed if (arg == null) { // rebuild repositories, observerRepositories and processes lists synchronized (repositories) { // remember old observed repositories Set<RemoteRepository> oldObserverRepositories = new HashSet<RemoteRepository>(observedRepositories); // remove all current repositories List<RemoteRepository> oldRemoteRepositories = new LinkedList<RemoteRepository>(repositories); for (RemoteRepository repo : oldRemoteRepositories) { removeRepository(repo); } // add all remote repositories that still exists List<RemoteRepository> newRemoteRepositories = new LinkedList<RemoteRepository>(RepositoryManager.getInstance(null).getRemoteRepositories()); for (RemoteRepository repo : newRemoteRepositories) { addRepository(repo); } // start observing all repositories again that still exists for (RemoteRepository repo : oldObserverRepositories) { if (repositories.contains(repo)) { observe(repo); } } } } else { // repository has been added if (arg instanceof RemoteRepository) { RemoteRepository addedRepo = (RemoteRepository) arg; addRepository(addedRepo); } } } }; private List<Pair<RemoteRepository, ProcessList>> processes = new LinkedList<Pair<RemoteRepository, ProcessList>>(); private List<RemoteRepository> repositories = new LinkedList<RemoteRepository>(); private Set<RemoteRepository> observedRepositories = new HashSet<RemoteRepository>(); private Object root = new Object(); private Timer updateTimer = new Timer("RemoteProcess-Updater", true); private XMLGregorianCalendar since; public RemoteProcessesTreeModel() { updateTimer.schedule(new UpdateTask(), UPDATE_PERIOD, UPDATE_PERIOD); RepositoryManager.getInstance(null).addObserver(repositoryManagerObserver, false); // initialize model for (RemoteRepository repo : RepositoryManager.getInstance(null).getRemoteRepositories()) { addRepository(repo); } } private void addRepository(RemoteRepository repo) { synchronized (repositories) { // add repository repositories.add(repo); // create new process list for repository processes.add(new Pair<RemoteRepository, ProcessList>(repo, new ProcessList())); fireAdd(new TreeModelEvent(this, new Object[] { root }, new int[] { getIndexOfChild(root, repo) }, new Object[] { repo })); } } private void removeRepository(RemoteRepository repo) { synchronized (repositories) { int indexOfRepo = repositories.indexOf(repo); // remove repository from list repositories.remove(indexOfRepo); // remove repository, process list pair from list Iterator<Pair<RemoteRepository, ProcessList>> iterator = processes.iterator(); while (iterator.hasNext()) { Pair<RemoteRepository, ProcessList> next = iterator.next(); if (next.getFirst() == repo) { iterator.remove(); break; } } // remove repository from observed repositories observedRepositories.remove(repo); fireDelete(new TreeModelEvent(this, new Object[] { root }, new int[] { indexOfRepo }, new Object[] { repo })); } } private EventListenerList listeners = new EventListenerList(); @Override public void addTreeModelListener(TreeModelListener l) { listeners.add(TreeModelListener.class, l); } @Override public void removeTreeModelListener(TreeModelListener l) { listeners.remove(TreeModelListener.class, l); } @Override public Object getChild(Object parent, int index) { if (parent == root) { return repositories.get(index); } else if (parent instanceof RemoteRepository) { ProcessList processList = getProcessList((RemoteRepository) parent); if (processList.size() == 0) { if (processList.getState() == ProcessListState.PENDING) { return PENDING_PROCESS_LIST; } else { return EMPTY_PROCESS_LIST; } } else { return processList.getByIndex(index); } } else if (parent instanceof ProcessResponse) { ProcessResponse proResponse = (ProcessResponse) parent; if (proResponse.getException() != null) { if (index == 0) { return new ExceptionWrapper(proResponse.getException()); } else { return null; } } else { ProcessStackTrace trace = proResponse.getTrace(); int elementsSize = 0; if ((trace != null) && (trace.getElements() != null)) { elementsSize = trace.getElements().size(); } if (index < elementsSize && trace != null) { return trace.getElements().get(index); } else { return new OutputLocation(proResponse.getOutputLocations().get(index - elementsSize)); } } } else { return null; } } @Override public int getChildCount(Object parent) { if (parent == root) { return repositories.size(); } else if (parent instanceof RemoteRepository) { ProcessList list = getProcessList((RemoteRepository) parent); if ((list == null) || (list.size() == 0)) { return 1; // if empty, just display single message string } else { return list.size(); } } else if (parent instanceof ProcessResponse) { ProcessResponse proResponse = (ProcessResponse) parent; if (proResponse.getException() != null) { return 1; } else { int size = 0; ProcessStackTrace trace = proResponse.getTrace(); if ((trace != null) && (trace.getElements() != null)) { size += trace.getElements().size(); } if (proResponse.getOutputLocations() != null) { size += proResponse.getOutputLocations().size(); } return size; } } else { return 0; } } @Override public int getIndexOfChild(Object parent, Object child) { if (child == RemoteProcessesTreeModel.EMPTY_PROCESS_LIST || child == RemoteProcessesTreeModel.PENDING_PROCESS_LIST) { return 0; } else if (parent == root) { return repositories.indexOf(child); } else if (parent instanceof RemoteRepository) { if (child instanceof ProcessResponse) { return getProcessList((RemoteRepository) parent).indexOf((ProcessResponse) child); } else { return 0; } } else if (parent instanceof ProcessResponse) { ProcessResponse proResponse = (ProcessResponse) parent; if (child instanceof ProcessStackTraceElement) { ProcessStackTrace trace = proResponse.getTrace(); if ((trace != null) && (trace.getElements() != null)) { return trace.getElements().indexOf(child); } else { return -1; } } else if (child instanceof OutputLocation) { if (proResponse.getOutputLocations() != null) { return proResponse.getOutputLocations().indexOf(((OutputLocation) child).getLocation()); } else { return -1; } } else if (child instanceof ExceptionWrapper) { return 0; } else { return -1; } } else { return -1; } } @Override public Object getRoot() { return root; } @Override public boolean isLeaf(Object node) { return (node != root) && !(node instanceof ProcessResponse) && !(node instanceof RemoteRepository); } @Override public void valueForPathChanged(TreePath path, Object newValue) { // not editable } private void fireAdd(TreeModelEvent e) { for (TreeModelListener l : listeners.getListeners(TreeModelListener.class)) { l.treeNodesInserted(e); } } private void fireDelete(TreeModelEvent event) { for (TreeModelListener l : listeners.getListeners(TreeModelListener.class)) { l.treeNodesRemoved(event); } } private void fireStructureChanged(TreeModelEvent e) { for (TreeModelListener l : listeners.getListeners(TreeModelListener.class)) { l.treeStructureChanged(e); } } public void setSince(Date since) { if (((since == null) && (this.since == null)) || ((since != null) && since.equals(this.since))) { return; } if (since == null) { this.since = null; } else { this.since = XMLTools.getXMLGregorianCalendar(since); } // update all models and reset state into PENDING Iterator<RemoteRepository> iterator = getRepositoryIterator(); while (iterator.hasNext()) { final RemoteRepository repos = iterator.next(); setIntoState(repos, ProcessListState.PENDING); } } private Iterator<RemoteRepository> getRepositoryIterator() { synchronized (repositories) { return new LinkedList<RemoteRepository>(repositories).iterator(); } } private void setIntoState(final RemoteRepository repos, ProcessListState state) { ProcessList processList = getProcessList(repos); if (processList.getState() == state) { return; // nothing to do } processList.setState(state); fireStructureChanged(new TreeModelEvent(this, new TreePath(new Object[] { root, repos }))); } protected void observe(RemoteRepository rep) { synchronized (repositories) { observedRepositories.add(rep); } } protected void ignore(RemoteRepository rep) { synchronized (repositories) { observedRepositories.remove(rep); } } protected ProcessList getProcessList(RemoteRepository repository) { List<Pair<RemoteRepository, ProcessList>> copiedProcessList = new LinkedList<Pair<RemoteRepository, ProcessList>>(processes); synchronized (repositories) { for (Pair<RemoteRepository, ProcessList> repoProcessListPair : copiedProcessList) { if (repoProcessListPair.getFirst() == repository) { return repoProcessListPair.getSecond(); } } } return null; } }