diff LTPDAConnectionManager.m @ 0:d5fef23867bb

First workig implementation.
author Daniele Nicolodi <daniele@science.unitn.it>
date Sun, 23 May 2010 10:51:35 +0200
parents
children c706c10a76bd
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LTPDAConnectionManager.m	Sun May 23 10:51:35 2010 +0200
@@ -0,0 +1,441 @@
+classdef LTPDAConnectionManager < handle
+
+  properties(SetAccess=private)
+
+    connections = {};
+    credentials = {};
+
+  end % private properties
+
+  properties(Dependent=true)
+
+    credentialsExpiry; % seconds
+    cachePassword; % 0=no 1=yes 2=ask
+    maxConnectionsNumber;
+
+  end % dependent properties
+
+  methods(Static)
+
+    function reset()
+      setappdata(0, LTPDAConnectionManager.appdataKey, []);
+    end
+
+    function key = appdataKey()
+      % defined as static method to be acessible by the reset static method
+      key = 'LTPDAConnectionManager';
+    end
+
+  end % static methods
+
+  methods
+
+    function cm = LTPDAConnectionManager(pl)
+
+      % load state from appdata
+      acm = getappdata(0, cm.appdataKey());
+
+      if isempty(acm)
+        % take those from user preferences
+        cm.credentials{end+1} = credentials('localhost', 'one');
+        cm.credentials{end+1} = credentials('localhost', 'two', 'daniele');
+
+        % 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 val = get.credentialsExpiry(cm)
+      % obtain from user preferences
+      p = getappdata(0, 'LTPDApreferences');
+      val = p.cm.credentialsExpiry;
+    end
+
+
+    function val = get.cachePassword(cm)
+      % obtain from user preferences
+      p = getappdata(0, 'LTPDApreferences');
+      val = p.cm.cachePassword;
+    end
+
+
+    function val = get.maxConnectionsNumber(cm)
+      % obtain from user preferences
+      p = getappdata(0, 'LTPDApreferences');
+      val = p.cm.maxConnectionsNumber;
+    end
+
+
+    function n = count(cm)
+      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)
+      % remove all cached credentials
+      cm.credentials = {};
+    end
+
+
+    function conn = connect(cm, varargin)
+      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, 'connection');
+        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 and database ignore other parameters
+        if ~ischar(hostname) || ~ischar(database)
+          varargin = {};
+        end
+        % password can not be null but can be an empty string
+        if ~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{:});
+      catch ex
+        % restore our copy of the credentials cache
+        utils.helper.msg(msg.PROC1, 'undo cache changes');
+        cm.credentials = cache;
+
+        % hide implementation details
+        %ex.throwAsCaller();
+        ex.rethrow()
+      end
+    end
+
+
+    function close(cm, ids)
+      if nargin < 2
+        ids = 1:numel(cm.connections);
+      end
+      cellfun(@close, cm.connections(ids));
+    end
+
+
+    function add(cm, c)
+      if nargin < 2 || ~isa(c, 'credentials')
+        error('### invalid call');
+      end
+      cm.cacheCredentials(c);
+    end
+
+  end % methods
+
+  methods(Access=private)
+
+    function conn = getConnection(cm, varargin)
+
+      import utils.const.*
+
+      switch numel(varargin)
+        case 0
+          [hostname, database, username] = cm.selectDatabase();
+          conn = cm.getConnection(hostname, database, username);
+
+        case 2
+          conn = cm.getConnection(varargin{1}, varargin{2}, []);
+
+        case 3
+          % find credentials
+          cred = cm.findCredentials(varargin{1}, varargin{2}, varargin{3});
+          if isempty(cred)
+            % no credentials found
+            cred = credentials(varargin{1}, varargin{2}, varargin{3});
+          else
+            utils.helper.msg(msg.PROC1, 'use cached credentials');
+          end
+
+          cache = false;
+          if numel(cred) > 1 || ~cred.complete
+            % ask for which username and password to use
+            [username, password, cache] = cm.inputCredentials(cred);
+
+            % cache credentials
+            cred = credentials(varargin{1}, varargin{2}, username);
+            cm.cacheCredentials(cred);
+
+            % add password to credentials
+            cred.password = password;
+          end
+
+          % try to connect
+          conn = cm.getConnection(cred.hostname, cred.database, cred.username, cred.password);
+
+          % cache password
+          if cache
+            utils.helper.msg(msg.PROC1, 'cache password');
+            cm.cacheCredentials(cred);
+          end
+
+        case 4
+          try
+            % connect
+            conn = connect(varargin{1}, varargin{2}, varargin{3}, varargin{4});
+
+            % cache credentials without password
+            cred = credentials(varargin{1}, varargin{2}, varargin{3}, []);
+            cm.cacheCredentials(cred);
+
+          catch ex
+            % look for access denied errors
+            if strcmp(ex.identifier, 'utils:jmysql:connect:AccessDenied')
+              % ask for new new credentials
+              utils.helper.msg(msg.PROC1, ex.message);
+              conn = cm.getConnection(varargin{1}, varargin{2}, varargin{3});
+            else
+              % error out
+              throw(MException('', '### connection error').addCause(ex));
+            end
+          end
+
+          % add connection to pool
+          utils.helper.msg(msg.PROC1, 'add connection to pool');
+          cm.connections{end+1} = conn;
+
+        otherwise
+          error('### invalid call')
+      end
+
+    end
+
+
+    function ids = findCredentialsId(cm, varargin)
+      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)
+      % default
+      cred = [];
+
+      % search
+      ids = findCredentialsId(cm, varargin{:});
+
+      % return an array credentials
+      if ~isempty(ids)
+        cred = [ cm.credentials{ids} ];
+      end
+    end
+
+
+    function cacheCredentials(cm, c)
+      import utils.const.*
+
+      % find entry to update
+      id = findCredentialsId(cm, c.hostname, c.database, c.username);
+
+      % sanity check
+      if numel(id) > 1
+        error('### more than one cache entry for %s', char(c, 'short'));
+      end
+
+      % set password expiry time
+      if ischar(c.password)
+        c.expiry = double(time()) + cm.credentialsExpiry;
+      end
+
+      if isempty(id)
+        % add at the end
+        utils.helper.msg(msg.PROC1, 'add cache entry %s', char(c));
+        cm.credentials{end+1} = c;
+      else
+        % update only if the cached informations are less than the one we have
+        if ~complete(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
+
+
+    function [username, password, cache] = inputCredentials(cm, cred)
+      % this is a stubb
+
+      % build a cell array of usernames and passwords
+      users = { cred(:).username };
+      passw = { cred(:).password };
+
+      % default to the latest used username
+      [e, ids] = sort([ cred(:).expiry ]);
+      default = users{ids(1)};
+
+      username = choose('Username', users, default);
+
+      % pick the corresponding password
+      ids = find(strcmp(users, username));
+      if ~isempty(ids)
+        default = passw{ids(1)};
+      else
+        default = [];
+      end
+
+      password = ask('Password', '');
+
+      if cm.cachePassword == 2
+        cache    = ask('Store credentials', 'n');
+        if ~isempty(cache) && cache(1) == 'y'
+          cache = true;
+        else
+          cache = false;
+        end
+      else
+        cache = logical(cm.cachePassword);
+      end
+    end
+
+
+    function [hostname, database, username] = selectDatabase(cm)
+      % this is a stubb
+
+      for kk = 1:numel(cm.credentials)
+        fprintf('% 2d.  %s\n', char(cm.credentials{kk}));
+      end
+      fprintf('%d. NEW (default)\n', numel(cm.credentials)+1);
+      str = input('Select connection:  ', 's');
+      if isempty(str)
+        id = numel(cm.credentials)+1;
+      else
+        id = eval(str);
+      end
+      if id > numel(cm.credentials)
+        hostname = input('Hostname:  ', 's');
+        database = input('Database:  ', 's');
+        username = [];
+      else
+        hostname = cm.credentials{kk}.hostname;
+        database = cm.credentials{kk}.database;
+        username = cm.credentials{kk}.username;
+      end
+    end
+
+  end % private methods
+
+end % classdef
+
+
+% this should become utils.jmysql.connect
+function conn = connect(hostname, database, username, password)
+
+  % 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:jmysql:connect:AccessDenied', '### access denied').addCause(ex));
+      end
+    end
+    rethrow(ex);
+  end
+end
+
+
+function str = ask(msg, default)
+  str = input(sprintf('%s (default: %s):  ', msg, default), 's');
+  if isempty(str)
+    str = default;
+  end
+  if ~ischar(str)
+    str = char(str);
+  end
+end
+
+function str = choose(msg, choices, default)
+  options = sprintf('%s, ', choices{:});
+  options = options(1:end-2);
+  str = input(sprintf('%s (options: %s, default: %s):  ', msg, options, default), 's');
+  if isempty(str)
+    str = default;
+  end
+  if ~ischar(str)
+    str = char(str);
+  end
+end