Mercurial > hg > ltpda
comparison m-toolbox/classes/@LTPDADatabaseConnectionManager/LTPDADatabaseConnectionManager.m @ 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 | |
children | e3c5468b1bfe |
comparison
equal
deleted
inserted
replaced
1:2014ba5b353a | 2:18e956c96a1b |
---|---|
1 classdef LTPDADatabaseConnectionManager < handle | |
2 | |
3 properties(SetAccess=private) | |
4 | |
5 connections = {}; | |
6 credentials = {}; | |
7 | |
8 p_credentialsExpiry = 3600; | |
9 p_cachePassword = 2; | |
10 p_maxConnectionsNumber = 10; | |
11 | |
12 end % private properties | |
13 | |
14 properties(Dependent=true) | |
15 | |
16 credentialsExpiry; % seconds | |
17 cachePassword; % 0=no 1=yes 2=ask | |
18 maxConnectionsNumber; | |
19 | |
20 end % dependent properties | |
21 | |
22 methods(Static) | |
23 | |
24 function reset() | |
25 % RESET Resets the state of the connection manager. | |
26 % | |
27 % This static method removes the LTPDADatabaseConnectionManager | |
28 % instance data from the appdata storage. Causes the reset of the | |
29 % credentials cache and the removal of all the connections from | |
30 % the connection pool. | |
31 | |
32 rmappdata(0, LTPDADatabaseConnectionManager.appdataKey); | |
33 end | |
34 | |
35 | |
36 function key = appdataKey() | |
37 % APPDATAKEY Returns the key used to store instance data in appdata. | |
38 % | |
39 % This is defined as static method, and not has an instance constant | |
40 % property, to to be accessible by the reset static method. | |
41 | |
42 key = 'LTPDADatabaseConnectionManager'; | |
43 end | |
44 | |
45 end % static methods | |
46 | |
47 methods | |
48 | |
49 function cm = LTPDADatabaseConnectionManager() | |
50 % LTPDACONNECTIONMANAGER Manages credentials and database connections. | |
51 % | |
52 % This constructor returns an handler to a LTPDADatabaseConnectionManager | |
53 % class instance. Database connections can be obtained trough the | |
54 % obtained object with the connect() method. | |
55 % | |
56 % The purpose of this class it to keep track of open database connections | |
57 % and to cache database credentials. It must be used in all LTPDA toolbox | |
58 % functions that required to obtain database connections. Its behaviour can | |
59 % be configured via LTPDA toolbox user preferences. The object status is | |
60 % persisted trough the appdata matlab facility. | |
61 | |
62 % import credentials class | |
63 import utils.credentials | |
64 | |
65 % load state from appdata | |
66 acm = getappdata(0, cm.appdataKey()); | |
67 | |
68 if isempty(acm) | |
69 % store state in appdata | |
70 setappdata(0, cm.appdataKey(), cm); | |
71 | |
72 import utils.const.* | |
73 utils.helper.msg(msg.PROC1, 'new connection manager'); | |
74 else | |
75 cm = acm; | |
76 end | |
77 end | |
78 | |
79 function str = disp(cm) | |
80 disp(sprintf('%s()\n', class(cm))); | |
81 end | |
82 | |
83 | |
84 function val = get.credentialsExpiry(cm) | |
85 % obtain from user preferences | |
86 %p = getappdata(0, 'LTPDApreferences'); | |
87 val = cm.p_credentialsExpiry; | |
88 end | |
89 | |
90 | |
91 function val = get.cachePassword(cm) | |
92 % obtain from user preferences | |
93 %p = getappdata(0, 'LTPDApreferences'); | |
94 val = cm.p_cachePassword; | |
95 end | |
96 | |
97 | |
98 function val = get.maxConnectionsNumber(cm) | |
99 % obtain from user preferences | |
100 %p = getappdata(0, 'LTPDApreferences'); | |
101 val = cm.p_maxConnectionsNumber; | |
102 end | |
103 | |
104 | |
105 function n = count(cm) | |
106 % COUNT Returns the number of open connections in the connections pool. | |
107 % | |
108 % This method has the side effect of removing all closed connections from | |
109 % the connections pool, so that the underlying objects can be garbage | |
110 % collected. | |
111 | |
112 import utils.const.* | |
113 | |
114 % find closed connections in the pool | |
115 mask = false(numel(cm.connections), 1); | |
116 for kk = 1:numel(cm.connections) | |
117 if cm.connections{kk}.isClosed() | |
118 utils.helper.msg(msg.PROC1, 'connection id=%d closed', kk); | |
119 mask(kk) = true; | |
120 end | |
121 end | |
122 | |
123 % remove them | |
124 cm.connections(mask) = []; | |
125 | |
126 % count remainig ones | |
127 n = numel(cm.connections); | |
128 end | |
129 | |
130 function clear(cm) | |
131 % CLEAR Removes all cached credentials from the connection manager. | |
132 cm.credentials = {}; | |
133 end | |
134 | |
135 | |
136 function conn = connect(cm, varargin) | |
137 % CONNECT Uses provided credential to establish a database connection. | |
138 % | |
139 % CONNECT(hostname, database, username, password) Returns an object | |
140 % implementing the java.sql.Connection interface handing a connection to | |
141 % the specified database. Any of the parameter is optional. The user will | |
142 % be queried for the missing information. | |
143 % | |
144 % The returned connection are added to a connections pool. When the number | |
145 % of connections in the pool exceeds a configurable maximum, no more | |
146 % connection are instantiated. Closed connections are automatically | |
147 % removed from the pool. | |
148 % | |
149 % CONNECT(pl) Works as the above but the parameters are obtained from the | |
150 % plist object PL. If the 'connection' parameter in the plist contains an | |
151 % object implementing the java.sql.Connection interface, this object is | |
152 % returned instead that opening a new connection. In this case the | |
153 % connection in not added to the connection pool. | |
154 | |
155 import utils.const.* | |
156 | |
157 % save current credentials cache | |
158 cache = cm.credentials; | |
159 | |
160 % count open connections in the pool | |
161 count = cm.count(); | |
162 | |
163 % check parameters | |
164 if numel(varargin) == 1 && isa(varargin{1}, 'plist') | |
165 | |
166 % extract parameters from plist | |
167 pl = varargin{1}; | |
168 | |
169 % check if we have a connection parameter | |
170 conn = find(pl, 'conn'); | |
171 if ~isempty(conn) | |
172 % check that it implements java.sql.Connection interface | |
173 if ~isa(conn, 'java.sql.Connection') | |
174 error('### connection is not valid database connection'); | |
175 end | |
176 % return this connection | |
177 return; | |
178 end | |
179 | |
180 % otherwise | |
181 hostname = find(pl, 'hostname'); | |
182 database = find(pl, 'database'); | |
183 username = find(pl, 'username'); | |
184 password = find(pl, 'password'); | |
185 | |
186 % if there is no hostname ignore other parameters | |
187 if ~ischar(hostname) || isempty(hostname) | |
188 varargin = {}; | |
189 % if there is no database ignore other parameters | |
190 elseif ~ischar(database) || isempty(database) | |
191 varargin = {hostname}; | |
192 % if there is no username ignore other parameters | |
193 elseif ~ischar(username) || isempty(username) | |
194 varargin = {hostname, database}; | |
195 % password can not be null but can be an empty string | |
196 elseif ~ischar(password) | |
197 varargin = {hostname, database, username}; | |
198 else | |
199 varargin = {hostname, database, username, password}; | |
200 end | |
201 end | |
202 | |
203 % check number of connections | |
204 if count > cm.maxConnectionsNumber | |
205 error('### too many open connections'); | |
206 end | |
207 | |
208 % connect | |
209 try | |
210 conn = cm.getConnection(varargin{:}); | |
211 | |
212 % add connection to pool | |
213 utils.helper.msg(msg.PROC1, 'add connection to pool'); | |
214 cm.connections{end+1} = conn; | |
215 | |
216 catch ex | |
217 % restore our copy of the credentials cache | |
218 utils.helper.msg(msg.PROC1, 'undo cache changes'); | |
219 cm.credentials = cache; | |
220 | |
221 % hide implementation details | |
222 ex.throw(); | |
223 end | |
224 end | |
225 | |
226 | |
227 function close(cm, ids) | |
228 % CLOSE Forces connections to be closed. | |
229 % | |
230 % In the case bugs in other routines working with database connections | |
231 % produce orphan connections, this method can be used to force the close | |
232 % of those connections. | |
233 % | |
234 % CLOSE(ids) Closes the connections with the corresponding IDs in the | |
235 % connections pool. If no ID is given all connections in the pool are | |
236 % closed. | |
237 | |
238 if nargin < 2 | |
239 ids = 1:numel(cm.connections); | |
240 end | |
241 cellfun(@close, cm.connections(ids)); | |
242 | |
243 % remove closed connections from pool | |
244 cm.count(); | |
245 end | |
246 | |
247 | |
248 function add(cm, c) | |
249 % ADD Adds credentials to the credentials cache. | |
250 % | |
251 % This method can be used to initialize or add to the cache, credentials | |
252 % that will be used in subsequent connections attempts. This method accepts | |
253 % only credentials in the form of utils.credentials objects. | |
254 | |
255 % check input arguments | |
256 if nargin < 2 || ~isa(c, 'utils.credentials') | |
257 error('### invalid call'); | |
258 end | |
259 | |
260 % add to the cache | |
261 cm.cacheCredentials(c); | |
262 end | |
263 | |
264 end % methods | |
265 | |
266 methods(Access=private) | |
267 | |
268 function conn = getConnection(cm, varargin) | |
269 % GETCONNECTION Where the implementation of the connect method really is. | |
270 | |
271 import utils.const.* | |
272 | |
273 % handle variable number of arguments | |
274 switch numel(varargin) | |
275 case 0 | |
276 % find credentials | |
277 [hostname, database, username] = cm.selectDatabase([cm.credentials{:}]); | |
278 conn = cm.getConnection(hostname, database, username); | |
279 | |
280 case 1 | |
281 % find credentials | |
282 cred = cm.findCredentials(varargin{:}); | |
283 if numel(cred) == 0 | |
284 cred = utils.credentials(varargin{:}); | |
285 end | |
286 [hostname, database, username] = cm.selectDatabase(cred); | |
287 conn = cm.getConnection(hostname, database, username); | |
288 | |
289 case 2 | |
290 % find credentials | |
291 cred = cm.findCredentials(varargin{:}); | |
292 switch numel(cred) | |
293 case 0 | |
294 conn = cm.getConnection(varargin{1}, varargin{2}, []); | |
295 case 1 | |
296 conn = cm.getConnection(cred.hostname, cred.database, cred.username); | |
297 otherwise | |
298 [hostname, database, username] = cm.selectDatabase(cred); | |
299 conn = cm.getConnection(hostname, database, username); | |
300 end | |
301 | |
302 case 3 | |
303 % find credentials | |
304 cred = cm.findCredentials(varargin{1}, varargin{2}, varargin{3}); | |
305 switch numel(cred) | |
306 case 0 | |
307 % no credentials found | |
308 usernames = { varargin{3} }; | |
309 if isempty(varargin{3}) | |
310 % use usernames for same hostname | |
311 tmp = cm.findCredentials(varargin{1}); | |
312 if ~isempty(tmp) | |
313 usernames = { tmp(:).username }; | |
314 end | |
315 end | |
316 % build credentials objects | |
317 tmp = {}; | |
318 for kk = 1:numel(usernames) | |
319 tmp{kk} = utils.credentials(varargin{1}, varargin{2}, usernames{kk}); | |
320 end | |
321 % convert from cell array to array | |
322 cred = [tmp{:}]; | |
323 case 1 | |
324 % credentials in cache | |
325 utils.helper.msg(msg.PROC1, 'use cached credentials'); | |
326 otherwise | |
327 % we should not have more than one credentials set | |
328 error('### more than one credentials set matching'); | |
329 end | |
330 | |
331 cache = false; | |
332 if (numel(cred) > 1) || ~cred.complete | |
333 % ask for password | |
334 [username, password, cache] = cm.inputCredentials(cred); | |
335 | |
336 % cache credentials | |
337 cred = utils.credentials(varargin{1}, varargin{2}, username); | |
338 cm.cacheCredentials(cred); | |
339 | |
340 % add password to credentials | |
341 cred.password = password; | |
342 end | |
343 | |
344 % try to connect | |
345 try | |
346 conn = utils.mysql.connect(cred.hostname, cred.database, cred.username, cred.password); | |
347 catch ex | |
348 % look for access denied errors | |
349 if strcmp(ex.identifier, 'utils:mysql:connect:AccessDenied') | |
350 % ask for new new credentials | |
351 utils.helper.msg(msg.IMPORTANT, ex.message); | |
352 conn = cm.getConnection(varargin{1}, varargin{2}, varargin{3}); | |
353 else | |
354 % error out | |
355 throw(ex); | |
356 end | |
357 end | |
358 | |
359 % cache password | |
360 if cache | |
361 utils.helper.msg(msg.PROC1, 'cache password'); | |
362 cm.cacheCredentials(cred); | |
363 end | |
364 | |
365 case 4 | |
366 % connect | |
367 conn = utils.mysql.connect(varargin{1}, varargin{2}, varargin{3}, varargin{4}); | |
368 | |
369 if cm.cachePassword == 1 | |
370 % cache credentials with password | |
371 cred = utils.credentials(varargin{1}, varargin{2}, varargin{3}, varargin{4}); | |
372 else | |
373 % cache credentials without password | |
374 cred = utils.credentials(varargin{1}, varargin{2}, varargin{3}); | |
375 end | |
376 cm.cacheCredentials(cred); | |
377 | |
378 otherwise | |
379 error('### invalid call') | |
380 end | |
381 | |
382 end | |
383 | |
384 | |
385 function ids = findCredentialsId(cm, varargin) | |
386 % FINDCREDENTIALSID Find credentials in the cache and returns their IDs. | |
387 | |
388 import utils.const.* | |
389 ids = []; | |
390 | |
391 for kk = 1:numel(cm.credentials) | |
392 % invalidate expired passwords | |
393 if expired(cm.credentials{kk}) | |
394 utils.helper.msg(msg.PROC1, 'cache entry id=%d expired', kk); | |
395 cm.credentials{kk}.password = []; | |
396 cm.credentials{kk}.expiry = 0; | |
397 end | |
398 | |
399 % match input with cache | |
400 if match(cm.credentials{kk}, varargin{:}) | |
401 ids = [ ids kk ]; | |
402 end | |
403 end | |
404 end | |
405 | |
406 | |
407 function cred = findCredentials(cm, varargin) | |
408 % FINDCREDENTIALS Find credentials in the cache and returns them in a list. | |
409 | |
410 % default | |
411 cred = []; | |
412 | |
413 % search | |
414 ids = findCredentialsId(cm, varargin{:}); | |
415 | |
416 % return a credentials objects array | |
417 if ~isempty(ids) | |
418 cred = [cm.credentials{ids}]; | |
419 end | |
420 end | |
421 | |
422 | |
423 function cacheCredentials(cm, c) | |
424 % CACHECREDENTIALS Adds to or updates the credentials cache. | |
425 | |
426 import utils.const.* | |
427 | |
428 % find entry to update | |
429 ids = findCredentialsId(cm, c.hostname, c.database, c.username); | |
430 | |
431 % set password expiry time | |
432 if ischar(c.password) | |
433 c.expiry = double(time()) + cm.credentialsExpiry; | |
434 end | |
435 | |
436 if isempty(ids) | |
437 % add at the end | |
438 utils.helper.msg(msg.PROC1, 'add cache entry %s', char(c)); | |
439 cm.credentials{end+1} = c; | |
440 else | |
441 for id = ids | |
442 % update only if the cached informations are less than the one we have | |
443 if length(c) > length(cm.credentials{id}) | |
444 utils.helper.msg(msg.PROC1, 'update cache entry id=%d %s', id, char(c)); | |
445 cm.credentials{id} = c; | |
446 else | |
447 % always update expiry time | |
448 cm.credentials{id}.expiry = c.expiry; | |
449 end | |
450 end | |
451 end | |
452 end | |
453 | |
454 | |
455 function [username, password, cache] = inputCredentials(cm, cred) | |
456 % INPUTCREDENTIALS Queries the user for database username and password. | |
457 | |
458 % build a cell array of usernames | |
459 users = {}; | |
460 for id = 1:numel(cred) | |
461 if ~isempty(cred(id).username) | |
462 users = [ users { cred(id).username } ]; | |
463 end | |
464 end | |
465 users = sort(unique(users)); | |
466 | |
467 parent = com.mathworks.mde.desk.MLDesktop.getInstance().getMainFrame(); | |
468 dialog = javaObjectEDT('connectionmanager.CredentialsDialog', ... | |
469 parent, cred(1).hostname, cred(1).database, users, cm.cachePassword); | |
470 dialog.show(); | |
471 if dialog.cancelled | |
472 throw(MException('utils:mysql:connect:UserCancelled', '### user cancelled')); | |
473 end | |
474 username = char(dialog.username); | |
475 password = char(dialog.password); | |
476 cache = logical(dialog.cache); | |
477 end | |
478 | |
479 | |
480 function [hostname, database, username] = selectDatabase(cm, credentials) | |
481 % SELECTDATABASE Makes the user choose to which database connect to. | |
482 | |
483 parent = com.mathworks.mde.desk.MLDesktop.getInstance().getMainFrame(); | |
484 dialog = javaObjectEDT('connectionmanager.DatabaseSelectorDialog', parent); | |
485 for c = credentials | |
486 dialog.add(c.hostname, c.database, c.username); | |
487 end | |
488 dialog.show(); | |
489 if dialog.cancelled | |
490 throw(MException('utils:mysql:connect:UserCancelled', '### user cancelled')); | |
491 end | |
492 hostname = char(dialog.hostname); | |
493 database = char(dialog.database); | |
494 username = char(dialog.username); | |
495 if isempty(username) | |
496 username = []; | |
497 end | |
498 end | |
499 | |
500 end % private methods | |
501 | |
502 end % classdef |