changeset 2:18e956c96a1b database-connection-manager

Add LTPDADatabaseConnectionManager implementation. Matlab code
author Daniele Nicolodi <nicolodi@science.unitn.it>
date Sun, 04 Dec 2011 21:23:09 +0100
parents 2014ba5b353a
children 960fe1aa1c10
files m-toolbox/classes/+utils/@credentials/credentials.m m-toolbox/classes/+utils/@mysql/connect.m m-toolbox/classes/+utils/@mysql/mysql.m m-toolbox/classes/@LTPDADatabaseConnectionManager/LTPDADatabaseConnectionManager.m
diffstat 4 files changed, 733 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m-toolbox/classes/+utils/@credentials/credentials.m	Sun Dec 04 21:23:09 2011 +0100
@@ -0,0 +1,183 @@
+classdef credentials
+
+  properties
+
+    hostname = [];
+    database = [];
+    username = [];
+    password = [];
+    expiry = 0;
+
+  end % properties
+
+  methods
+
+    function obj = credentials(hostname, database, username, password)
+    % CREDENTIALS Constructor for credentials objects.
+    %
+    % Those are simple container objects to hold credentials required for
+    % establishing a connection to a database server, in addition to an
+    % expiry time.
+    %
+    % CREDENTIALS(hostname, database, username, password) The constructor can
+    % be called with any number of arguments. The default value for the object
+    % properties is the empty vector.
+
+      switch nargin
+        case 1
+          obj.hostname = hostname;
+        case 2
+          obj.hostname = hostname;
+          obj.database = database;
+        case 3
+          obj.hostname = hostname;
+          obj.database = database;
+          obj.username = username;
+        case 4
+          obj.hostname = hostname;
+          obj.database = database;
+          obj.username = username;
+          obj.password = password;
+      end
+    end
+
+    function str = char(obj, mode)
+    % CHAR Convert a credentials object to string representation.
+    %
+    % It takes an optional second argument that defines the representation to
+    % use. The default is to replace the password, if present, with the YES
+    % string, other possible modes are SHORT where password is omitted, or FULL
+    % where the password is shown at it is.
+
+      if nargin < 2
+        mode = '';
+      end
+      switch mode
+        case 'short'
+          % do not show password
+          frm = 'mysql://%s/%s username=%s';
+          str = sprintf(frm, obj.hostname, obj.database, obj.username);
+        case 'full'
+          % show password
+          frm = 'mysql://%s/%s username=%s password=%s';
+          str = sprintf(frm, obj.hostname, obj.database, obj.username, obj.password);
+        otherwise
+          % by default only show if a password is known
+          passwd = [];
+          if ischar(obj.password)
+            passwd = 'YES';
+          end
+          frm = 'mysql://%s/%s username=%s password=%s';
+          str = sprintf(frm, obj.hostname, obj.database, obj.username, passwd);
+      end
+    end
+
+    function disp(objs)
+    % DISP Overloaded display method for credentials objects.
+    %
+    % Uses the default string representation of the char() method where the
+    % password, if present, is replaced with the string YES.
+
+      for obj = objs
+        disp(['    ' char(obj) char(10)]);
+      end
+    end
+
+    function len = length(obj)
+    % LENGTH Returns the number of not null fields in the object.
+
+      len = 0;
+      if ~isempty(obj.hostname)
+        len = len + 1;
+      end
+      if ~isempty(obj.database)
+        len = len + 1;
+      end
+      if ~isempty(obj.username)
+        len = len + 1;
+      end
+      if ~isempty(obj.password)
+        len = len + 1;
+      end
+    end
+
+    function rv = complete(obj)
+    % COMPLETE Checks if the credentials are complete.
+    %
+    % Credentials object are complete when they contains all the required
+    % information to connect to a database. Namely the HOSTNAME, DATABASE
+    % and USERNAME properties should not be empty, the PASSWORD property is
+    % allowed to be an empty string '' but not [].
+
+      info = {'hostname', 'database', 'username'};
+      for kk = 1:numel(info)
+        if isempty(obj.(info{kk}))
+          rv = false;
+          return;
+        end
+      end
+      if ~ischar(obj.password)
+        rv = false;
+        return;
+      end
+      rv = true;
+    end
+
+    function rv = expired(obj)
+    % EXPIRED Checks if the credentials are expired.
+    %
+    % Credential objects expire when their expiry time is smaller than the
+    % current time in seconds since the epoch, as obtained by the time()
+    % function. Credentials with zero or negative expiry time never expire.
+
+      rv = false;
+      if obj.expiry > 0 && double(time()) > obj.expiry
+        rv = true;
+      end
+    end
+
+    function rv = match(obj, hostname, database, username)
+    % MATCH Check if the credentials object matches the given information.
+    %
+    % MATCH(obj, hostname) Returns true when HOSTANAME parameter match
+    % the object properties.
+    %
+    % MATCH(obj, hostname, database) Returns true when HOSTANAME and
+    % DATABASE parameters match the object properties.
+    %
+    % MATCH(obj, hostname, database, username) Returns true when
+    % HOSTANAME, DATABASE, and USERNAME parameters match the object
+    % properties.
+
+      % default arguments
+      switch nargin
+        case 4
+        case 3
+          username = [];
+        case 2
+          username = [];
+          database = [];
+        otherwise
+          error('### wrong number of parameters');
+      end
+
+      % default return value
+      rv = true;
+
+      if ~strcmp(obj.hostname, hostname)
+        rv = false;
+        return;
+      end
+      if ischar(database) && ~strcmp(obj.database, database)
+        rv = false;
+        return;
+      end
+      if ischar(username) && ~strcmp(obj.username, username)
+        rv = false;
+        return;
+      end
+    end
+
+  end % methods
+
+end
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m-toolbox/classes/+utils/@mysql/connect.m	Sun Dec 04 21:23:09 2011 +0100
@@ -0,0 +1,38 @@
+function conn = connect(hostname, database, username, password)
+% CONNECT Opens a connection to the given database.
+%
+% CALL:
+%
+%   conn = utils.mysql.connect(hostname, database, username, password)
+%
+% This function returns a Java object implementing the java.sql.Connection
+% interface connected to the given database using the provided credentials.
+% If the connection fails because the given username and password pair is not
+% accepted by the server an utils:mysql:connect:AccessDenied error is thrown.
+%
+
+  % informative message
+  import utils.const.*
+  utils.helper.msg(msg.PROC1, 'connection to mysql://%s/%s username=%s', hostname, database, username);
+
+  % connection credential
+  uri = sprintf('jdbc:mysql://%s/%s', hostname, database);
+  db = javaObject('com.mysql.jdbc.Driver');
+  pl = javaObject('java.util.Properties');
+  pl.setProperty(db.USER_PROPERTY_KEY, username);
+  pl.setProperty(db.PASSWORD_PROPERTY_KEY, password);
+
+  try
+    % connect
+    conn = db.connect(uri, pl);
+  catch ex
+    % haven't decided yet if this code should be here or higher in the stack
+    if strcmp(ex.identifier, 'MATLAB:Java:GenericException')
+      % exceptions handling in matlab sucks
+      if ~isempty(strfind(ex.message, 'java.sql.SQLException: Access denied'))
+        throw(MException('utils:mysql:connect:AccessDenied', '### access denied').addCause(ex));
+      end
+    end
+    rethrow(ex);
+  end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m-toolbox/classes/+utils/@mysql/mysql.m	Sun Dec 04 21:23:09 2011 +0100
@@ -0,0 +1,10 @@
+classdef mysql
+% UTILS.MYSQL  MySQL database utilities.
+
+  methods (Static)
+
+    conn = connect(hostname, database, username, password)
+
+  end % static methods
+
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/m-toolbox/classes/@LTPDADatabaseConnectionManager/LTPDADatabaseConnectionManager.m	Sun Dec 04 21:23:09 2011 +0100
@@ -0,0 +1,502 @@
+classdef LTPDADatabaseConnectionManager < handle
+
+  properties(SetAccess=private)
+
+    connections = {};
+    credentials = {};
+
+    p_credentialsExpiry = 3600;
+    p_cachePassword = 2;
+    p_maxConnectionsNumber = 10;
+
+  end % private properties
+
+  properties(Dependent=true)
+
+    credentialsExpiry; % seconds
+    cachePassword; % 0=no 1=yes 2=ask
+    maxConnectionsNumber;
+
+  end % dependent properties
+
+  methods(Static)
+
+    function reset()
+    % RESET Resets the state of the connection manager.
+    %
+    % This static method removes the LTPDADatabaseConnectionManager
+    % instance data from the appdata storage. Causes the reset of the
+    % credentials cache and the removal of all the connections from
+    % the connection pool.
+
+      rmappdata(0, LTPDADatabaseConnectionManager.appdataKey);
+    end
+
+
+    function key = appdataKey()
+    % APPDATAKEY Returns the key used to store instance data in appdata.
+    %
+    % This is defined as static method, and not has an instance constant
+    % property, to to be accessible by the reset static method.
+
+      key = 'LTPDADatabaseConnectionManager';
+    end
+
+  end % static methods
+
+  methods
+
+    function cm = LTPDADatabaseConnectionManager()
+    % LTPDACONNECTIONMANAGER Manages credentials and database connections.
+    %
+    % This constructor returns an handler to a LTPDADatabaseConnectionManager
+    % class instance. Database connections can be obtained trough the
+    % obtained object with the connect() method.
+    %
+    % The purpose of this class it to keep track of open database connections
+    % and to cache database credentials. It must be used in all LTPDA toolbox
+    % functions that required to obtain database connections. Its behaviour can
+    % be configured via LTPDA toolbox user preferences. The object status is
+    % persisted trough the appdata matlab facility.
+
+      % import credentials class
+      import utils.credentials
+
+      % load state from appdata
+      acm = getappdata(0, cm.appdataKey());
+
+      if isempty(acm)
+        % store state in appdata
+        setappdata(0, cm.appdataKey(), cm);
+
+        import utils.const.*
+        utils.helper.msg(msg.PROC1, 'new connection manager');
+      else
+        cm = acm;
+      end
+    end
+
+    function str = disp(cm)
+      disp(sprintf('%s()\n', class(cm)));
+    end
+
+
+    function val = get.credentialsExpiry(cm)
+      % obtain from user preferences
+      %p = getappdata(0, 'LTPDApreferences');
+      val = cm.p_credentialsExpiry;
+    end
+
+
+    function val = get.cachePassword(cm)
+      % obtain from user preferences
+      %p = getappdata(0, 'LTPDApreferences');
+      val = cm.p_cachePassword;
+    end
+
+
+    function val = get.maxConnectionsNumber(cm)
+      % obtain from user preferences
+      %p = getappdata(0, 'LTPDApreferences');
+      val = cm.p_maxConnectionsNumber;
+    end
+
+
+    function n = count(cm)
+    % COUNT Returns the number of open connections in the connections pool.
+    %
+    % This method has the side effect of removing all closed connections from
+    % the connections pool, so that the underlying objects can be garbage
+    % collected.
+
+      import utils.const.*
+
+      % find closed connections in the pool
+      mask = false(numel(cm.connections), 1);
+      for kk = 1:numel(cm.connections)
+        if cm.connections{kk}.isClosed()
+          utils.helper.msg(msg.PROC1, 'connection id=%d closed', kk);
+          mask(kk) = true;
+        end
+      end
+
+      % remove them
+      cm.connections(mask) = [];
+
+      % count remainig ones
+      n = numel(cm.connections);
+    end
+
+    function clear(cm)
+    % CLEAR Removes all cached credentials from the connection manager.
+      cm.credentials = {};
+    end
+
+
+    function conn = connect(cm, varargin)
+    % CONNECT Uses provided credential to establish a database connection.
+    %
+    % CONNECT(hostname, database, username, password) Returns an object
+    % implementing the java.sql.Connection interface handing a connection to
+    % the specified database. Any of the parameter is optional. The user will
+    % be queried for the missing information.
+    %
+    % The returned connection are added to a connections pool. When the number
+    % of connections in the pool exceeds a configurable maximum, no more
+    % connection are instantiated. Closed connections are automatically
+    % removed from the pool.
+    %
+    % CONNECT(pl) Works as the above but the parameters are obtained from the
+    % plist object PL. If the 'connection' parameter in the plist contains an
+    % object implementing the java.sql.Connection interface, this object is
+    % returned instead that opening a new connection. In this case the
+    % connection in not added to the connection pool.
+
+      import utils.const.*
+
+      % save current credentials cache
+      cache = cm.credentials;
+
+      % count open connections in the pool
+      count = cm.count();
+
+      % check parameters
+      if numel(varargin) == 1 && isa(varargin{1}, 'plist')
+
+        % extract parameters from plist
+        pl = varargin{1};
+
+        % check if we have a connection parameter
+        conn = find(pl, 'conn');
+        if ~isempty(conn)
+          % check that it implements java.sql.Connection interface
+          if ~isa(conn, 'java.sql.Connection')
+            error('### connection is not valid database connection');
+          end
+          % return this connection
+          return;
+        end
+
+        % otherwise
+        hostname = find(pl, 'hostname');
+        database = find(pl, 'database');
+        username = find(pl, 'username');
+        password = find(pl, 'password');
+
+        % if there is no hostname ignore other parameters
+        if ~ischar(hostname) || isempty(hostname)
+          varargin = {};
+        % if there is no database ignore other parameters
+        elseif ~ischar(database) || isempty(database)
+          varargin = {hostname};
+        % if there is no username ignore other parameters
+        elseif ~ischar(username) || isempty(username)
+          varargin = {hostname, database};
+        % password can not be null but can be an empty string
+        elseif ~ischar(password)
+          varargin = {hostname, database, username};
+        else
+          varargin = {hostname, database, username, password};
+        end
+      end
+
+      % check number of connections
+      if count > cm.maxConnectionsNumber
+        error('### too many open connections');
+      end
+
+      % connect
+      try
+        conn = cm.getConnection(varargin{:});
+
+        % add connection to pool
+        utils.helper.msg(msg.PROC1, 'add connection to pool');
+        cm.connections{end+1} = conn;
+
+      catch ex
+        % restore our copy of the credentials cache
+        utils.helper.msg(msg.PROC1, 'undo cache changes');
+        cm.credentials = cache;
+
+        % hide implementation details
+        ex.throw();
+      end
+    end
+
+
+    function close(cm, ids)
+    % CLOSE Forces connections to be closed.
+    %
+    % In the case bugs in other routines working with database connections
+    % produce orphan connections, this method can be used to force the close
+    % of those connections.
+    %
+    % CLOSE(ids) Closes the connections with the corresponding IDs in the
+    % connections pool. If no ID is given all connections in the pool are
+    % closed.
+
+      if nargin < 2
+        ids = 1:numel(cm.connections);
+      end
+      cellfun(@close, cm.connections(ids));
+
+      % remove closed connections from pool
+      cm.count();
+    end
+
+
+    function add(cm, c)
+    % ADD Adds credentials to the credentials cache.
+    %
+    % This method can be used to initialize or add to the cache, credentials
+    % that will be used in subsequent connections attempts. This method accepts
+    % only credentials in the form of utils.credentials objects.
+
+      % check input arguments
+      if nargin < 2 || ~isa(c, 'utils.credentials')
+        error('### invalid call');
+      end
+
+      % add to the cache
+      cm.cacheCredentials(c);
+    end
+
+  end % methods
+
+  methods(Access=private)
+
+    function conn = getConnection(cm, varargin)
+    % GETCONNECTION Where the implementation of the connect method really is.
+
+      import utils.const.*
+
+      % handle variable number of arguments
+      switch numel(varargin)
+        case 0
+          % find credentials
+          [hostname, database, username] = cm.selectDatabase([cm.credentials{:}]);
+          conn = cm.getConnection(hostname, database, username);
+
+        case 1
+          % find credentials
+          cred = cm.findCredentials(varargin{:});
+          if numel(cred) == 0
+            cred = utils.credentials(varargin{:});
+          end
+          [hostname, database, username] = cm.selectDatabase(cred);
+          conn = cm.getConnection(hostname, database, username);
+
+        case 2
+          % find credentials
+          cred = cm.findCredentials(varargin{:});
+          switch numel(cred)
+            case 0
+              conn = cm.getConnection(varargin{1}, varargin{2}, []);
+            case 1
+              conn = cm.getConnection(cred.hostname, cred.database, cred.username);
+            otherwise
+              [hostname, database, username] = cm.selectDatabase(cred);
+              conn = cm.getConnection(hostname, database, username);
+          end
+
+        case 3
+          % find credentials
+          cred = cm.findCredentials(varargin{1}, varargin{2}, varargin{3});
+          switch numel(cred)
+            case 0
+              % no credentials found
+              usernames = { varargin{3} };
+              if isempty(varargin{3})
+                % use usernames for same hostname
+                tmp = cm.findCredentials(varargin{1});
+                if ~isempty(tmp)
+                  usernames = { tmp(:).username };
+                end
+              end
+              % build credentials objects
+              tmp = {};
+              for kk = 1:numel(usernames)
+                tmp{kk} = utils.credentials(varargin{1}, varargin{2}, usernames{kk});
+              end
+              % convert from cell array to array
+              cred = [tmp{:}];
+            case 1
+              % credentials in cache
+              utils.helper.msg(msg.PROC1, 'use cached credentials');
+            otherwise
+              % we should not have more than one credentials set
+              error('### more than one credentials set matching');
+          end
+
+          cache = false;
+          if (numel(cred) > 1) || ~cred.complete
+            % ask for password
+            [username, password, cache] = cm.inputCredentials(cred);
+
+            % cache credentials
+            cred = utils.credentials(varargin{1}, varargin{2}, username);
+            cm.cacheCredentials(cred);
+
+            % add password to credentials
+            cred.password = password;
+          end
+
+          % try to connect
+          try
+            conn = utils.mysql.connect(cred.hostname, cred.database, cred.username, cred.password);
+          catch ex
+            % look for access denied errors
+            if strcmp(ex.identifier, 'utils:mysql:connect:AccessDenied')
+              % ask for new new credentials
+              utils.helper.msg(msg.IMPORTANT, ex.message);
+              conn = cm.getConnection(varargin{1}, varargin{2}, varargin{3});
+            else
+              % error out
+              throw(ex);
+            end
+          end
+
+          % cache password
+          if cache
+            utils.helper.msg(msg.PROC1, 'cache password');
+            cm.cacheCredentials(cred);
+          end
+
+        case 4
+          % connect
+          conn = utils.mysql.connect(varargin{1}, varargin{2}, varargin{3}, varargin{4});
+
+          if cm.cachePassword == 1
+            % cache credentials with password
+            cred = utils.credentials(varargin{1}, varargin{2}, varargin{3}, varargin{4});
+          else
+            % cache credentials without password
+            cred = utils.credentials(varargin{1}, varargin{2}, varargin{3});
+          end
+          cm.cacheCredentials(cred);
+
+        otherwise
+          error('### invalid call')
+      end
+
+    end
+
+
+    function ids = findCredentialsId(cm, varargin)
+    % FINDCREDENTIALSID Find credentials in the cache and returns their IDs.
+
+      import utils.const.*
+      ids = [];
+
+      for kk = 1:numel(cm.credentials)
+        % invalidate expired passwords
+        if expired(cm.credentials{kk})
+          utils.helper.msg(msg.PROC1, 'cache entry id=%d expired', kk);
+          cm.credentials{kk}.password = [];
+          cm.credentials{kk}.expiry = 0;
+        end
+
+        % match input with cache
+        if match(cm.credentials{kk}, varargin{:})
+          ids = [ ids kk ];
+        end
+      end
+    end
+
+
+    function cred = findCredentials(cm, varargin)
+    % FINDCREDENTIALS Find credentials in the cache and returns them in a list.
+
+      % default
+      cred = [];
+
+      % search
+      ids = findCredentialsId(cm, varargin{:});
+
+      % return a credentials objects array
+      if ~isempty(ids)
+        cred = [cm.credentials{ids}];
+      end
+    end
+
+
+    function cacheCredentials(cm, c)
+    % CACHECREDENTIALS Adds to or updates the credentials cache.
+
+      import utils.const.*
+
+      % find entry to update
+      ids = findCredentialsId(cm, c.hostname, c.database, c.username);
+
+      % set password expiry time
+      if ischar(c.password)
+        c.expiry = double(time()) + cm.credentialsExpiry;
+      end
+
+      if isempty(ids)
+        % add at the end
+        utils.helper.msg(msg.PROC1, 'add cache entry %s', char(c));
+        cm.credentials{end+1} = c;
+      else
+        for id = ids
+          % update only if the cached informations are less than the one we have
+          if length(c) > length(cm.credentials{id})
+            utils.helper.msg(msg.PROC1, 'update cache entry id=%d %s', id, char(c));
+            cm.credentials{id} = c;
+          else
+            % always update expiry time
+            cm.credentials{id}.expiry = c.expiry;
+          end
+        end
+      end
+    end
+
+
+    function [username, password, cache] = inputCredentials(cm, cred)
+    % INPUTCREDENTIALS Queries the user for database username and password.
+
+      % build a cell array of usernames
+      users = {};
+      for id = 1:numel(cred)
+        if ~isempty(cred(id).username)
+          users = [ users { cred(id).username } ];
+        end
+      end
+      users = sort(unique(users));
+
+      parent = com.mathworks.mde.desk.MLDesktop.getInstance().getMainFrame();
+      dialog = javaObjectEDT('connectionmanager.CredentialsDialog', ...
+          parent, cred(1).hostname, cred(1).database, users, cm.cachePassword);
+      dialog.show();
+      if dialog.cancelled
+        throw(MException('utils:mysql:connect:UserCancelled', '### user cancelled'));
+      end
+      username = char(dialog.username);
+      password = char(dialog.password);
+      cache    = logical(dialog.cache);
+    end
+
+
+    function [hostname, database, username] = selectDatabase(cm, credentials)
+    % SELECTDATABASE Makes the user choose to which database connect to.
+
+      parent = com.mathworks.mde.desk.MLDesktop.getInstance().getMainFrame();
+      dialog = javaObjectEDT('connectionmanager.DatabaseSelectorDialog', parent);
+      for c = credentials
+        dialog.add(c.hostname, c.database, c.username);
+      end
+      dialog.show();
+      if dialog.cancelled
+        throw(MException('utils:mysql:connect:UserCancelled', '### user cancelled'));
+      end
+      hostname = char(dialog.hostname);
+      database = char(dialog.database);
+      username = char(dialog.username);
+      if isempty(username)
+        username = [];
+      end
+    end
+
+  end % private methods
+
+end % classdef