/*
* Copyright (c) 2008, SQL Power Group Inc.
*
* This file is part of SQL Power Library.
*
* SQL Power Library is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* SQL Power Library 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ca.sqlpower.swingui.db;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import org.apache.log4j.Logger;
import ca.sqlpower.sql.JDBCDataSource;
import ca.sqlpower.sql.JDBCDataSourceType;
import ca.sqlpower.sql.SPDataSource;
import ca.sqlpower.swingui.DataEntryPanel;
import ca.sqlpower.swingui.Messages;
import ca.sqlpower.swingui.ProgressWatcher;
import ca.sqlpower.swingui.SPSUtils;
import ca.sqlpower.util.JarScanClassLoader;
import ca.sqlpower.util.Monitorable;
public class JDBCDriverPanel extends JPanel implements DataEntryPanel {
/**
* The level in the tree that the drivers are located at.
*/
public static final int DRIVER_LEVEL = 2;
private static class DriverTreeCellRenderer extends DefaultTreeCellRenderer implements TreeCellRenderer {
private Icon jarFileIcon =
new ImageIcon(JDBCDriverPanel.class.getClassLoader().getResource("ca/sqlpower/swingui/db/folder_wrench.png")); //$NON-NLS-1$
private Icon driverIcon =
new ImageIcon(JDBCDriverPanel.class.getClassLoader().getResource("ca/sqlpower/swingui/db/wrench.png")); //$NON-NLS-1$
private Icon jarFileErrorIcon =
new ImageIcon(JDBCDriverPanel.class.getClassLoader().getResource("ca/sqlpower/swingui/db/folder_error.png")); //$NON-NLS-1$
private Icon driverErrorIcon =
new ImageIcon(JDBCDriverPanel.class.getClassLoader().getResource("ca/sqlpower/swingui/db/error.png")); //$NON-NLS-1$
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
int level = node.getLevel();
if (level == 0) {
// root is invisible in the driver tree
} else if (level == 1) {
setIcon(jarFileIcon);
for (int i = 0; i < node.getChildCount(); i++){
if(((DefaultMutableTreeNode) node.getChildAt(i)).getUserObject() instanceof Throwable){
setIcon(jarFileErrorIcon);
break;
}
}
} else if (level == DRIVER_LEVEL) {
if (node.getUserObject() instanceof Throwable) {
setForeground(Color.RED);
setIcon(driverErrorIcon);
setText(Messages.getString("JDBCDriverPanel.jarFileNotFound"));
} else {
setIcon(driverIcon);
}
} else {
throw new IllegalStateException("This renderer doesn't know how to handle node depth "+level); //$NON-NLS-1$
}
return this;
}
}
private static final Logger logger = Logger.getLogger(JDBCDriverPanel.class);
/**
* The current data source type (whose JDBC driver search path we're editting).
* This value will be null when there is no "current" data source type to edit.
*/
private JDBCDataSourceType dataSourceType;
/**
* This view shows the driver JAR files and the JDBC drivers they
* contain.
*/
private JTree driverTree;
/**
* This tree model holds the registered JAR files under the root,
* and lists the JDBC driver classes as children of each JAR file.
*/
private DefaultTreeModel dtm;
/**
* The file choosed used by the add action.
*/
private JFileChooser fileChooser;
/**
* progress bar stuff
*/
private JProgressBar progressBar;
private JLabel progressLabel;
private JButton delButton;
private JButton addButton;
private DefaultMutableTreeNode rootNode;
private final URI serverBaseURI;
/**
* Creates an interactive GUI for users to find JDBC drivers inside a
* collection of JAR files. The JAR files can be specified as built-in JARs
* on the classpath, JARs that reside on a remote server (such as a SQL
* Power Enterprise Server), or local files.
* <p>
* This is part of a larger system rooted at {@link DataSourceTypeDialogFactory}.
*
* @param serverBaseURI
* The base URI to the server where JAR files are stored. Can be
* null if server JAR specs are not in use.
* @see {@link SPDataSource#jarSpecToFile(String, ClassLoader, URI)} for a
* description of which types of file specifications are allowed.
*/
public JDBCDriverPanel(URI serverBaseURI) {
this.serverBaseURI = serverBaseURI;
// TODO default to most recent JDBC driver location
fileChooser = new JFileChooser();
setLayout(new BorderLayout());
rootNode = new DefaultMutableTreeNode("The Root"); //$NON-NLS-1$
dtm = new DefaultTreeModel(rootNode);
driverTree = new JTree(dtm);
driverTree.setRootVisible(false);
// Let the user delete multiple driver jars at once
driverTree.getSelectionModel().setSelectionMode(
TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
driverTree.addTreeSelectionListener(new TreeSelectionListener() {
public void valueChanged(TreeSelectionEvent e) {
// enabled when a driver has been selected
delButton.setEnabled(driverTree.getSelectionPath() != null);
}
});
driverTree.setCellRenderer(new DriverTreeCellRenderer());
add(new JScrollPane(driverTree), BorderLayout.CENTER);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
buttonPanel.add(addButton = new JButton(new AddAction()));
buttonPanel.add(delButton = new JButton(new DelAction()));
delButton.setEnabled(false);
addButton.setEnabled(false);
add(buttonPanel, BorderLayout.NORTH);
JPanel progressPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
progressBar = new JProgressBar();
progressBar.setStringPainted(true); //get space for the string
progressBar.setVisible(false);
progressPanel.add(progressBar);
progressLabel = new JLabel(Messages.getString("JDBCDriverPanel.scanningForJdbcDrivers")); //$NON-NLS-1$
progressLabel.setVisible(false);
progressPanel.add(progressLabel);
progressPanel.setPreferredSize(new Dimension(300, progressBar.getPreferredSize().height + 20));
add(progressPanel, BorderLayout.SOUTH);
setPreferredSize(new Dimension(400, 400));
}
/**
* Copies the pathnames to all the JAR files to
* ArchitectSession.addDriverJar().
*/
public boolean applyChanges() {
logger.debug("applyChanges"); //$NON-NLS-1$
List<String> driverList = new ArrayList<String>();
for (int i = 0, n = dtm.getChildCount(dtm.getRoot()); i < n; i++) {
driverList.add(((DefaultMutableTreeNode) dtm.getChild(dtm.getRoot(), i)).getUserObject().toString());
}
if (dataSourceType != null) dataSourceType.setJdbcJarList(driverList);
return true;
}
/**
* Does nothing.
*/
public void discardChanges() {
// really do nothing!
//editDsType(dataSourceType);
}
/**
* Switches to edit the given data source type.
*/
public void editDsType(JDBCDataSourceType dst) {
dataSourceType = dst;
dtm.setRoot(new DefaultMutableTreeNode());
if (dst != null) {
doLoad(dataSourceType.getJdbcJarList());
}
// enabled when a datasource has been selected
addButton.setEnabled(dst != null);
}
protected class AddAction extends AbstractAction {
public AddAction() {
super(Messages.getString("JDBCDriverPanel.addJarActionName")); //$NON-NLS-1$
}
public void actionPerformed(ActionEvent e) {
fileChooser.addChoosableFileFilter(SPSUtils.JAR_ZIP_FILE_FILTER);
fileChooser.setMultiSelectionEnabled(true);
int returnVal = fileChooser.showOpenDialog(JDBCDriverPanel.this);
if(returnVal == JFileChooser.APPROVE_OPTION) {
File[] files = fileChooser.getSelectedFiles();
List list = new ArrayList();
for(int ii=0; ii < files.length;ii++) {
list.add(files[ii].getAbsolutePath());
}
// always add the files to the data source type. if there are problems,
// they will be made visible to the user via the tree UI
for (int i = 0; i < files.length; i++) {
dataSourceType.addJdbcJar(files[i].getAbsolutePath());
}
doLoad(list);
}
}
}
/**
* Loads the given List of driver names into the tree, then starts
* a worker thread that searches for implementations of the JDBC Driver
* interface in them. As the worker finds JDBC Drivers in the JARs,
* it will add them to the tree.
*/
private void doLoad(List<String> list) {
logger.debug("about to start a worker", new Exception()); //$NON-NLS-1$
LoadJDBCDrivers ljd = new LoadJDBCDrivers(list);
LoadJDBCDriversWorker worker = new LoadJDBCDriversWorker(ljd);
// Create a progress bar to show JDBC driver load progress, and hide when finished
ProgressWatcher pw = new ProgressWatcher(progressBar,ljd,progressLabel);
pw.setHideLabelWhenFinished(true);
pw.start();
new Thread(worker).start();
}
private class DelAction extends AbstractAction {
public DelAction() {
super(Messages.getString("JDBCDriverPanel.removeJarActionName")); //$NON-NLS-1$
}
public void actionPerformed(ActionEvent e) {
for (TreePath p : driverTree.getSelectionPaths()) {
logger.debug(String.format("DelAction: p=%s, pathCount=%d", p, p.getPathCount())); //$NON-NLS-1$
if (p != null && p.getPathCount() >= 2) {
logger.debug("Removing: " + p.getPathComponent(1)); //$NON-NLS-1$
dtm.removeNodeFromParent((MutableTreeNode) p.getPathComponent(1));
dataSourceType.removeJdbcJar(p.getPathComponent(1).toString());
}
}
delButton.setEnabled(false);
}
}
private class LoadJDBCDriversWorker implements Runnable {
LoadJDBCDrivers ljd;
LoadJDBCDriversWorker (LoadJDBCDrivers ljd) {
this.ljd = ljd;
}
public void run() {
ljd.execute();
}
}
private class LoadJDBCDrivers implements Monitorable {
public boolean hasStarted = false;
public boolean finished = false;
private List<String> driverJarList = null;
private int jarCount = 0; // which member of the JAR file list are we currently processing
private JDBCScanClassLoader cl = null;
/**
* A monitorable scanner that finds all JDBC drivers within a given set
* of JAR files.
*
* @param driverJarList
* The list of JAR locations. These can be absolute or
* relative path names, or special prefixed values as
* specified in
* {@link SPDataSource#jarSpecToFile(String, ClassLoader, URI)}
* .
* @see {@link SPDataSource#jarSpecToFile(String, ClassLoader, URI)}
*/
public LoadJDBCDrivers(List<String> driverJarList) {
this.driverJarList = driverJarList;
logger.debug("in constructor, setting finished to false..."); //$NON-NLS-1$
finished = false;
}
public Integer getJobSize() {
return new Integer(driverJarList.size() * 1000);
}
public int getProgress() {
double fraction = 0.0;
if (cl != null) {
fraction = cl.getFraction();
}
int progress = (jarCount - 1) * 1000 + (int) (fraction * 1000.0);
if (logger.isDebugEnabled()) logger.debug("******************* progress is: " + progress + " of " + getJobSize()); //$NON-NLS-1$ //$NON-NLS-2$
return progress;
}
public boolean isFinished() {
return finished;
}
/**
* The driver scan cannot be cancelled. This method has no effect.
*/
public void setCancelled(boolean cancelled) {
// job not cancellable, do nothing
}
/**
* The driver scan cannot be cancelled. This method always returns
* false.
*/
public boolean isCancelled() {
return false;
}
public boolean hasStarted() {
return hasStarted;
}
public String getMessage () {
return null; // no messages returned from this job
}
public void execute() {
hasStarted = true;
try {
Iterator it = driverJarList.iterator();
while (it.hasNext()) {
// initialize counters
jarCount++;
logger.debug("**************** processing file #" + jarCount + " of " + driverJarList.size()); //$NON-NLS-1$ //$NON-NLS-2$
String path = (String) it.next();
URL jarLocation = JDBCDataSource.jarSpecToFile(path, getClass().getClassLoader(), serverBaseURI);
if (jarLocation != null) {
addJarLocation(jarLocation);
}
}
finished = true;
logger.debug("done loading (normal operation), setting finished to true."); //$NON-NLS-1$
} catch ( Exception exp ) {
logger.error("something went wrong in LoadJDBCDrivers worker thread!",exp); //$NON-NLS-1$
} finally {
finished = true;
hasStarted = false;
logger.debug("done loading (error condition), setting finished to true."); //$NON-NLS-1$
}
}
private void addJarLocation(URL url) {
DefaultMutableTreeNode root = (DefaultMutableTreeNode) dtm.getRoot();
DefaultMutableTreeNode node = new DefaultMutableTreeNode(url.toString());
dtm.insertNodeInto(node, root, root.getChildCount());
try {
cl = new JDBCScanClassLoader(url);
List driverClasses = cl.scanForDrivers();
logger.info("Found drivers: "+driverClasses); //$NON-NLS-1$
Iterator it = driverClasses.iterator();
while (it.hasNext()) {
DefaultMutableTreeNode child = new DefaultMutableTreeNode(it.next());
dtm.insertNodeInto(child, node, node.getChildCount());
}
} catch (IOException ex) {
logger.warn("I/O Error reading JAR file",ex); //$NON-NLS-1$
DefaultMutableTreeNode child = new DefaultMutableTreeNode(ex);
dtm.insertNodeInto(child, node, node.getChildCount());
}
TreePath path = new TreePath(node.getPath());
driverTree.expandPath(path);
driverTree.scrollPathToVisible(path);
}
}
/**
* Scans a jar file for instances of java.sql.Driver.
*/
private class JDBCScanClassLoader extends JarScanClassLoader {
/**
* Creates a class loader that can scan the given JAR for JDBC drivers.
* Uses this class's class loader as its parent.
*
* @param jarLocation
* The JAR to scan. This URL must <i>not</i> be a jar: URL;
* it will be converted to one within this constructor.
* @throws IOException
*/
public JDBCScanClassLoader(URL jarLocation) throws IOException {
super(jarLocation);
}
@Override
protected boolean checkClass(Class<?> clazz) {
return java.sql.Driver.class.isAssignableFrom(clazz);
}
}
public JPanel getPanel() {
return this;
}
public boolean hasUnsavedChanges() {
// TODO return whether this panel has been changed
return true;
}
public void addDriverTreeSelectionListener(TreeSelectionListener tsl) {
driverTree.addTreeSelectionListener(tsl);
}
public void removeDriverTreeSelectionListener(TreeSelectionListener tsl) {
driverTree.removeTreeSelectionListener(tsl);
}
}