/* * Licensed to "Neo Technology," Network Engine for Objects in Lund AB * (http://neotechnology.com) under one or more contributor license agreements. * See the NOTICE file distributed with this work for additional information * regarding copyright ownership. Neo Technology licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License * at (http://www.apache.org/licenses/LICENSE-2.0). Unless required by * applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS * OF ANY KIND, either express or implied. See the License for the specific * language governing permissions and limitations under the License. */ package org.neo4j.neoclipse.view; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import org.eclipse.core.runtime.Preferences.IPropertyChangeListener; import org.eclipse.core.runtime.Preferences.PropertyChangeEvent; import org.eclipse.draw2d.ChangeEvent; import org.eclipse.draw2d.ChangeListener; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.viewers.DoubleClickEvent; import org.eclipse.jface.viewers.IDoubleClickListener; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Composite; import org.eclipse.ui.ISelectionListener; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.part.ViewPart; import org.eclipse.ui.views.properties.IPropertySheetPage; import org.eclipse.zest.core.viewers.AbstractZoomableViewer; import org.eclipse.zest.core.viewers.GraphViewer; import org.eclipse.zest.core.viewers.IZoomableWorkbenchPart; import org.eclipse.zest.layouts.LayoutStyles; import org.eclipse.zest.layouts.algorithms.SpringLayoutAlgorithm; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.NotFoundException; import org.neo4j.graphdb.Relationship; import org.neo4j.neoclipse.Activator; import org.neo4j.neoclipse.event.NeoclipseEvent; import org.neo4j.neoclipse.event.NeoclipseEventListener; import org.neo4j.neoclipse.event.NeoclipseListenerList; import org.neo4j.neoclipse.graphdb.GraphCallable; import org.neo4j.neoclipse.graphdb.GraphDbServiceEvent; import org.neo4j.neoclipse.graphdb.GraphDbServiceEventListener; import org.neo4j.neoclipse.graphdb.GraphDbServiceManager; import org.neo4j.neoclipse.graphdb.GraphDbServiceMode; import org.neo4j.neoclipse.graphdb.GraphDbServiceStatus; import org.neo4j.neoclipse.graphdb.GraphRunnable; import org.neo4j.neoclipse.help.HelpContextConstants; import org.neo4j.neoclipse.preference.Preferences; import org.neo4j.neoclipse.property.NeoPropertySheetPage; import org.neo4j.neoclipse.reltype.RelationshipTypeView; /** * This class is a view that shows the contents of a Neo database as a graph of * connected objects. * * @author Peter Hänsgen * @author Anders Nawroth */ public class NeoGraphViewPart extends ViewPart implements IZoomableWorkbenchPart { /** * The Eclipse view ID. */ public static final String ID = "org.neo4j.neoclipse.view.NeoGraphViewPart"; /** * Max number of nodes to try to find a better starting point. */ private static final int MAX_NODE_TRIES = 1000; /** * The property sheet page. */ protected NeoPropertySheetPage propertySheetPage; /** * The graph. */ protected GraphViewer viewer; /** * Keep track of visited nodes. */ private BrowserHistory browserHistory = null; /** * The depth how deep we should traverse into the network. */ private int traversalDepth = 1; private final List<Node> currentSelectedNodes = new ArrayList<Node>(); private final List<Relationship> currentSelectedRels = new ArrayList<Relationship>(); private RelationshipTypeView relTypeView; private final List<InputChangeListener> listeners = new ArrayList<InputChangeListener>(); private Node previousInputNode = null; private final NeoclipseListenerList relColorChange = new NeoclipseListenerList(); private NeoGraphMenu menu; /** * Keep track of the current database state. */ private boolean dirty = false; private static final IStructuredSelection EMPTY_SELECTION; static { EMPTY_SELECTION = new IStructuredSelection() { public boolean isEmpty() { return true; } @SuppressWarnings( "rawtypes" ) public List toList() { return Collections.emptyList(); } public Object[] toArray() { return toList().toArray(); } public int size() { return 0; } @SuppressWarnings( "rawtypes" ) public Iterator iterator() { return null; } public Object getFirstElement() { return null; } }; } /** * Creates the view. */ @Override public void createPartControl( final Composite parent ) { viewer = new GraphViewer( parent, SWT.NONE ); viewer.setUseHashlookup( true ); viewer.setContentProvider( new NeoGraphContentProvider( this ) ); viewer.addDoubleClickListener( new NeoGraphDoubleClickListener() ); viewer.setLayoutAlgorithm( new SpringLayoutAlgorithm( LayoutStyles.NO_LAYOUT_NODE_RESIZING ) ); NeoGraphLabelProvider labelProvider = NeoGraphLabelProviderWrapper.getInstance(); viewer.setLabelProvider( labelProvider ); addListener( labelProvider ); getSite().getPage().addSelectionListener( ID, new SelectionChangeHandler() ); getSite().getPage().addSelectionListener( RelationshipTypeView.ID, new RelTypeSelectionChangeHandler() ); menu = new NeoGraphMenu( this ); GraphDbServiceManager sm = Activator.getDefault().getGraphDbServiceManager(); sm.addServiceEventListener( new NeoGraphServiceEventListener() ); getSite().setSelectionProvider( viewer ); Activator.getDefault().getPluginPreferences().addPropertyChangeListener( new PreferenceChangeHandler() ); PlatformUI.getWorkbench().getHelpSystem().setHelp( viewer.getControl(), HelpContextConstants.NEO_GRAPH_VIEW_PART ); } /** * Get current relationship type view. * * @return */ public RelationshipTypeView getRelTypeView() { return relTypeView; } /** * Set the current relationship type view. * * @param relTypeView */ private void setRelTypeView( final RelationshipTypeView relTypeView ) { this.relTypeView = relTypeView; } /** * Add listener for input changes. * * @param listener */ public void addListener( final InputChangeListener listener ) { listeners.add( listener ); } /** * Add listener for changes in the relationship colors setting. * * @param listener */ public void addRelColorChangeListener( final NeoclipseEventListener listener ) { relColorChange.add( listener ); } /** * Notify listeners of new input node. * * @param node */ private void notifyListeners( final Node node ) { Activator.getDefault().getGraphDbServiceManager().submitTask( new Runnable() { public void run() { notifyTheListeners( node ); } }, "notify listeners" ); } private void notifyTheListeners( final Node node ) { for ( InputChangeListener listener : listeners ) { listener.inputChange( node ); } // make sure to update only these label colors if ( previousInputNode != null ) { refresh( previousInputNode, true ); } refresh( node, true ); previousInputNode = node; } /** * Show or hide relationship colors. * * @param state set true to display */ public void setShowRelationshipColors( final boolean state ) { getLabelProvider().getViewSettings().setShowRelationshipColors( state ); relColorChange.notifyListeners( new NeoclipseEvent( Boolean.valueOf( state ) ) ); refreshPreserveLayout(); } /** * Set context menu to the correct state. */ public void updateMenuState() { GraphDbServiceManager sm = Activator.getDefault().getGraphDbServiceManager(); if ( !sm.isReadOnlyMode() ) { int selectedNodeCount = currentSelectedNodes.size(); int selectedRelationshipCount = currentSelectedRels.size(); menu.setEnableDeleteAction( selectedNodeCount > 0 || selectedRelationshipCount > 0 ); boolean rel = selectedNodeCount == 2; boolean outIn = selectedNodeCount > 0; menu.setEnabledRelActions( rel, outIn, outIn ); } } /** * Gets the current node. * * @return current node */ public Node getCurrentNode() { try { Object node = viewer.getInput(); if ( node instanceof Node ) { return (Node) node; } return Activator.getDefault().getGraphDbServiceManager().submitTask( new GraphCallable<Node>() { public Node call( final GraphDatabaseService graphDb ) { Node refNode = graphDb.getReferenceNode(); if ( refNode != null ) { return refNode; } throw new NotFoundException( "No current node could be found." ); } }, "get current node" ).get(); } catch ( Exception e ) { throw new NotFoundException( "No current node could be found." ); } } /* * Add the current node to the view. Used when traversal find nothing to * show. * @SuppressWarnings( "restriction" ) public void addCurrentNode() { * viewer.addNode( getCurrentNode() ); } */ /** * Updates the content of the status bar. */ private void refreshStatusBar() { UiHelper.asyncExec( new Runnable() { public void run() { StringBuilder str = new StringBuilder( 64 ); str.append( "Traversal depth: " ).append( traversalDepth ); str.append( " Nodes: " ).append( viewer.getGraphControl().getNodes().size() ); str.append( " Relationships: " ).append( viewer.getGraphControl().getConnections().size() ); getViewSite().getActionBars().getStatusLineManager().setMessage( str.toString() ); } } ); } /** * Returns the viewer that contains the graph. */ public GraphViewer getViewer() { return viewer; } /** * Returns the graph viewer for zooming. */ public AbstractZoomableViewer getZoomableViewer() { return viewer; } /** * This is how the framework determines which interfaces we implement. */ @Override public Object getAdapter( @SuppressWarnings( "rawtypes" ) final Class key ) { if ( key.equals( IPropertySheetPage.class ) ) { return getPropertySheetPage(); } else { return super.getAdapter( key ); } } /** * This accesses a cached version of the property sheet. */ public IPropertySheetPage getPropertySheetPage() { if ( propertySheetPage == null ) { propertySheetPage = new NeoPropertySheetPage(); propertySheetPage.addChangeListener( new PropertyChangeHandler() ); } return propertySheetPage; } /** * Cleans up. */ @Override public void dispose() { cleanTransactionBeforeShutdown(); if ( propertySheetPage != null ) { propertySheetPage.dispose(); } super.dispose(); } /** * Make sure the transaction is clean before Neo is to be * shutdown/restarted. */ public void cleanTransactionBeforeShutdown() { // clear the properties view if it hasn't already // been disposed if ( !propertySheetPage.getControl().isDisposed() ) { getPropertySheetPage().selectionChanged( NeoGraphViewPart.this, EMPTY_SELECTION ); } if ( !dirty ) { return; // no need to do anything } if ( viewer.getContentProvider() != null ) { viewer.setInput( null ); } GraphDbServiceManager sm = Activator.getDefault().getGraphDbServiceManager(); if ( MessageDialog.openQuestion( null, "Stopping the database", "There are changes that are not commited to the database. Do you want to commit (save) them?" ) ) { sm.commit(); } else { sm.rollback(); } } /** * Sets the focus. */ @Override public void setFocus() { viewer.getControl().setFocus(); } /** * Go back to previous node. */ public void goBack() { Node node = getBrowserHistory().getPrevious(); if ( node != null ) { showNode( node ); } updateNavStatus(); } /** * Go forward to next node. */ public void goForward() { Node node = getBrowserHistory().getNext(); if ( node != null ) { showNode( node ); } updateNavStatus(); } /** * Focuses the view on the reference node. */ public void showReferenceNode() { try { Activator.getDefault().getGraphDbServiceManager().executeTask( new GraphRunnable() { public void run( final GraphDatabaseService graphDb ) { Node node = graphDb.getReferenceNode(); setInput( node ); } }, "show reference node" ); } catch ( Exception e ) { ErrorMessage.showDialog( "Navigation error", e ); } } /** * Focuses the view on the reference node or some other node. If the * reference node has no relationships, it will try to find a node that has * relationships. */ public void showSomeNode() { try { final GraphDbServiceManager gsm = Activator.getDefault().getGraphDbServiceManager(); gsm.submitTask( new GraphRunnable() { public void run( final GraphDatabaseService graphDb ) { if ( graphDb == null ) { return; } Node node = graphDb.getReferenceNode(); if ( !node.hasRelationship() ) { // so, find a more friendly node if possible! int tries = 0; for ( Node candidateNode : graphDb.getAllNodes() ) { if ( node.equals( candidateNode ) ) { continue; } if ( candidateNode.hasRelationship() ) { node = candidateNode; break; } if ( tries++ > MAX_NODE_TRIES ) { break; } } } setInput( node ); gsm.submitDisplayTask( new Runnable() { public void run() { NeoGraphViewPart.this.refreshStatusBar(); } }, "refresh statusbar" ); } }, "show some node" ); } catch ( Exception e ) { ErrorMessage.showDialog( "Graph view", e ); } } /** * Focuses the view on the node with the given id. */ public void showNode( final long nodeId ) { try { GraphDbServiceManager gsm = Activator.getDefault().getGraphDbServiceManager(); gsm.submitTask( new GraphRunnable() { public void run( final GraphDatabaseService graphDb ) { if ( graphDb != null ) { try { Node node = graphDb.getNodeById( nodeId ); setInput( node ); } catch ( Exception e ) { showSomeNode(); } } } }, "show node byid" ); } catch ( Exception e ) { ErrorMessage.showDialog( "Can't show node", e ); } } /** * Focuses the view on the given node. */ public void showNode( final Node node ) { try { Activator.getDefault().getGraphDbServiceManager().submitTask( new Runnable() { public void run() { setInput( node ); } }, "show node" ); } catch ( Exception e ) { ErrorMessage.showDialog( "Can't show node", e ); } } /** * Returns the current traversal depth. */ public int getTraversalDepth() { return traversalDepth; } /** * Increments the traversal depth. */ public void incTraversalDepth() { traversalDepth++; refreshViewer(); viewer.applyLayout(); if ( traversalDepth > 0 ) { menu.setEnabledDecAction( true ); } } /** * Decrements the traversal depth. */ public void decTraversalDepth() { if ( traversalDepth > 0 ) { traversalDepth--; refreshViewer(); viewer.applyLayout(); if ( traversalDepth < 1 ) { menu.setEnabledDecAction( false ); } } } /** * Refreshes the view. */ public void refresh() { refreshViewer(); viewer.applyLayout(); } /** * Refreshes the view without changing the layout. */ public void refreshPreserveLayout() { refreshViewer(); } /** * Refresh viewer and status bar as well. */ private void refreshViewer() { refresh( false ); } public void refresh( final boolean updateLabels ) { UiHelper.asyncExec( new Runnable() { public void run() { disableDelete(); viewer.refresh( updateLabels ); if ( viewer.getGraphControl().getNodes().size() == 0 ) { // will take care of if the input node // gets deleted or disappears in a rollback showSomeNode(); } refreshStatusBar(); } } ); } /** * Refresh the graph view. * * @param element * @param updateLabels */ public void refresh( final Object element, final boolean updateLabels ) { UiHelper.asyncExec( new Runnable() { public void run() { disableDelete(); if ( element == null ) { viewer.refresh( updateLabels ); } else { viewer.refresh( element, updateLabels ); } refreshStatusBar(); } } ); } /** * Disable the delete action. (we need this in some places to avoid ending * up in an inconsistent state due to bugs in the underlying frameworks) */ private void disableDelete() { currentSelectedNodes.clear(); currentSelectedRels.clear(); updateMenuState(); } /** * Get label provider. * * @return current label provider */ public NeoGraphLabelProvider getLabelProvider() { return (NeoGraphLabelProvider) viewer.getLabelProvider(); } /** * Get browser history. * * @return */ private BrowserHistory getBrowserHistory() { if ( browserHistory == null ) { browserHistory = new BrowserHistory(); } return browserHistory; } /** * Set new input for the view. * * @param node the node to use as input/start */ public void setInput( final Node node ) { UiHelper.asyncExec( new Runnable() { public void run() { viewer.setInput( node ); if ( node != null ) { UiHelper.asyncExec( new Runnable() { public void run() { notifyListeners( node ); } } ); getBrowserHistory().add( node ); } updateNavStatus(); } } ); } /** * Update navigation buttons according to current status. Must be called * after all browser history-related operations. */ private void updateNavStatus() { menu.setEnabledBackAction( getBrowserHistory().hasPrevious() ); menu.setEnabledForwardAction( getBrowserHistory().hasNext() ); } /** * Updates the view according to service changes. */ private class NeoGraphServiceEventListener implements GraphDbServiceEventListener { /** * Refreshes the input source of the view. */ public void serviceChanged( final GraphDbServiceEvent event ) { UiHelper.asyncExec( new Runnable() { public void run() { handleServiceChange( event ); } } ); } private void handleServiceChange( final GraphDbServiceEvent event ) { if ( event.getStatus() == GraphDbServiceStatus.STOPPING ) { browserHistory.clear(); updateNavStatus(); menu.setEnabledSyncAction( false ); menu.setEnabledStartAction( true ); menu.setEnabledStopAction( false ); menu.setEnabledShowRefNodeAction( false ); menu.setEnabledRefreshAction( false ); // when called during shutdown the content provider may already // have been disposed if ( getViewer().getContentProvider() != null ) { setInput( null ); } } else if ( event.getStatus() == GraphDbServiceStatus.STARTED ) { // throw away old relationship colors getLabelProvider().refreshRelationshipColors(); menu.setEnabledSyncAction( true ); menu.setEnabledStartAction( false ); menu.setEnabledStopAction( true ); menu.setEnabledShowRefNodeAction( true ); menu.setEnabledRefreshAction( true ); if ( event.isReadOnlyMode() ) { // set up menus for read-only mode menu.setEnableDeleteAction( false ); menu.setEnabledRelActions( false, false, false ); menu.setEnabledCommitAction( false ); menu.setEnabledRollbackAction( false ); } else { // TODO set up menus for read/write mode // - not needed? } // showSomeNode(); } else if ( event.getStatus() == GraphDbServiceStatus.ROLLBACK ) { refresh( true ); setDirty( false ); } else if ( event.getStatus() == GraphDbServiceStatus.COMMIT ) { setDirty( false ); } } } /** * Update UI according to the state of the database. * * @param dirty */ public void setDirty( final boolean dirty ) { // TODO add a StructureChangeEvent instead of having // this method public? GraphDbServiceManager sm = Activator.getDefault().getGraphDbServiceManager(); this.dirty = dirty; if ( sm.isReadOnlyMode() ) { // menu.setEnabledSyncAction( !dirty ); return; } menu.setEnabledSyncAction( !dirty ); menu.setEnabledCommitAction( dirty ); menu.setEnabledRollbackAction( dirty ); } /** * Handles double clicks on graph figures. */ private class NeoGraphDoubleClickListener implements IDoubleClickListener { /** * Sets the selected node as input for the viewer. */ public void doubleClick( final DoubleClickEvent event ) { StructuredSelection sel = (StructuredSelection) event.getSelection(); Object s = sel.getFirstElement(); if ( ( s != null ) && ( s instanceof Node ) ) { Node node = (Node) s; if ( viewer != event.getViewer() ) { throw new IllegalStateException( "Double click event comes from wrong view." ); } setInput( node ); refreshStatusBar(); } } } /** * Class to handle changes in selection of this view. */ private class SelectionChangeHandler implements ISelectionListener { /** * Handles selection, making the context menu look right. */ public void selectionChanged( final IWorkbenchPart part, final ISelection selection ) { currentSelectedNodes.clear(); currentSelectedRels.clear(); if ( !( selection instanceof IStructuredSelection ) ) { updateMenuState(); return; } IStructuredSelection structSel = (IStructuredSelection) selection; Iterator<?> iter = structSel.iterator(); while ( iter.hasNext() ) { Object o = iter.next(); if ( o instanceof Node ) { currentSelectedNodes.add( (Node) o ); } else if ( o instanceof Relationship ) { currentSelectedRels.add( (Relationship) o ); } } updateMenuState(); } } /** * Class that handles changes in the relationship properties view. */ private class RelTypeSelectionChangeHandler implements ISelectionListener { /** * Handles selection, just updating the relTypeView reference. */ public void selectionChanged( final IWorkbenchPart part, final ISelection selection ) { if ( part instanceof RelationshipTypeView ) { setRelTypeView( (RelationshipTypeView) part ); } } } /** * Get current selected nodes. * * @return */ public List<Node> getCurrentSelectedNodes() { return currentSelectedNodes; } /** * Get current selected relationships. * * @return */ public List<Relationship> getCurrentSelectedRels() { return currentSelectedRels; } /** * Class that responds to changes in properties. */ private class PropertyChangeHandler implements ChangeListener { /** * Handle change in properties. */ public void handleStateChanged( final ChangeEvent event ) { UiHelper.asyncExec( new Runnable() { public void run() { refresh( event.getSource(), true ); if ( event.getPropertyName() != null ) { setDirty( true ); } } } ); } } /** * Class that responds to changes in preferences. */ private class PreferenceChangeHandler implements IPropertyChangeListener { /** * Forward event, then refresh view. */ public void propertyChange( final PropertyChangeEvent event ) { UiHelper.asyncExec( new Runnable() { public void run() { handleChange( event ); } } ); } private void handleChange( final PropertyChangeEvent event ) { String property = event.getProperty(); if ( Preferences.CONNECTION_MODE.equals( property ) ) { GraphDbServiceMode newConnectionMode; newConnectionMode = GraphDbServiceMode.valueOf( (String) event.getNewValue() ); GraphDbServiceManager sm = Activator.getDefault().getGraphDbServiceManager(); cleanTransactionBeforeShutdown(); sm.setGraphServiceMode( newConnectionMode ); // TODO refresh what needs to be refreshed here } else if ( Preferences.DATABASE_LOCATION.equals( property ) ) { // handle change in database location // cleanTransactionBeforeShutdown(); // TODO UGLY traversalDepth = 0; incTraversalDepth(); } else { if ( NeoGraphLabelProviderWrapper.getInstance().propertyChanged( event ) ) { refresh( true ); } } } } }