/*
* 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.sql;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URI;
import java.net.URL;
import java.security.AllPermission;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.Policy;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoableEdit;
import org.apache.log4j.Logger;
/**
* Defines a type of data source. We wanted to call this SPDataSourceClass,
* but that would be confusing in that we mean class as in class, type, genre, sort,
* variety, breed (ahem, that's enough, Mr. Data).
* <p>
* Data Source types can have supertypes, from which they inherit any undefined property
* values.
*/
public class JDBCDataSourceType {
static final Logger logger = Logger.getLogger(JDBCDataSourceType.class);
/**
* Map of classpaths to classloaders. This facilitates class sharing between
* incarnations of the same database type (as long as it still has the same
* set of JAR files as its previous incarnations).
*/
private static final Map<List<String>, JDBCClassLoader> jdbcClassloaders = new HashMap<List<String>, JDBCClassLoader>();
/**
* For debugging only, we count how many times we've attempted to load
* each class by name. This tool will hopefully help us track down cases
* when we are loading the same drivers for multiple connections that should
* have been sharing the same driver classes.
*/
private static Map<String, Integer> classLoadCounts = new HashMap<String, Integer>();
private class UndoablePropertyEdit extends AbstractUndoableEdit {
private final String changedProperty;
private final String oldValue;
private final String newValue;
private final JDBCDataSourceType source;
public UndoablePropertyEdit(String propertyName, String oldValue, String newValue, JDBCDataSourceType source) {
changedProperty = propertyName;
this.oldValue = oldValue;
this.newValue = newValue;
this.source = source;
}
@Override
public void redo() throws CannotRedoException {
super.redo();
source.properties.put(changedProperty, newValue);
source.classLoader = getClassLoaderFromCache();
}
@Override
public void undo() throws CannotUndoException {
super.undo();
source.properties.put(changedProperty, oldValue);
source.classLoader = getClassLoaderFromCache();
}
}
/**
* A special ClassLoader that searches the classpath associated with this
* type of data source only. Each SPDataSourceType should have
* one of these class loaders, configured to search the database vendor's
* jar/zip files.
*/
static class JDBCClassLoader extends ClassLoader {
/**
* The base URI against which to resolve server: type JAR specs. This
* can legally be null, which will simply cause attempts at reading
* server: JAR files to fail. If there are no server: JAR specs, this
* null value won't cause any trouble.
*/
private final URI serverBaseUri;
/**
* The classpath of this loader at the time it was created.
*/
private final List<String> classpath;
/**
* Don't call this method directly. Use the
* {@link JDBCDataSourceType#getClassLoaderFromCache()} method instead.
*
* @param serverBaseUri Base URI to resolve classpath entries against.
* @param classpath The JAR specifications to consult.
*/
protected JDBCClassLoader(URI serverBaseUri, List<String> classpath) {
super(JDBCClassLoader.class.getClassLoader());
this.serverBaseUri = serverBaseUri;
this.classpath = classpath;
// sanity check
for (String jarPath : classpath) {
if (jarPath.startsWith(SPDataSource.SERVER) && serverBaseUri == null) {
throw new IllegalStateException(
"Found a server-based JAR file \"" + jarPath + "\" on the" +
" classpath but there is no server base URI configured");
}
}
logger.debug("Created new JDBC Classloader @"+System.identityHashCode(this));
// classes loaded with this classloader need their own security policy,
// because in WebStart, the allPermissions tag applies only to the
// webstart classloader.
// I found this code in a comment on the big ranch java saloon. It works!
Policy.setPolicy( new Policy() {
public PermissionCollection
getPermissions(CodeSource codesource) {
Permissions perms = new Permissions();
perms.add(new AllPermission());
return(perms);
}
public void refresh(){
// no need to refresh
}
});
}
/**
* Searches the jar files listed by getJdbcJarList() for the
* named class. Throws ClassNotFoundException if the class can't
* be located.
*/
@Override
public Class<?> findClass(String name)
throws ClassNotFoundException {
if (logger.isDebugEnabled()) {
Integer count = classLoadCounts.get(name);
if (count == null) {
count = new Integer(1);
} else {
count += 1;
}
classLoadCounts.put(name, count);
logger.debug("JDBC Classloader @"+System.identityHashCode(this)+
": Looking for class "+name+" (count = "+count+")");
}
for (String jarFileName : classpath) {
try {
logger.debug("checking file "+jarFileName);
URL jarLocation = JDBCDataSource.jarSpecToFile(jarFileName, getParent(), serverBaseUri);
if (jarLocation == null) {
// missing JAR file in classpath. just skip it.
continue;
}
String jarEntryPath = name.replace('.','/') + ".class";
URL url = new URL("jar:" + jarLocation.toString() + "!/" + jarEntryPath);
JarURLConnection jarConnection;
try {
jarConnection = (JarURLConnection) url.openConnection();
} catch (IOException ex) {
// this was the old behaviour if the file didn't exist. still a good idea?
logger.debug("Skipping non-existant JAR file "+jarFileName);
continue;
}
JarEntry ent = jarConnection.getJarEntry();
byte[] buf = new byte[(int) ent.getSize()];
InputStream is = jarConnection.getInputStream();
int offs = 0, n = 0;
while ( (n = is.read(buf, offs, buf.length-offs)) >= 0 && offs < buf.length) {
offs += n;
}
final int total = offs;
if (total != ent.getSize()) {
logger.warn("What gives? ZipEntry "+ent.getName()+" is "+ent.getSize()+" bytes long, but we only read "+total+" bytes!");
}
return defineClass(name, buf, 0, buf.length);
} catch (IOException ex) {
// there might be more classpath entries to search
continue;
}
}
String errorMsg =
"Could not locate class " + name +
" in any of the JDBC Driver JAR files: " + classpath;
logger.debug(errorMsg);
throw new ClassNotFoundException(errorMsg);
}
/**
* Returns the first result that would be obtained from {@link #findResources(String)},
* or null if findResources would return an empty result.
*/
@Override
protected URL findResource(String name) {
Enumeration<URL> resources = findResources(name);
if (resources.hasMoreElements()) {
return resources.nextElement();
} else {
return null;
}
}
/**
* Creates URL objects for each instance of the named file within the
* same set of JAR files that classes are loaded from.
*/
@Override
protected Enumeration<URL> findResources(String name) {
logger.debug("Looking for all resources with path "+name);
List<URL> results = new ArrayList<URL>();
for (String jarName : classpath) {
logger.debug("Converting JAR name: " + jarName);
URL jarLocation = JDBCDataSource.jarSpecToFile(jarName, getParent(), serverBaseUri);
logger.debug(" JAR is "+jarLocation);
try {
if (jarLocation == null) {
logger.debug(" Skipping non-existant JAR file " + jarName);
continue;
} else {
logger.debug(" Searching JAR " + jarLocation);
}
URL url = new URL("jar:" + jarLocation.toString() + "!/" + name);
JarURLConnection jarConnection;
try {
jarConnection = (JarURLConnection) url.openConnection();
} catch (IOException ex) {
// this was the old behaviour if the file didn't exist. still a good idea?
logger.debug("Skipping non-existant JAR file " + jarLocation);
continue;
}
JarEntry ent = jarConnection.getJarEntry();
if (ent != null) {
results.add(url);
logger.debug(" Found entry " + name);
}
} catch (IOException ex) {
// missing resource is not a fatal error
logger.debug(" IO Exception while searching "+ jarLocation
+ " for resource " + name + ". Continuing...", ex);
}
}
return Collections.enumeration(results);
}
}
public static final String JDBC_DRIVER = "JDBC Driver Class";
public static final String JDBC_URL = "JDBC URL";
public static final String JDBC_JAR_BASE = "JDBC JAR";
public static final String JDBC_JAR_COUNT = "JDBC JAR Count";
public static final String TYPE_NAME = "Name";
public static final String PARENT_TYPE_NAME = "Parent Type";
public static final String PL_DB_TYPE = "PL Type";
public static final String COMMENT = "Comment";
public static final String DDL_GENERATOR = "DDL Generator";
public static final String KETTLE_DB_TYPES = "ca.sqlpower.architect.etl.kettle.connectionType";
public static final String SUPPORTS_UPDATEABLE_RESULT_SETS = "Supports Updatable Result Sets";
public static final String SUPPORTS_STREAM_QUERIES = "Supports Stream Queries";
/**
* This property is used for Postgres database to quotes the physical name (name of table/column/sequences etc.)
* during 'Forward engineering' in a 'SQL Power Architect'.
*/
public static final String SUPPORTS_QUOTING_NAME = "Supports Quoting Name";
/**
* This type's parent type. This value will be null if this type has no
* parent.
*/
private JDBCDataSourceType parentType;
/**
* This type's properties. There are a set of property
* names that we know what they mean, but instances of this class carry
* all the property name=value pairs that get thrown at them so that
* we don't end up deleting new properties when reading then saving over
* a file with an older version of the app.
*/
private Map<String,String> properties = new LinkedHashMap<String, String>();
/**
* The Class Loader that is responsible for finding and defining the JDBC
* driver classes from the database vendor, for this connection type only.
*/
private JDBCClassLoader classLoader;
/**
* Deletgate class for supporting the bound properties of this class.
*/
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
/**
* Listeners listening for undoable edits.
*/
private final List<UndoableEditListener> undoableEditListeners = new ArrayList<UndoableEditListener>();
private final URI serverBaseUri;
/**
* Creates a new default data source type that can load drivers from a local
* file or a JAR resource on the classpath, but not from a server.
*
* @see #SPDataSourceType(URI)
*/
public JDBCDataSourceType() {
this(null);
}
/**
* Creates a new default data source type that can load drivers from a
* server, a local file, or JAR resources on the classpath.
*/
public JDBCDataSourceType(URI serverBaseUri) {
super();
this.serverBaseUri = serverBaseUri;
classLoader = getClassLoaderFromCache();
}
/**
* Returns the cached classloader that has the same set of jar files in its
* classpath as this data source type currently does. If no such classloader
* is already cached, a new one will be created and stored in the cache.
*/
private JDBCClassLoader getClassLoaderFromCache() {
List<String> classpath = Collections.unmodifiableList(new ArrayList<String>(getJdbcJarList()));
JDBCClassLoader classLoader = jdbcClassloaders.get(classpath);
if (classLoader == null) {
classLoader = new JDBCClassLoader(getServerBaseUri(), classpath);
jdbcClassloaders.put(classpath, classLoader);
}
return classLoader;
}
public String getComment() {
return getProperty(COMMENT);
}
public void setComment(String comment) {
putPropertyImpl("comment", COMMENT, comment);
}
public String getPlDbType() {
return getProperty(PL_DB_TYPE);
}
public void setPlDbType(String type) {
putPropertyImpl("plDbType", PL_DB_TYPE, type);
}
public String getJdbcDriver() {
return getProperty(JDBC_DRIVER);
}
public void setJdbcDriver(String jdbcDriver) {
putPropertyImpl("jdbcDriver", JDBC_DRIVER, jdbcDriver);
}
/**
* Returns an unmodifiable list of all the JAR file paths
* associated with this data source type. This list is not
* guaranteed to stay up-to-date with additional jars that get
* added to this ds type after you call this method.
* <p>
* @return An unmodifiable list of jar file pathnames.
*/
public List<String> getJdbcJarList() {
return Collections.unmodifiableList(makeModifiableJdbcJarList());
}
/**
* Creates a list of the Jar files tracked by this data source type. Although
* the list is modifiable, it is your own independant copy of the jar list, and
* modifying it will have no effect on the actual list of jars tracked by this
* instance.
*/
private List<String> makeModifiableJdbcJarList() {
int count = getJdbcJarCount();
List<String> list = new ArrayList<String>();
for (int i = 0; i < count; i++) {
String key = JDBC_JAR_BASE+"_"+i;
String value = getProperty(key);
if (value != null) {
logger.debug("Found jar \""+value+"\" under key \""+key+"\"");
list.add(value);
} else {
logger.debug("Skipping null jar entry under key \""+key+"\"");
}
}
return list;
}
/**
* Replaces the current list of JDBC driver jar files with a copy of the given list.
* <p>
* Warning: this method does not presently cause any events to be fired, although
* a future revision hopefully will.
*/
public void setJdbcJarList(List<String> jdbcJarList) {
clearJdbcJarList();
int count = jdbcJarList.size();
setJdbcJarCount(count);
int i = 0;
for (String jar : jdbcJarList) {
logger.debug("Setting jar list value " + i + " to path " + jar);
properties.put(JDBC_JAR_BASE+"_"+i, jar);
i++;
}
classLoader = getClassLoaderFromCache();
}
private void clearJdbcJarList() {
Set<String> keys = properties.keySet();
Iterator<String> it = keys.iterator();
while (it.hasNext()) {
String key = it.next();
if (key.startsWith(JDBC_JAR_BASE)) {
it.remove();
}
}
}
/**
* Adds the JDBC driver jar path name to the internal list.
* <p>
* Warning: this method does not presently cause any events to be fired, although
* a future revision hopefully will.
*/
public void addJdbcJar(String jarPath) {
logger.debug("Adding jar at path " + jarPath);
int count = getJdbcJarCount();
properties.put(JDBC_JAR_BASE+"_"+count, jarPath);
setJdbcJarCount(count + 1);
classLoader = getClassLoaderFromCache();
}
private void setJdbcJarCount(int count) {
putPropertyImpl("jdbcJarCount", JDBC_JAR_COUNT, String.valueOf(count));
classLoader = getClassLoaderFromCache();
}
private int getJdbcJarCount() {
String jarCountString = getProperty(JDBC_JAR_COUNT);
if (jarCountString == null) {
return 0;
} else {
return Integer.parseInt(jarCountString);
}
}
public String getJdbcUrl() {
return getProperty(JDBC_URL);
}
public void setJdbcUrl(String jdbcUrl) {
putPropertyImpl("jdbcUrl", JDBC_URL, jdbcUrl);
}
/**
* For each property in the template, if the property has a default value
* its property name and the default value will be put into the map otherwise
* the property name and an empty string will be stored.
*/
public Map<String, String> retrieveURLDefaults() {
String template = getProperty(JDBC_URL);
Map<String, String> map = new LinkedHashMap<String, String>();
if (template == null) return map;
int searchFrom = 0;
while (template.indexOf('<', searchFrom) >= 0) {
int openBrace = template.indexOf('<', searchFrom);
searchFrom = openBrace + 1;
int colon = template.indexOf(':', searchFrom);
int closeBrace = template.indexOf('>', searchFrom);
if (closeBrace == -1) break;
if (colon >= 0 && colon < closeBrace) {
map.put(template.substring(openBrace+1, colon), template.substring(colon+1, closeBrace));
} else if (closeBrace >=0) {
map.put(template.substring(openBrace+1, closeBrace), "");
}
searchFrom = closeBrace++;
}
return map;
}
/**
* This method takes a url and matches it to the template pattern that is stored.
* The returned map contains a key, value pair for each property in the template
* and the value itself.
*/
public Map<String, String> retrieveURLParsing(String url) {
String template = getProperty(JDBC_URL);
Map<String, String> map = new LinkedHashMap<String, String>();
if (template == null) return map;
String reTemplate = template.replaceAll("<.*?>", "(.*)");
logger.debug("Regex of template is "+reTemplate);
Pattern p = Pattern.compile(reTemplate);
Matcher m = p.matcher(url);
if (m.find()) {
int searchFrom = 0;
for (int g = 1; g <= m.groupCount(); g++) {
int openBrace = template.indexOf('<', searchFrom);
searchFrom = openBrace + 1;
int colon = template.indexOf(':', searchFrom);
int closeBrace = template.indexOf('>', searchFrom);
if (colon >= 0 && colon < closeBrace) {
map.put(template.substring(openBrace+1, colon), m.group(g));
} else if (closeBrace >=0) {
map.put(template.substring(openBrace+1, closeBrace), m.group(g));
}
searchFrom = closeBrace++;
}
}
logger.debug("The map! dun dun dun: " + map.toString());
return map;
}
public String getName() {
return getProperty(TYPE_NAME);
}
public void setName(String name) {
putPropertyImpl("name", TYPE_NAME, name);
}
public String getDDLGeneratorClass() {
return getProperty(DDL_GENERATOR);
}
public void setDDLGeneratorClass(String className) {
putPropertyImpl("DDLGeneratorClass", DDL_GENERATOR, className);
}
public JDBCDataSourceType getParentType() {
return parentType;
}
public void setParentType(JDBCDataSourceType parentType) {
this.parentType = parentType;
}
/**
* Returns true if and only if the platform supports updateable result sets.
*/
public boolean getSupportsUpdateableResultSets() {
String ret = getProperty(SUPPORTS_UPDATEABLE_RESULT_SETS);
if (ret == null || !Boolean.parseBoolean(ret)) {
return false;
}
return true;
}
/**
* Returns true if and only if the platform supports quoting
* physical name of table/column name/sequence name etc.
* @return
*/
public boolean getSupportsQuotingName() {
String ret = getProperty(SUPPORTS_QUOTING_NAME);
if (ret == null || !Boolean.parseBoolean(ret)) {
return false;
}
return true;
}
/**
* Returns true if and only if the platform supports the SELECT STREAM syntax.
*/
public boolean getSupportsStreamQueries() {
String ret = getProperty(SUPPORTS_STREAM_QUERIES);
if (ret == null || !Boolean.parseBoolean(ret)) {
return false;
}
return true;
}
/**
* Returns all the properties of this data source type. This will not
* include any inherited values, so unless you're trying to save this data source
* to a file or something, you'd probably prefer to use one of the getter methods.
* Also, you really, really shouldn't modify the map you get back.
*
* @throws UnsupportedOperationException if you ignored our nice warning and tried
* to modify the returned map.
*/
Map<String, String> getProperties() {
return Collections.unmodifiableMap(properties);
}
/**
* Returns a set of the properties defined for this data source type.
* The set returned is unmodifiable and will throw an exception if any modification
* is made to it.
*/
public Set<String> getPropertyNames() {
return Collections.unmodifiableSet(properties.keySet());
}
/**
* Adds or replaces a value in the property map. Fires an undoable edit
* event, but not a property change event. Use
* {@link #putPropertyImpl(String, String, String)} if the change should
* produce a property change event.
*/
public void putProperty(String key, String value) {
putPropertyImpl(null, key, value);
}
public ClassLoader getJdbcClassLoader() {
return classLoader;
}
/**
* Returns a list of the kettle database type names, empty
* list if the database types property is null.
*/
public List<String> getKettleNames() {
List<String> ret = new LinkedList<String>();
String dbTypes = getProperty(KETTLE_DB_TYPES);
if (dbTypes == null) return Collections.emptyList();
Scanner s = new Scanner(dbTypes);
s.useDelimiter(":");
while (s.hasNext()) {
ret.add(s.next());
}
return ret;
}
/**
* Gets a property from this data source type, checking with the supertype
* when this type doesn't have a value for the requested property.
*
* @param key The property name to look up. Null isn't allowed.
*/
public String getProperty(String key) {
String value = properties.get(key);
if (value != null) {
return value;
} else if (parentType != null) {
return parentType.getProperty(key);
} else {
return null;
}
}
/**
* Removes the jar file's path from the list of jar files. Has no effect
* if the named JAR file is not in the list.
*
* @param path the path you want to remove
*/
public void removeJdbcJar(String path) {
List<String> jdbcJars = makeModifiableJdbcJarList();
logger.debug("Removing jdbc jar " + path + " at index " + jdbcJars.indexOf(path));
if (jdbcJars.contains(path)) {
jdbcJars.remove(path);
} else {
File file = new File(path);
jdbcJars.remove(SPDataSource.BUILTIN + file.getName());
}
setJdbcJarList(jdbcJars);
}
@Override
public String toString() {
return "DataSourceType: "+properties;
}
/**
* Checks that all prerequisites for making a connection to this type of database
* have been met. Currently, this only checks that the driver class field is filled
* in.
*
* @throws SQLException if there are unmet prerequisites. The exception message will
* explain which prerequisite is not met.
*/
public void checkConnectPrereqs() throws SQLException {
if (getJdbcDriver() == null
|| getJdbcDriver().trim().length() == 0) {
throw new SQLException("Data Source Type \""+getName()+"\" has no JDBC Driver class specified.");
}
}
/**
* Modifies the value of the named property, firing a change event if the
* new value differs from the pre-existing one.
*
* @param javaPropName
* The name of the JavaBeans property you are modifying. If the
* change does not correspond with a bean property, or you want
* to suppress the property change event, this parameter should
* be null.
* @param plPropName
* The name of PL.INI the property to set/update (this is also
* the key in the in-memory properties map)
* @param propValue
* The new value for the property
*/
private void putPropertyImpl(String javaPropName, String plPropName, String propValue) {
String oldValue = properties.get(plPropName);
properties.put(plPropName, propValue);
classLoader = getClassLoaderFromCache(); // in case this changes the classpath
if (javaPropName != null) {
firePropertyChange(javaPropName, oldValue, propValue);
}
if ((oldValue == null && propValue != null) || (oldValue != null && !oldValue.equals(propValue))) {
UndoableEdit edit = new UndoablePropertyEdit(plPropName, oldValue, propValue, this);
for (int i = undoableEditListeners.size() -1; i >= 0; i--) {
undoableEditListeners.get(i).undoableEditHappened(new UndoableEditEvent(this, edit));
}
}
}
public void addUndoableEditListener(UndoableEditListener l) {
undoableEditListeners.add(l);
}
public void removeUndoableEditListener(UndoableEditListener l) {
undoableEditListeners.remove(l);
}
// ---------------- Methods that delegate to the PropertyChangeSupport -----------------
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
pcs.addPropertyChangeListener(propertyName, listener);
}
public void firePropertyChange(PropertyChangeEvent evt) {
pcs.firePropertyChange(evt);
}
public void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
pcs.firePropertyChange(propertyName, oldValue, newValue);
}
public PropertyChangeListener[] getPropertyChangeListeners() {
return pcs.getPropertyChangeListeners();
}
public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
return pcs.getPropertyChangeListeners(propertyName);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
pcs.removePropertyChangeListener(propertyName, listener);
}
public URI getServerBaseUri() {
return serverBaseUri;
}
}