NetBeans provides a very nice Database Explorer, and in NB 6.0 the API will be more accessible for extensions (most important D&D support ...), but there is one thing that I've been missing: A visual component, that shows the database structure, like in DbVisualizer. DbVisualizer has many more features, but that is the one I was missing the most. The main asset of such a visual component is that it can show how the tables are related regarding foreign keys. Everything else can be shown in the available tree view as well. The tables and foreignkeys can be shown as a graph with the tables being the nodes, and the foreign keys building the edges; a perfect job for the Visual library:
The Visual Library ships with a lot of examples, and we can choose one to get started and adjust it to our needs. So lets go:
Now lets go where the action is:
The Wizard automatically adds the Nodes and Actions API, but it won't add the API that contains the ConnectionNode, so we will have to do that to fix the imports:
Implementation Version means that we use the whole library, not just the public part. Normally you wouldn't do that, because the non public part of the library might change and this would break our code. Unfortunately the part of the API we need is not (yet?) public. So for the scope of a tutorial we will just ignore that rule.
You will see a grayed out icon in the toolbar. If you go to the Runtime tab and click on a connection, the Toolbar button will be activated.
Now that we have our Action we want it to do something. In our case it should launch a TopComponent that shows the DatabaseStructure. So lets create a topcomponentand link it with our action:
The JPanel is going to hold our view later on, now we will link our action to the TopComponent. Two classes were generated by the wizard: DBVisualExplorerTopComponent and DBVisualExplorerAction. We won't use the latter, but we will ransack it for some useful code:
You might want to clean up a bit now. There are two entries related to the generated DBVisualExplorerAction in the layer.xml:
Everything up to now was boilerplate code, things you probably have done before, or will do like this or similar a lot of times when developing NetBeans modules. Now for the fun part! We will use the Visual Library to create a GraphScene that we can use later on to display the DatabaseStructure. First we will use one of David Kaspar's cool examples to get started and create a scene. Then we will get a view from the scene and add it to our TopComponent.
So first we make the visual library visible for our project:
Now we can create our Scene. We will base ours on VMDGraphScene.
package your.package.here
import java.awt.Image;
import java.awt.Point;
import java.util.Arrays;
import java.util.List;
import org.netbeans.api.visual.vmd.VMDGraphScene;
import org.netbeans.api.visual.vmd.VMDNodeWidget;
import org.netbeans.api.visual.vmd.VMDPinWidget;
import org.openide.util.Utilities;
public class DBGraphScene extends VMDGraphScene{
private static final Image IMAGE_LIST = Utilities.loadImage ("de/eppleton/visualexplorer/resources/list_16.png"); // NOI18N
private static final Image IMAGE_CANVAS = Utilities.loadImage ("de/eppleton/visualexplorer/resources/custom_displayable_16.png"); // NOI18N
private static final Image IMAGE_COMMAND = Utilities.loadImage ("de/eppleton/visualexplorer/resources/command_16.png"); // NOI18N
private static final Image IMAGE_ITEM = Utilities.loadImage ("de/eppleton/visualexplorer/resources/item_16.png"); // NOI18N
private static final Image GLYPH_PRE_CODE = Utilities.loadImage ("de/eppleton/visualexplorer/resources/preCodeGlyph.png"); // NOI18N
private static final Image GLYPH_POST_CODE = Utilities.loadImage ("de/eppleton/visualexplorer/resources/postCodeGlyph.png"); // NOI18N
private static final Image GLYPH_CANCEL = Utilities.loadImage ("de/eppleton/visualexplorer/resources/cancelGlyph.png"); // NOI18N
private static int nodeID = 1;
private static int edgeID = 1;
/** Creates a new instance of DBGraphScene */
public DBGraphScene() {
String mobile = createNode (this, 100, 100, IMAGE_LIST, "menu", "List", null);
createPin (this, mobile, "start", IMAGE_ITEM, "Start", "Element");
String game = createNode (this, 600, 100, IMAGE_CANVAS, "gameCanvas", "MyCanvas", Arrays.asList (GLYPH_PRE_CODE, GLYPH_CANCEL, GLYPH_POST_CODE));
createPin (this, game, "ok", IMAGE_COMMAND, "okCommand1", "Command");
createEdge (this, "start", game);
createEdge (this, "ok", mobile);
}
private static String createNode (VMDGraphScene scene, int x, int y, Image image, String name, String type, List<Image> glyphs) {
String nodeID = "node" + DBGraphScene.nodeID ++;
VMDNodeWidget widget = (VMDNodeWidget) scene.addNode (nodeID);
widget.setPreferredLocation (new Point (x, y));
widget.setNodeProperties (image, name, type, glyphs);
scene.addPin (nodeID, nodeID + VMDGraphScene.PIN_ID_DEFAULT_SUFFIX);
return nodeID;
}
private static void createPin (VMDGraphScene scene, String nodeID, String pinID, Image image, String name, String type) {
((VMDPinWidget) scene.addPin (nodeID, pinID)).setProperties (name, null);
}
private static void createEdge (VMDGraphScene scene, String sourcePinID, String targetNodeID) {
String edgeID = "edge" + DBGraphScene.edgeID ++;
scene.addEdge (edgeID);
scene.setEdgeSource (edgeID, sourcePinID);
scene.setEdgeTarget (edgeID, targetNodeID + VMDGraphScene.PIN_ID_DEFAULT_SUFFIX);
}
}
This class is based on David's VMDTest. Only three methods are necessary to create the graph. createNode creates a VMDNodeWidget, an advanced Widget that can hold a dropdown list with so called "Pins". The Pins are added to the nodes with the createPin method. They can be used as source or target for a connection (edge) between two nodes. And at last the connection iis established via the createEdge method. At the top there are some Images declared. We will need to create a folder for the images and copy them from the Visual library (Visual-Library-2.0-pre2/sources/graph/examples/src/test/resources). We will use it to simply provide something to show. Don't care about the content... Let's see if it works:
Beautiful, isn't it? We have linked the TopComponent to our DBGraphScene. In the next section we'll use this mechanism to add real data.
Our TopComponent looks nice, but it's got nothing to do with our DatabaseConnection. Let's fix that. We will get the Database information from our ConnectionNode:
protected void performAction(Node[] activatedNodes) {
ConnectionNode connectionNode = (ConnectionNode) activatedNodes[0].getLookup().lookup(ConnectionNode.class);
DatabaseConnection databaseConnection = connectionNode.getInfo().getDatabaseConnection().getDatabaseConnection();
if (databaseConnection.getJDBCConnection() == null) {
ConnectionManager.getDefault().showConnectionDialog(databaseConnection);
}
Connection connection = databaseConnection.getJDBCConnection();
DBVisualExplorerTopComponent win = DBVisualExplorerTopComponent.findInstance();
win.open();
win.requestActive();
win.setConnection(connection);
}
private DBVisualExplorerTopComponent() {
System.out.println("initComponents");
initComponents();
setName(NbBundle.getMessage(DBVisualExplorerTopComponent.class, "CTL_DBVisualExplorerTopComponent"));
setToolTipText(NbBundle.getMessage(DBVisualExplorerTopComponent.class, "HINT_DBVisualExplorerTopComponent"));
// setIcon(Utilities.loadImage(ICON_PATH, true));
}
public void setConnection(Connection connection){
scene = new DBGraphScene(connection);
jPanel1.removeAll();
JComponent component = scene.createView();
jPanel1.add(component);
}
Again the IDE will complain that there is no such Constructor for DBGraphScene
import java.awt.Image;
import java.awt.Point;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import org.netbeans.api.visual.vmd.VMDGraphScene;
import org.netbeans.api.visual.vmd.VMDNodeWidget;
import org.netbeans.api.visual.vmd.VMDPinWidget;
import org.openide.util.Utilities;
/**
*
* @author Anton Epple
*/
public class DBGraphScene extends VMDGraphScene{
private static final Image IMAGE_LIST = Utilities.loadImage ("de/eppleton/visualexplorer/resources/list_16.png"); // NOI18N
private static final Image IMAGE_ITEM = Utilities.loadImage ("de/eppleton/visualexplorer/resources/item_16.png"); // NOI18N
private static int edgeID = 1;
/** Creates a new instance of DBGraphScene */
public DBGraphScene(Connection connection) {
try{
createSceneFromConnection(connection);
}catch (SQLException e){
e.printStackTrace();
}
}
private void createSceneFromConnection(Connection jdbcConnection) throws SQLException{
ArrayList <String>tables = new ArrayList<String>();
DatabaseMetaData databaseMetaData = jdbcConnection.getMetaData();
String[] names = {"TABLE"};
ResultSet resultSet = databaseMetaData.getTables(null, "%", "%", names);
while(resultSet.next()) {
String table = resultSet.getString("TABLE_NAME" );
tables.add(table);
createNode (this, (int)(Math.random()*800), (int)(Math.random()*800), IMAGE_LIST, table, "Table", null);
ResultSet columns = jdbcConnection.getMetaData().getColumns(null, null, table.toUpperCase(), "%");
while (columns.next()){
String columnName = columns.getString("COLUMN_NAME");
createPin (this, table, table+":"+columnName, IMAGE_ITEM, columnName, columnName);
}
}
for (String string : tables) {
ResultSet resultSet1 = databaseMetaData.getExportedKeys(null, null, string);
while(resultSet1.next()){
String pkTable = resultSet1.getString("PKTABLE_NAME");
String pkColumn = resultSet1.getString("PKCOLUMN_NAME");
String fkTable = resultSet1.getString("FKTABLE_NAME");
String fkColumn = resultSet1.getString("FKCOLUMN_NAME");
createEdge (this, fkTable+":"+fkColumn, pkTable+":"+pkColumn );
}
}
this.moveTo(null);
}
private static String createNode (VMDGraphScene scene, int x, int y, Image image, String name, String type, java.util.List<Image> glyphs) {
String nodeID = name;
VMDNodeWidget widget = (VMDNodeWidget) scene.addNode (nodeID);
widget.setPreferredLocation (new Point (x, y));
widget.setNodeProperties (image, name, type, glyphs);
return nodeID;
}
private static void createPin (VMDGraphScene scene, String nodeID, String pinID, Image image, String name, String type) {
((VMDPinWidget) scene.addPin (nodeID, pinID)).setProperties (name, null);
}
private static void createEdge (VMDGraphScene scene, String sourcePinID, String targetPinID) {
String edgeID = "edge" + DBGraphScene.edgeID ++;
scene.addEdge (edgeID);
System.out.println("createEdge "+sourcePinID+"<->"+targetPinID);
scene.setEdgeSource (edgeID, sourcePinID);
scene.setEdgeTarget (edgeID, targetPinID);
}
private void moveTo (Point point) {
int index = 0;
for (String node : getNodes ())
getSceneAnimator ().animatePreferredLocation (findWidget (node), point != null ? point : new Point (++ index * 100, index * 100));
}
}
The createSceneFromConnection method gets the DatabaseMetadata ( jdbcConnection.getMetaData() ) from the Connection. With the subsequent calls to getTables table structure is retrieved and the nodes of the graph are created. For every table getColumns is called and a Pin is added for every column to the table node. These pins can be used in the next step to create a connection between two tables. Now we iterate through the tables and call getExportedKeys to get hold of the ForeignKeys. For every exported key an edge is created between the pins of the related columns. The moveTo method does the animation, when the window is opened.
If you run the application again, you should see something like this:
We could enhance that with a nice graph layout algorithm ( I beautified the layout for the screenshot manually, but as far as I know David is working on a orthogonal layout, so this might be there out of the box quite soon also).