Coverage Report - org.owasp.dependencycheck.data.nvdcve.ConnectionFactory
 
Classes in this File Line Coverage Branch Coverage Complexity
ConnectionFactory
38%
70/181
28%
13/46
8.5
 
 1  
 /*
 2  
  * This file is part of dependency-check-core.
 3  
  *
 4  
  * Licensed under the Apache License, Version 2.0 (the "License");
 5  
  * you may not use this file except in compliance with the License.
 6  
  * You may obtain a copy of the License at
 7  
  *
 8  
  *     http://www.apache.org/licenses/LICENSE-2.0
 9  
  *
 10  
  * Unless required by applicable law or agreed to in writing, software
 11  
  * distributed under the License is distributed on an "AS IS" BASIS,
 12  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  
  * See the License for the specific language governing permissions and
 14  
  * limitations under the License.
 15  
  *
 16  
  * Copyright (c) 2014 Jeremy Long. All Rights Reserved.
 17  
  */
 18  
 package org.owasp.dependencycheck.data.nvdcve;
 19  
 
 20  
 import java.io.File;
 21  
 import java.io.IOException;
 22  
 import java.io.InputStream;
 23  
 import java.sql.PreparedStatement;
 24  
 import java.sql.Connection;
 25  
 import java.sql.Driver;
 26  
 import java.sql.DriverManager;
 27  
 import java.sql.ResultSet;
 28  
 import java.sql.SQLException;
 29  
 import java.sql.Statement;
 30  
 import org.apache.commons.io.IOUtils;
 31  
 import org.owasp.dependencycheck.utils.DBUtils;
 32  
 import org.owasp.dependencycheck.utils.DependencyVersion;
 33  
 import org.owasp.dependencycheck.utils.DependencyVersionUtil;
 34  
 import org.owasp.dependencycheck.utils.Settings;
 35  
 import org.slf4j.Logger;
 36  
 import org.slf4j.LoggerFactory;
 37  
 
 38  
 /**
 39  
  * Loads the configured database driver and returns the database connection. If
 40  
  * the embedded H2 database is used obtaining a connection will ensure the
 41  
  * database file exists and that the appropriate table structure has been
 42  
  * created.
 43  
  *
 44  
  * @author Jeremy Long
 45  
  */
 46  
 public final class ConnectionFactory {
 47  
 
 48  
     /**
 49  
      * The Logger.
 50  
      */
 51  1
     private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionFactory.class);
 52  
     /**
 53  
      * The version of the current DB Schema.
 54  
      */
 55  1
     public static final String DB_SCHEMA_VERSION = Settings.getString(Settings.KEYS.DB_VERSION);
 56  
     /**
 57  
      * Resource location for SQL file used to create the database schema.
 58  
      */
 59  
     public static final String DB_STRUCTURE_RESOURCE = "data/initialize.sql";
 60  
     /**
 61  
      * Resource location for SQL file used to create the database schema.
 62  
      */
 63  
     public static final String DB_STRUCTURE_UPDATE_RESOURCE = "data/upgrade_%s.sql";
 64  
     /**
 65  
      * The URL that discusses upgrading non-H2 databases.
 66  
      */
 67  
     public static final String UPGRADE_HELP_URL = "http://jeremylong.github.io/DependencyCheck/data/upgrade.html";
 68  
     /**
 69  
      * The database driver used to connect to the database.
 70  
      */
 71  1
     private static Driver driver = null;
 72  
     /**
 73  
      * The database connection string.
 74  
      */
 75  1
     private static String connectionString = null;
 76  
     /**
 77  
      * The username to connect to the database.
 78  
      */
 79  1
     private static String userName = null;
 80  
     /**
 81  
      * The password for the database.
 82  
      */
 83  1
     private static String password = null;
 84  
 
 85  
     /**
 86  
      * Private constructor for this factory class; no instance is ever needed.
 87  
      */
 88  0
     private ConnectionFactory() {
 89  0
     }
 90  
 
 91  
     /**
 92  
      * Initializes the connection factory. Ensuring that the appropriate drivers
 93  
      * are loaded and that a connection can be made successfully.
 94  
      *
 95  
      * @throws DatabaseException thrown if we are unable to connect to the
 96  
      * database
 97  
      */
 98  
     public static void initialize() throws DatabaseException {
 99  
         //this only needs to be called once.
 100  21
         if (connectionString != null) {
 101  19
             return;
 102  
         }
 103  2
         Connection conn = null;
 104  
         try {
 105  
             //load the driver if necessary
 106  2
             final String driverName = Settings.getString(Settings.KEYS.DB_DRIVER_NAME, "");
 107  2
             if (!driverName.isEmpty()) { //likely need to load the correct driver
 108  2
                 LOGGER.debug("Loading driver: {}", driverName);
 109  2
                 final String driverPath = Settings.getString(Settings.KEYS.DB_DRIVER_PATH, "");
 110  
                 try {
 111  2
                     if (!driverPath.isEmpty()) {
 112  0
                         LOGGER.debug("Loading driver from: {}", driverPath);
 113  0
                         driver = DriverLoader.load(driverName, driverPath);
 114  
                     } else {
 115  2
                         driver = DriverLoader.load(driverName);
 116  
                     }
 117  0
                 } catch (DriverLoadException ex) {
 118  0
                     LOGGER.debug("Unable to load database driver", ex);
 119  0
                     throw new DatabaseException("Unable to load database driver");
 120  2
                 }
 121  
             }
 122  2
             userName = Settings.getString(Settings.KEYS.DB_USER, "dcuser");
 123  
             //yes, yes - hard-coded password - only if there isn't one in the properties file.
 124  2
             password = Settings.getString(Settings.KEYS.DB_PASSWORD, "DC-Pass1337!");
 125  
             try {
 126  2
                 connectionString = Settings.getConnectionString(
 127  
                         Settings.KEYS.DB_CONNECTION_STRING,
 128  
                         Settings.KEYS.DB_FILE_NAME);
 129  0
             } catch (IOException ex) {
 130  0
                 LOGGER.debug(
 131  
                         "Unable to retrieve the database connection string", ex);
 132  0
                 throw new DatabaseException("Unable to retrieve the database connection string");
 133  2
             }
 134  2
             boolean shouldCreateSchema = false;
 135  
             try {
 136  2
                 if (connectionString.startsWith("jdbc:h2:file:")) { //H2
 137  2
                     shouldCreateSchema = !h2DataFileExists();
 138  2
                     LOGGER.debug("Need to create DB Structure: {}", shouldCreateSchema);
 139  
                 }
 140  0
             } catch (IOException ioex) {
 141  0
                 LOGGER.debug("Unable to verify database exists", ioex);
 142  0
                 throw new DatabaseException("Unable to verify database exists");
 143  2
             }
 144  2
             LOGGER.debug("Loading database connection");
 145  2
             LOGGER.debug("Connection String: {}", connectionString);
 146  2
             LOGGER.debug("Database User: {}", userName);
 147  
 
 148  
             try {
 149  2
                 conn = DriverManager.getConnection(connectionString, userName, password);
 150  0
             } catch (SQLException ex) {
 151  0
                 if (ex.getMessage().contains("java.net.UnknownHostException") && connectionString.contains("AUTO_SERVER=TRUE;")) {
 152  0
                     connectionString = connectionString.replace("AUTO_SERVER=TRUE;", "");
 153  
                     try {
 154  0
                         conn = DriverManager.getConnection(connectionString, userName, password);
 155  0
                         Settings.setString(Settings.KEYS.DB_CONNECTION_STRING, connectionString);
 156  0
                         LOGGER.debug(
 157  
                                 "Unable to start the database in server mode; reverting to single user mode");
 158  0
                     } catch (SQLException sqlex) {
 159  0
                         LOGGER.debug("Unable to connect to the database", ex);
 160  0
                         throw new DatabaseException("Unable to connect to the database");
 161  0
                     }
 162  
                 } else {
 163  0
                     LOGGER.debug("Unable to connect to the database", ex);
 164  0
                     throw new DatabaseException("Unable to connect to the database");
 165  
                 }
 166  2
             }
 167  
 
 168  2
             if (shouldCreateSchema) {
 169  
                 try {
 170  0
                     createTables(conn);
 171  0
                 } catch (DatabaseException dex) {
 172  0
                     LOGGER.debug("", dex);
 173  0
                     throw new DatabaseException("Unable to create the database structure");
 174  0
                 }
 175  
             }
 176  
             try {
 177  2
                 ensureSchemaVersion(conn);
 178  0
             } catch (DatabaseException dex) {
 179  0
                 LOGGER.debug("", dex);
 180  0
                 throw new DatabaseException("Database schema does not match this version of dependency-check", dex);
 181  2
             }
 182  
         } finally {
 183  2
             if (conn != null) {
 184  
                 try {
 185  2
                     conn.close();
 186  0
                 } catch (SQLException ex) {
 187  0
                     LOGGER.debug("An error occurred closing the connection", ex);
 188  2
                 }
 189  
             }
 190  
         }
 191  2
     }
 192  
 
 193  
     /**
 194  
      * Cleans up resources and unloads any registered database drivers. This
 195  
      * needs to be called to ensure the driver is unregistered prior to the
 196  
      * finalize method being called as during shutdown the class loader used to
 197  
      * load the driver may be unloaded prior to the driver being de-registered.
 198  
      */
 199  
     public static void cleanup() {
 200  1
         if (driver != null) {
 201  
             try {
 202  1
                 DriverManager.deregisterDriver(driver);
 203  0
             } catch (SQLException ex) {
 204  0
                 LOGGER.debug("An error occurred unloading the database driver", ex);
 205  0
             } catch (Throwable unexpected) {
 206  0
                 LOGGER.debug(
 207  
                         "An unexpected throwable occurred unloading the database driver", unexpected);
 208  1
             }
 209  1
             driver = null;
 210  
         }
 211  1
         connectionString = null;
 212  1
         userName = null;
 213  1
         password = null;
 214  1
     }
 215  
 
 216  
     /**
 217  
      * Constructs a new database connection object per the database
 218  
      * configuration.
 219  
      *
 220  
      * @return a database connection object
 221  
      * @throws DatabaseException thrown if there is an exception loading the
 222  
      * database connection
 223  
      */
 224  
     public static Connection getConnection() throws DatabaseException {
 225  14
         initialize();
 226  14
         Connection conn = null;
 227  
         try {
 228  14
             conn = DriverManager.getConnection(connectionString, userName, password);
 229  0
         } catch (SQLException ex) {
 230  0
             LOGGER.debug("", ex);
 231  0
             throw new DatabaseException("Unable to connect to the database");
 232  14
         }
 233  14
         return conn;
 234  
     }
 235  
 
 236  
     /**
 237  
      * Determines if the H2 database file exists. If it does not exist then the
 238  
      * data structure will need to be created.
 239  
      *
 240  
      * @return true if the H2 database file does not exist; otherwise false
 241  
      * @throws IOException thrown if the data directory does not exist and
 242  
      * cannot be created
 243  
      */
 244  
     private static boolean h2DataFileExists() throws IOException {
 245  2
         final File dir = Settings.getDataDirectory();
 246  2
         final String fileName = Settings.getString(Settings.KEYS.DB_FILE_NAME);
 247  2
         final File file = new File(dir, fileName);
 248  2
         return file.exists();
 249  
     }
 250  
 
 251  
     /**
 252  
      * Creates the database structure (tables and indexes) to store the CVE
 253  
      * data.
 254  
      *
 255  
      * @param conn the database connection
 256  
      * @throws DatabaseException thrown if there is a Database Exception
 257  
      */
 258  
     private static void createTables(Connection conn) throws DatabaseException {
 259  0
         LOGGER.debug("Creating database structure");
 260  0
         InputStream is = null;
 261  
         try {
 262  0
             is = ConnectionFactory.class.getClassLoader().getResourceAsStream(DB_STRUCTURE_RESOURCE);
 263  0
             final String dbStructure = IOUtils.toString(is, "UTF-8");
 264  
 
 265  0
             Statement statement = null;
 266  
             try {
 267  0
                 statement = conn.createStatement();
 268  0
                 statement.execute(dbStructure);
 269  0
             } catch (SQLException ex) {
 270  0
                 LOGGER.debug("", ex);
 271  0
                 throw new DatabaseException("Unable to create database statement", ex);
 272  
             } finally {
 273  0
                 DBUtils.closeStatement(statement);
 274  0
             }
 275  0
         } catch (IOException ex) {
 276  0
             throw new DatabaseException("Unable to create database schema", ex);
 277  
         } finally {
 278  0
             IOUtils.closeQuietly(is);
 279  0
         }
 280  0
     }
 281  
 
 282  
     /**
 283  
      * Updates the database schema by loading the upgrade script for the version
 284  
      * specified. The intended use is that if the current schema version is 2.9
 285  
      * then we would call updateSchema(conn, "2.9"). This would load the
 286  
      * upgrade_2.9.sql file and execute it against the database. The upgrade
 287  
      * script must update the 'version' in the properties table.
 288  
      *
 289  
      * @param conn the database connection object
 290  
      * @param appExpectedVersion the schema version that the application expects
 291  
      * @param currentDbVersion the current schema version of the database
 292  
      * @throws DatabaseException thrown if there is an exception upgrading the
 293  
      * database schema
 294  
      */
 295  
     private static void updateSchema(Connection conn, DependencyVersion appExpectedVersion, DependencyVersion currentDbVersion)
 296  
             throws DatabaseException {
 297  
 
 298  
         final String databaseProductName;
 299  
         try {
 300  0
             databaseProductName = conn.getMetaData().getDatabaseProductName();
 301  0
         } catch (SQLException ex) {
 302  0
             throw new DatabaseException("Unable to get the database product name");
 303  0
         }
 304  0
         if ("h2".equalsIgnoreCase(databaseProductName)) {
 305  0
             LOGGER.debug("Updating database structure");
 306  0
             InputStream is = null;
 307  0
             String updateFile = null;
 308  
             try {
 309  0
                 updateFile = String.format(DB_STRUCTURE_UPDATE_RESOURCE, currentDbVersion.toString());
 310  0
                 is = ConnectionFactory.class.getClassLoader().getResourceAsStream(updateFile);
 311  0
                 if (is == null) {
 312  0
                     throw new DatabaseException(String.format("Unable to load update file '%s'", updateFile));
 313  
                 }
 314  0
                 final String dbStructureUpdate = IOUtils.toString(is, "UTF-8");
 315  
 
 316  0
                 Statement statement = null;
 317  
                 try {
 318  0
                     statement = conn.createStatement();
 319  0
                     final boolean success = statement.execute(dbStructureUpdate);
 320  0
                     if (!success && statement.getUpdateCount() <= 0) {
 321  0
                         throw new DatabaseException(String.format("Unable to upgrade the database schema to %s",
 322  0
                                 currentDbVersion.toString()));
 323  
                     }
 324  0
                 } catch (SQLException ex) {
 325  0
                     LOGGER.debug("", ex);
 326  0
                     throw new DatabaseException("Unable to update database schema", ex);
 327  
                 } finally {
 328  0
                     DBUtils.closeStatement(statement);
 329  0
                 }
 330  0
             } catch (IOException ex) {
 331  0
                 final String msg = String.format("Upgrade SQL file does not exist: %s", updateFile);
 332  0
                 throw new DatabaseException(msg, ex);
 333  
             } finally {
 334  0
                 IOUtils.closeQuietly(is);
 335  0
             }
 336  0
         } else {
 337  0
             final int e0 = Integer.parseInt(appExpectedVersion.getVersionParts().get(0));
 338  0
             final int c0 = Integer.parseInt(currentDbVersion.getVersionParts().get(0));
 339  0
             final int e1 = Integer.parseInt(appExpectedVersion.getVersionParts().get(1));
 340  0
             final int c1 = Integer.parseInt(currentDbVersion.getVersionParts().get(1));
 341  0
             if (e0 == c0 && e1 < c1) {
 342  0
                 LOGGER.warn("A new version of dependency-check is available; consider upgrading");
 343  0
                 Settings.setBoolean(Settings.KEYS.AUTO_UPDATE, false);
 344  0
             } else if (e0 == c0 && e1 == c1) {
 345  
                 //do nothing - not sure how we got here, but just incase...
 346  
             } else {
 347  0
                 LOGGER.error("The database schema must be upgraded to use this version of dependency-check. Please see {} for more information.",
 348  
                         UPGRADE_HELP_URL);
 349  0
                 throw new DatabaseException("Database schema is out of date");
 350  
             }
 351  
         }
 352  0
     }
 353  
 
 354  
     /**
 355  
      * Counter to ensure that calls to ensureSchemaVersion does not end up in an
 356  
      * endless loop.
 357  
      */
 358  1
     private static int callDepth = 0;
 359  
 
 360  
     /**
 361  
      * Uses the provided connection to check the specified schema version within
 362  
      * the database.
 363  
      *
 364  
      * @param conn the database connection object
 365  
      * @throws DatabaseException thrown if the schema version is not compatible
 366  
      * with this version of dependency-check
 367  
      */
 368  
     private static void ensureSchemaVersion(Connection conn) throws DatabaseException {
 369  2
         ResultSet rs = null;
 370  2
         PreparedStatement ps = null;
 371  
         try {
 372  
             //TODO convert this to use DatabaseProperties
 373  2
             ps = conn.prepareStatement("SELECT value FROM properties WHERE id = 'version'");
 374  2
             rs = ps.executeQuery();
 375  2
             if (rs.next()) {
 376  2
                 final DependencyVersion appDbVersion = DependencyVersionUtil.parseVersion(DB_SCHEMA_VERSION);
 377  2
                 if (appDbVersion == null) {
 378  0
                     throw new DatabaseException("Invalid application database schema");
 379  
                 }
 380  2
                 final DependencyVersion db = DependencyVersionUtil.parseVersion(rs.getString(1));
 381  2
                 if (db == null) {
 382  0
                     throw new DatabaseException("Invalid database schema");
 383  
                 }
 384  2
                 if (appDbVersion.compareTo(db) > 0) {
 385  0
                     LOGGER.debug("Current Schema: {}", DB_SCHEMA_VERSION);
 386  0
                     LOGGER.debug("DB Schema: {}", rs.getString(1));
 387  0
                     updateSchema(conn, appDbVersion, db);
 388  0
                     if (++callDepth < 10) {
 389  0
                         ensureSchemaVersion(conn);
 390  
                     }
 391  
                 }
 392  2
             } else {
 393  0
                 throw new DatabaseException("Database schema is missing");
 394  
             }
 395  0
         } catch (SQLException ex) {
 396  0
             LOGGER.debug("", ex);
 397  0
             throw new DatabaseException("Unable to check the database schema version");
 398  
         } finally {
 399  2
             DBUtils.closeResultSet(rs);
 400  2
             DBUtils.closeStatement(ps);
 401  2
         }
 402  2
     }
 403  
 }