package org.openslx.imagemaster.db; import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collections; import java.util.Properties; import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import org.apache.log4j.Logger; import org.openslx.imagemaster.util.Util; public class Database { private static final Logger LOGGER = Logger.getLogger( Database.class ); /** * Pool of available connections. */ private static final Queue pool = new ConcurrentLinkedQueue<>(); /** * Set of connections currently handed out. */ private static final Set busyConnections = Collections.newSetFromMap( new ConcurrentHashMap() ); private static final String host; private static final String dbname; private static final String user; private static final String password; /** * Static initializer for setting up the database connection. * This gets called implicitly as soon as the class loader loads * the class. In most cases that happens when the class is being * accessed for the first time during run time. */ static { // Load connection info from class (TODO: Make pretty) Properties properties = new Properties(); try { final BufferedInputStream stream = new BufferedInputStream( new FileInputStream( "config/mysql.properties" ) ); properties.load( stream ); stream.close(); } catch ( FileNotFoundException e ) { LOGGER.fatal( "config/mysql.properties not found!" ); System.exit( 1 ); } catch ( IOException e ) { LOGGER.fatal( "Error reading from config/mysql.properties: " + e.getMessage() ); System.exit( 1 ); } catch ( Exception e ) { LOGGER.fatal( "Generic error loading mysql properties file." ); e.printStackTrace(); System.exit( 1 ); } host = properties.getProperty( "host" ); dbname = properties.getProperty( "db" ); user = properties.getProperty( "user" ); password = properties.getProperty( "password" ); Util.notNullFatal( host, "host not set in mysql properties" ); Util.notNullFatal( dbname, "db not set in mysql properties" ); Util.notNullFatal( user, "user not set in mysql properties" ); Util.notNullFatal( password, "password not set in mysql properties" ); try { Class.forName( "com.mysql.jdbc.Driver" ).newInstance(); } catch ( InstantiationException | IllegalAccessException | ClassNotFoundException e ) { LOGGER.fatal( "Cannot get mysql JDBC driver!", e ); System.exit( 1 ); } } /** * Get a connection to the database. If there is a valid connection in the * pool, it will be returned. Otherwise, a new connection is created. If * there are more than 20 busy connections, null is returned. * * @return connection to database, or null */ public static MysqlConnection getConnection() { MysqlConnection con; for ( ;; ) { con = pool.poll(); if ( con == null ) break; if ( !con.isValid() ) { con.release(); continue; } if ( !busyConnections.add( con ) ) throw new RuntimeException( "Tried to hand out a busy connection!" ); return con; } // No pooled connection if ( busyConnections.size() > 20 ) { LOGGER.warn( "Too many open MySQL connections. Possible connection leak!" ); return null; } try { // Create fresh connection String uri = "jdbc:mysql://" + host + "/" + dbname + "?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8" + "&characterSetResults=utf8&connectionCollation=utf8mb4_unicode_ci"; Connection rawConnection = DriverManager.getConnection( uri, user, password ); // By convention in our program we don't want auto commit rawConnection.setAutoCommit( false ); // Wrap into our proxy con = new MysqlConnection( rawConnection ); // Keep track of busy mysql connection if ( !busyConnections.add( con ) ) throw new RuntimeException( "Tried to hand out a busy connection!" ); return con; } catch ( SQLException e ) { LOGGER.info( "Failed to connect to local mysql server", e ); } return null; } /** * Called by a {@link MysqlConnection} when its close()-method * is called, so the connection will be added to the pool of available * connections again. * * @param connection */ static void returnConnection( MysqlConnection connection ) { if ( !busyConnections.remove( connection ) ) throw new RuntimeException( "Tried to return a mysql connection to the pool that was not taken!" ); pool.add( connection ); } /** * Return true if the given sql exception is "duplicate entry XXXX for key YYYY. */ public static boolean isDuplicateKeyException( SQLException e ) { return e != null && e.getErrorCode() == 1062; } // public static void printCharsetInformation() { LOGGER.info( "MySQL charset related variables:" ); try ( MysqlConnection connection = Database.getConnection() ) { MysqlStatement stmt = connection.prepareStatement( "SHOW VARIABLES LIKE :what" ); stmt.setString( "what", "char%" ); ResultSet rs = stmt.executeQuery(); while ( rs.next() ) { LOGGER.info( rs.getString( "Variable_name" ) + ": " + rs.getString( "Value" ) ); } stmt.setString( "what", "collat%" ); rs = stmt.executeQuery(); while ( rs.next() ) { LOGGER.info( rs.getString( "Variable_name" ) + ": " + rs.getString( "Value" ) ); } } catch ( SQLException e ) { LOGGER.error( "Query failed in Database.printCharsetInformation()", e ); } LOGGER.info( "End of variables" ); } public static void printDebug() { LOGGER.info( "Available: " + pool.size() ); LOGGER.info( "Busy: " + busyConnections.size() ); } }// end class