Project: cytoscape-plugins
/*
 * KAM Navigator Plugin 
 * 
 * URLs: http://openbel.org/ 
 * Copyright (C) 2012, Selventa 
 * 
 * This program is free software; you can redistribute it and/or modify 
 * it under the terms of the GNU Lesser 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 Lesser General Public License for more details. 
 * 
 * You should have received a copy of the GNU Lesser General Public License 
 * along with this program. If not, see <http://www.gnu.org/licenses/>. 
 */
package org.openbel.cytoscape.navigator; 
 
import java.awt.Component; 
import java.awt.Desktop; 
import java.awt.event.ActionEvent; 
import java.beans.PropertyChangeEvent; 
import java.io.BufferedInputStream; 
import java.io.File; 
import java.io.FileOutputStream; 
import java.io.IOException; 
import java.io.InputStream; 
import java.net.URI; 
import java.net.URISyntaxException; 
 
import javax.swing.JMenu; 
import javax.swing.JMenuItem; 
 
import org.openbel.cytoscape.navigator.dialog.SearchKamDialog; 
import org.openbel.cytoscape.navigator.dialog.SearchKamListDialog; 
import org.openbel.cytoscape.webservice.dialog.SettingsDialog; 
 
import cytoscape.CyNetwork; 
import cytoscape.Cytoscape; 
import cytoscape.CytoscapeVersion; 
import cytoscape.logger.CyLogger; 
import cytoscape.plugin.CytoscapePlugin; 
import cytoscape.util.CytoscapeAction; 
import cytoscape.view.CyNetworkView; 
import cytoscape.view.CytoscapeDesktop; 
import cytoscape.visual.CalculatorCatalog; 
import cytoscape.visual.VisualMappingManager; 
import cytoscape.visual.VisualStyle; 
 
/**
 * The {@link CytoscapePlugin cytoscape plugin} class for the KAM Navigator 
 * plugin. This plugin provides: 
 * <ul> 
 * <li>Load a KAM with Plugins -> KAM Navigator -> Load KAM</li> 
 * <li>Add KAM nodes to network with 
 * Plugins -> KAM Navigator -> Add KAM Nodes</li> 
 * <li>Expand existing network nodes with context-sensitive node actions</li> 
 * </ul> 
 * 
 * @author Anthony Bargnesi <abargnesi@selventa.com> 
 */
 
public class KamNavigatorPlugin extends CytoscapePlugin { 
    public static final String KAM_PLUGIN_SUBMENU = "KAM Navigator"
    public static final String KAM_NODE_ID_ATTR = "KAM_NODE_ID"
    public static final String KAM_NODE_FUNCTION_ATTR = "KAM_NODE_FUNCTION"
    public static final String KAM_NODE_LABEL_ATTR = "KAM_NODE_LABEL"
    public static final String KAM_EDGE_ID_ATTR = "KAM_EDGE_ID"
    public static final String KAM_NAME_ATTR = "KAM_NAME"
    public static final String KAM_COMPILE_DATE_ATTR = "KAM_COMPILE_DATE"
    public static final String WSDL_URL_ATTR = "WSDL_URL"
     
    private static final CyLogger log = CyLogger.getLogger(KamNavigatorPlugin.class); 
    private static final String KAM_NAVIGATOR_VERSION = "0.9"
    private static final String KAM_STYLE = "KAM Visualization"
    private static final String KAM_STYLE_FILE = "/org/openbel/cytoscape/navigator/style.props"
 
    /**
     * Default no-arg plugin construtor to initialize this plugin. 
     */
 
    public KamNavigatorPlugin() { 
        // add KAM_NODE_ID as a system node attribute 
        Cytoscape.getNodeAttributes().setUserEditable(KAM_NODE_ID_ATTR, false); 
        Cytoscape.getNodeAttributes().setUserVisible(KAM_NODE_ID_ATTR, false); 
 
        // add KAM_NODE_FUNCTION as a system node attribute 
        Cytoscape.getNodeAttributes().setUserEditable(KAM_NODE_FUNCTION_ATTR, false); 
        Cytoscape.getNodeAttributes().setUserVisible(KAM_NODE_FUNCTION_ATTR, true); 
         
        // add KAM_EDGE_ID as a system edge attribute 
        Cytoscape.getEdgeAttributes().setUserEditable(KAM_EDGE_ID_ATTR, false); 
        Cytoscape.getEdgeAttributes().setUserVisible(KAM_EDGE_ID_ATTR, false); 
         
        // add KAM_NAME as a system node attribute 
        Cytoscape.getNodeAttributes().setUserEditable(KAM_NAME_ATTR, false); 
        Cytoscape.getNodeAttributes().setUserVisible(KAM_NAME_ATTR, true); 
         
        // add KAM_COMPILE_DATE as a system node attribute 
        Cytoscape.getNodeAttributes().setUserEditable(KAM_COMPILE_DATE_ATTR, false); 
        Cytoscape.getNodeAttributes().setUserVisible(KAM_COMPILE_DATE_ATTR, false); 
         
        // add WSDL_URL as a system node attribute 
        Cytoscape.getNodeAttributes().setUserEditable(WSDL_URL_ATTR, false); 
        Cytoscape.getNodeAttributes().setUserVisible(WSDL_URL_ATTR, false); 
 
        // hook up propery change listeners 
        final KamNodeContextListener nctx = new KamNodeContextListener(); 
        Cytoscape.getPropertyChangeSupport().addPropertyChangeListener( 
                CytoscapeDesktop.NETWORK_VIEW_CREATED, nctx); 
        Cytoscape.getPropertyChangeSupport().addPropertyChangeListener( 
                CytoscapeDesktop.NETWORK_VIEW_DESTROYED, nctx); 
 
        // register property change listener for this instance 
        Cytoscape.getPropertyChangeSupport().addPropertyChangeListener( 
                CytoscapeDesktop.NETWORK_VIEW_CREATED, this); 
        Cytoscape.getPropertyChangeSupport().addPropertyChangeListener( 
                Cytoscape.NETWORK_CREATED, this); 
        Cytoscape.getPropertyChangeSupport().addPropertyChangeListener( 
                Cytoscape.NETWORK_DESTROYED, this); 
 
        // build menu 
        final JMenu pluginMenu = Cytoscape.getDesktop().getCyMenus() 
                .getOperationsMenu(); 
        JMenu kiMenu = getKamPluginMenu(); 
 
        // add to "KAM Navigator" menu if it doesn't exist 
        if (kiMenu == null) { 
            kiMenu = new JMenu(KAM_PLUGIN_SUBMENU); 
            pluginMenu.add(kiMenu); 
        } 
 
        // add "Add Kam Nodes" action to submenu 
        kiMenu.add(new SearchKAMDialogAction()); 
 
        // add "Add Kam List" action to submenu 
        kiMenu.add(new SearchKAMListDialogAction()); 
 
        // add separator before bel configuration entry 
        kiMenu.addSeparator(); 
 
        // add to "KAM Navigator" menu if KAM Plugin is available 
        kiMenu.add(new SettingsDialogAction()); 
         
        // add "Send Feedback" action to submenu 
        JMenuItem feedbackItem = kiMenu.add(new FeedbackMailToAction()); 
        // disable if default mail client is not setup 
        feedbackItem.setEnabled(Desktop.isDesktopSupported() ? Desktop 
                .getDesktop().isSupported(Desktop.Action.MAIL) : false); 
         
        // set the proper menu state 
        updateMenuState(false); 
         
        // load the default style or styles 
        loadKAMStyle(); 
    } 
     
    /**
     * {@inheritDoc} 
     */
 
    @Override 
    public void propertyChange(PropertyChangeEvent e) { 
        super.propertyChange(e); 
 
        if (CytoscapeDesktop.NETWORK_VIEW_CREATED.equals(e.getPropertyName())) { 
            CyNetwork cyn = ((CyNetworkView) e.getNewValue()).getNetwork(); 
            // TODO can this been done with a single instance? 
            // TODO do we need to remove this on network destruction? 
            cyn.addSelectEventListener(new NetworkDetailsListener()); 
        } else if (Cytoscape.NETWORK_CREATED.equals(e.getPropertyName()) ||  
                Cytoscape.NETWORK_DESTROYED.equals(e.getPropertyName())) { 
            updateMenuState(Cytoscape.NETWORK_DESTROYED.equals(e 
                    .getPropertyName())); 
        } 
    } 
     
    private static void updateMenuState(boolean networkDestroyed) { 
        JMenu kiMenu = getKamPluginMenu(); 
        JMenuItem addNodesItem = kiMenu.getItem(0); 
        JMenuItem addListItem = kiMenu.getItem(1); 
 
        boolean hasNetworks = !Cytoscape.getNetworkSet().isEmpty(); 
         
        // workaround for networks not being destroyed until AFTER event 
        if (networkDestroyed && Cytoscape.getNetworkSet().size() == 1) { 
            hasNetworks = false;  
        } 
         
        // disable / enable items that require networks 
        addNodesItem.setEnabled(hasNetworks); 
        addListItem.setEnabled(hasNetworks); 
    } 
 
    private static JMenu getKamPluginMenu() { 
        final JMenu pluginMenu = Cytoscape.getDesktop().getCyMenus() 
                .getOperationsMenu(); 
 
        JMenu kiMenu = null
        for (final Component menu : pluginMenu.getMenuComponents()) { 
            if (menu == null) { 
                continue
            } 
 
            if (menu instanceof JMenu 
                    && KAM_PLUGIN_SUBMENU.equals(((JMenu) menu).getText())) { 
                kiMenu = (JMenu) menu; 
                break
            } 
        } 
        return kiMenu; 
    } 
     
 
    /**
     * Load the {@link VisualStyle visual style} into the vizmapper. 
     */
 
    private void loadKAMStyle() { 
        final VisualMappingManager vismanager = Cytoscape.getVisualMappingManager(); 
 
        final CalculatorCatalog ccat = vismanager.getCalculatorCatalog(); 
        VisualStyle visualStyle = ccat.getVisualStyle(KAM_STYLE); 
        if (visualStyle == null) { 
            loadKAMStyleFromFile(); 
            visualStyle = ccat.getVisualStyle(KAM_STYLE); 
        } 
    } 
     
    // TODO better exception handling 
    private void loadKAMStyleFromFile() { 
        // XXX is there a way to do this statically? getClass requires the  
        // current class instance 
        InputStream in = this.getClass().getResourceAsStream(KAM_STYLE_FILE); 
        File f = null
        try { 
            f = File.createTempFile("viz"null); 
            writeInputStreamIntoFile(in, f); 
        } catch (IOException e) { 
            log.warn("Error loading style", e); 
            return
        } finally { 
            Utility.closeSilently(in); 
        } 
         
        if (!f.exists() || !f.canRead()) { 
            return
        } 
         
        // load style 
        Cytoscape.firePropertyChange(Cytoscape.VIZMAP_LOADED, null, f.getAbsolutePath()); 
    } 
     
    private static void writeInputStreamIntoFile(InputStream in, File f) 
            throws IOException { 
        BufferedInputStream bis = new BufferedInputStream(in); 
        FileOutputStream fos = null
        try { 
            fos = new FileOutputStream(f); 
             
            byte[] buffer = new byte[1024]; 
            int len = 0
            while ((len = bis.read(buffer)) > 0) { 
                fos.write(buffer, 0, len); 
            } 
        } finally { 
            Utility.closeSilently(bis); 
            Utility.closeSilently(fos); 
        } 
    } 
     
    /**
     * Simple feedback mail action 
     *  
     * @author James McMahon <jmcmahon@selventa.com> 
     */
 
    private static final class FeedbackMailToAction extends CytoscapeAction { 
        private static final long serialVersionUID = -2109588518850444632L
 
        public FeedbackMailToAction() { 
            super("Send Feedback"); 
        } 
 
        /**
         * {@inheritDoc} 
         */
 
        @Override 
        public void actionPerformed(ActionEvent event) { 
            String supportEmail = "support@belframework.org"
            String subject = "KAM%20Navigator%20Feedback"
            String body = "Autogenerated information: [" 
                    + "KAM Navigator Version:" + KAM_NAVIGATOR_VERSION 
                    + ", Cytoscape Version:" + CytoscapeVersion.version  
                    + ", OS Name:" + System.getProperty("os.name")  
                    + ", OS Version:" + System.getProperty("os.version"
                    + ", Java Version:" + System.getProperty("java.version"
                    + "]"
            body = urlEncode(body); 
             
            String uriString = "mailto:" + supportEmail + "?subject=" + subject 
                    + "&body=" + body; 
            URI uri = null
            try { 
                uri = new URI(uriString); 
            } catch (URISyntaxException e) { 
                log.error("Error generating support e-mail", e); 
                return
            } 
             
            try { 
                Desktop.getDesktop().mail(uri); 
            } catch (IOException e) { 
                log.error("Error generating support e-mail", e); 
            } 
        } 
     
        /**
         * Url encode a string. 
         *  
         * Taken from http://stackoverflow.com/a/4605816/20774 
         */
 
        private static String urlEncode(String input) { 
            StringBuilder resultStr = new StringBuilder(); 
            for (char ch : input.toCharArray()) { 
                if (isUnsafe(ch)) { 
                    resultStr.append('%'); 
                    resultStr.append(toHex(ch / 16)); 
                    resultStr.append(toHex(ch % 16)); 
                } else { 
                    resultStr.append(ch); 
                } 
            } 
            return resultStr.toString(); 
        } 
     
        private static char toHex(int ch) { 
            return (char) (ch < 10 ? '0' + ch : 'A' + ch - 10); 
        } 
     
        private static boolean isUnsafe(char ch) { 
            if (ch > 128 || ch < 0) { 
                return true
            } 
            return " %$&+,/:;=?@<>#%".indexOf(ch) >= 0
        } 
    } 
 
    /**
     * The {@link CytoscapeAction action} to trigger the Add KAM Nodes dialog. 
     * 
     * @author Anthony Bargnesi <abargnesi@selventa.com> 
     */
 
    private static final class SearchKAMDialogAction extends CytoscapeAction { 
        private static final long serialVersionUID = 2243171495622023060L
 
        public SearchKAMDialogAction() { 
            super("Add KAM Nodes"); 
        } 
 
        /**
         * {@inheritDoc} 
         */
 
        @Override 
        public void actionPerformed(ActionEvent event) { 
            SearchKamDialog kcdialog = new SearchKamDialog(); 
            kcdialog.setVisible(true); 
        } 
    } 
 
    /**
     * The {@link CytoscapeAction action} to trigger the Add KAM List dialog. 
     *  
     * @author James McMahon <jmcmahon@selventa.com> 
     */
 
    private static final class SearchKAMListDialogAction extends CytoscapeAction { 
        private static final long serialVersionUID = -5051721582642478695L
 
        public SearchKAMListDialogAction() { 
            super(SearchKamListDialog.TITLE); 
        } 
 
        /**
         * {@inheritDoc} 
         */
 
        @Override 
        public void actionPerformed(ActionEvent event) { 
            SearchKamListDialog dialog = new SearchKamListDialog(); 
            dialog.setVisible(true); 
        } 
    } 
 
    /**
     * Defines a {@link CytoscapeAction cytoscape action} to launch the 
     * <em>BELFramework Configuration</em> dialog.  This allows the cytoscape 
     * user to configure their access to the BELFramework Web API. 
     * 
     * @author Anthony Bargnesi <abargnesi@selventa.com> 
     */
 
    private static final class SettingsDialogAction extends CytoscapeAction { 
        private static final long serialVersionUID = 5424095704897475438L
 
        public SettingsDialogAction() { 
            super("BELFramework Configuration"); 
        } 
 
        /**
         * {@inheritDoc} 
         */
 
        @Override 
        public void actionPerformed(ActionEvent event) { 
            SettingsDialog settingsDialog = new SettingsDialog(); 
            settingsDialog.setVisible(true); 
        } 
    } 
}