/*
* Password Management Servlets (PWM)
* http://www.pwm-project.org
*
* Copyright (c) 2006-2009 Novell, Inc.
* Copyright (c) 2009-2017 The PWM Project
*
* This program 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 2 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package password.pwm.util.db;
import org.xeustechnologies.jcl.JarClassLoader;
import org.xeustechnologies.jcl.JclObjectFactory;
import password.pwm.PwmApplication;
import password.pwm.PwmConstants;
import password.pwm.error.ErrorInformation;
import password.pwm.error.PwmError;
import password.pwm.error.PwmUnrecoverableException;
import password.pwm.util.java.JsonUtil;
import password.pwm.util.logging.PwmLogger;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.Driver;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class JDBCDriverLoader {
private static final PwmLogger LOGGER = PwmLogger.forClass(JDBCDriverLoader.class, true);
static DriverWrapper loadDriver(
final PwmApplication pwmApplication,
final DBConfiguration dbConfiguration
)
throws DatabaseException
{
final List<ClassLoaderStrategy> strategies = dbConfiguration.getClassLoaderStrategies();
LOGGER.trace("attempting to load jdbc driver using strategies: " + JsonUtil.serializeCollection(strategies));
final List<String> errorMsgs = new ArrayList<>();
for (final ClassLoaderStrategy strategy : strategies) {
final DriverLoader loader = strategy.getJdbcDriverDriverLoader();
try {
final Driver driver = loader.loadDriver(pwmApplication, dbConfiguration);
if (driver != null) {
return new DriverWrapper(driver, loader);
}
} catch (DatabaseException e) {
errorMsgs.add(strategy + " error: " + e.getMessage());
}
}
final String errorMsg = " unable to load database driver: " + JsonUtil.serializeCollection(errorMsgs);
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_DB_UNAVAILABLE,errorMsg);
LOGGER.error(errorMsg);
throw new DatabaseException(errorInformation);
}
public enum ClassLoaderStrategy {
XeusLoader(new XeusJarClassDriverLoader()),
AppPathFileLoader(new AppPathDriverLoader()),
TempFile(new TempFileDriverLoader()),
Classpath(new JavaClasspathLoader()),
;
private final DriverLoader jdbcDriverDriverLoader;
ClassLoaderStrategy(final DriverLoader jdbcDriverDriverLoader) {
this.jdbcDriverDriverLoader = jdbcDriverDriverLoader;
}
private DriverLoader getJdbcDriverDriverLoader() {
return jdbcDriverDriverLoader;
}
}
interface DriverLoader {
Driver loadDriver(PwmApplication pwmApplication, DBConfiguration dbConfiguration) throws DatabaseException;
void unloadDriver();
}
private static class JavaClasspathLoader implements DriverLoader {
private static final PwmLogger LOGGER = PwmLogger.forClass(XeusJarClassDriverLoader.class, true);
@Override
public Driver loadDriver(final PwmApplication pwmApplication, final DBConfiguration dbConfiguration)
throws DatabaseException
{
final String jdbcClassName = dbConfiguration.getDriverClassname();
try {
LOGGER.debug("loading JDBC database driver from classpath: " + jdbcClassName);
final Driver driver = (Driver) Class.forName(jdbcClassName).newInstance();
LOGGER.debug("successfully loaded JDBC database driver from classpath: " + jdbcClassName);
return driver;
} catch (Throwable e) {
final String errorMsg = e.getClass().getName() + " error loading JDBC database driver from classpath: " + e.getMessage();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_DB_UNAVAILABLE,errorMsg);
throw new DatabaseException(errorInformation);
}
}
@Override
public void unloadDriver() {
}
}
private static class XeusJarClassDriverLoader implements DriverLoader {
private static final PwmLogger LOGGER = PwmLogger.forClass(XeusJarClassDriverLoader.class, true);
@Override
public Driver loadDriver(final PwmApplication pwmApplication, final DBConfiguration dbConfiguration)
throws DatabaseException
{
final String jdbcClassName = dbConfiguration.getDriverClassname();
final byte[] jdbcDriverBytes = dbConfiguration.getJdbcDriver();
try {
LOGGER.debug("loading JDBC database driver stored in configuration");
final JarClassLoader jarClassLoader = new JarClassLoader();
jarClassLoader.add(new ByteArrayInputStream(jdbcDriverBytes));
final JclObjectFactory jclObjectFactory = JclObjectFactory.getInstance(true);
//Create object of loaded class
final Driver driver = (Driver)jclObjectFactory.create(jarClassLoader, jdbcClassName);
LOGGER.debug("successfully loaded JDBC database driver '" + jdbcClassName + "' from application configuration");
return driver;
} catch (Throwable e) {
final String errorMsg = "error registering JDBC database driver stored in configuration: " + e.getMessage();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_DB_UNAVAILABLE,errorMsg);
throw new DatabaseException(errorInformation);
}
}
@Override
public void unloadDriver() {
}
}
private static class TempFileDriverLoader implements DriverLoader {
private static final PwmLogger LOGGER = PwmLogger.forClass(TempFileDriverLoader.class, true);
private File tempFile;
@Override
public Driver loadDriver(final PwmApplication pwmApplication, final DBConfiguration dbConfiguration)
throws DatabaseException
{
final String jdbcClassName = dbConfiguration.getDriverClassname();
final byte[] jdbcDriverBytes = dbConfiguration.getJdbcDriver();
if (jdbcDriverBytes == null || jdbcDriverBytes.length < 1) {
final String errorMsg = "jdbc driver file not configured, skipping";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_DB_UNAVAILABLE,errorMsg);
throw new DatabaseException(errorInformation);
}
try {
LOGGER.debug("loading JDBC database driver stored in configuration");
if (tempFile == null) {
final String prefixName = PwmConstants.PWM_APP_NAME.toLowerCase() + "_jdbcJar_";
tempFile = File.createTempFile(prefixName, "jar");
LOGGER.trace("created temp file " + tempFile.getAbsolutePath());
}
final FileOutputStream fos = new FileOutputStream(tempFile);
fos.write(jdbcDriverBytes);
fos.close();
final URLClassLoader urlClassLoader = new URLClassLoader(
new URL[] {tempFile.toURI().toURL()},
this.getClass().getClassLoader()
);
//Create object of loaded class
final Class jdbcDriverClass = urlClassLoader.loadClass(jdbcClassName);
final Driver driver = (Driver)jdbcDriverClass.newInstance();
LOGGER.debug("successfully loaded JDBC database driver '" + jdbcClassName + "' from application configuration");
return driver;
} catch (Throwable e) {
final String errorMsg = "error registering JDBC database driver stored in configuration: " + e.getMessage();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_DB_UNAVAILABLE,errorMsg);
throw new DatabaseException(errorInformation);
}
}
@Override
public void unloadDriver() {
if (tempFile != null) {
if (tempFile.delete()) {
LOGGER.trace("removed temporary file " + tempFile.getAbsolutePath());
}
}
tempFile = null;
}
}
private static class AppPathDriverLoader implements DriverLoader {
private final PwmLogger LOGGER = PwmLogger.forClass(AppPathDriverLoader.class, true);
// static ccache of classloader to prevent classloader memory leak
private static Map<String,ClassLoader> driverCache = new ConcurrentHashMap<>();
@Override
public Driver loadDriver(final PwmApplication pwmApplication, final DBConfiguration dbConfiguration)
throws DatabaseException
{
final String jdbcClassName = dbConfiguration.getDriverClassname();
final byte[] jdbcDriverBytes = dbConfiguration.getJdbcDriver();
if (jdbcDriverBytes == null || jdbcDriverBytes.length < 1) {
final String errorMsg = "jdbc driver file not configured, skipping";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_DB_UNAVAILABLE,errorMsg);
throw new DatabaseException(errorInformation);
}
final String jdbcDriverHash;
try {
jdbcDriverHash = pwmApplication.getSecureService().hash(jdbcDriverBytes);
} catch (PwmUnrecoverableException e) {
throw new DatabaseException(e.getErrorInformation());
}
final ClassLoader urlClassLoader;
if (driverCache.containsKey(jdbcDriverHash)) {
urlClassLoader = driverCache.get(jdbcDriverHash);
LOGGER.trace("loaded classloader from static cache");
} else {
try {
LOGGER.debug("loading JDBC database driver stored in configuration");
final File tempFile = createOrGetTempJarFile(pwmApplication, jdbcDriverBytes);
urlClassLoader = new URLClassLoader(
new URL[]{tempFile.toURI().toURL()},
this.getClass().getClassLoader()
);
driverCache.put(jdbcDriverHash, urlClassLoader);
} catch (Throwable e) {
final String errorMsg = "error establishing classloader for driver: " + e.getMessage();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_DB_UNAVAILABLE, errorMsg);
throw new DatabaseException(errorInformation);
}
}
try {
//Create object of loaded class
final Class jdbcDriverClass = urlClassLoader.loadClass(jdbcClassName);
final Driver driver = (Driver)jdbcDriverClass.newInstance();
LOGGER.debug("successfully loaded JDBC database driver '" + jdbcClassName + "' from application configuration");
return driver;
} catch (Throwable e) {
final String errorMsg = "error registering JDBC database driver stored in configuration: " + e.getMessage();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_DB_UNAVAILABLE,errorMsg);
throw new DatabaseException(errorInformation);
}
}
@Override
public void unloadDriver() {
}
File createOrGetTempJarFile(final PwmApplication pwmApplication, final byte[] jarBytes) throws PwmUnrecoverableException, IOException {
final File file = pwmApplication.getTempDirectory();
final String jarHash = pwmApplication.getSecureService().hash(jarBytes);
final String tempFileName = "jar-" + jarHash + ".jar";
final File tempFile = new File(file.getAbsolutePath() + File.separator + tempFileName);
if (tempFile.exists()) {
final String fileHash = pwmApplication.getSecureService().hash(tempFile);
if (!jarHash.equals(fileHash)) {
LOGGER.debug("existing temp jar file " + tempFile.getAbsolutePath() + " has wrong contents, will delete");
if (!tempFile.delete()) {
throw new IOException("unable to delete temp file " + jarHash);
}
}
}
if (!tempFile.exists()) {
LOGGER.debug("creating temp jar file " + tempFile.getAbsolutePath());
final OutputStream fos = new BufferedOutputStream(new FileOutputStream(tempFile));
fos.write(jarBytes);
fos.close();
} else {
LOGGER.trace("reusing existing temp jar file " + tempFile.getAbsolutePath());
}
return tempFile;
}
}
static class DriverWrapper {
private final Driver driver;
private final DriverLoader driverLoader;
DriverWrapper(final Driver driver, final DriverLoader driverLoader) {
this.driver = driver;
this.driverLoader = driverLoader;
}
public Driver getDriver() {
return driver;
}
public DriverLoader getDriverLoader() {
return driverLoader;
}
}
}