1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.dataConnector;
18
19 import java.lang.ref.SoftReference;
20 import java.sql.Connection;
21 import java.sql.DatabaseMetaData;
22 import java.sql.ResultSet;
23 import java.sql.ResultSetMetaData;
24 import java.sql.SQLException;
25 import java.util.Collection;
26 import java.util.HashMap;
27 import java.util.Map;
28
29 import javax.sql.DataSource;
30
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33 import org.springframework.context.ApplicationEvent;
34 import org.springframework.context.ApplicationListener;
35
36 import edu.internet2.middleware.shibboleth.common.attribute.BaseAttribute;
37 import edu.internet2.middleware.shibboleth.common.attribute.provider.BasicAttribute;
38 import edu.internet2.middleware.shibboleth.common.attribute.resolver.AttributeResolutionException;
39 import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.ShibbolethResolutionContext;
40 import edu.internet2.middleware.shibboleth.common.session.LogoutEvent;
41
42
43
44
45 public class RDBMSDataConnector extends BaseDataConnector implements ApplicationListener {
46
47
48 public static enum DATA_TYPES {
49 BigDecimal, Boolean, Byte, ByteArray, Date, Double, Float, Integer, Long, Object, Short, String, Time, Timestamp, URL
50 };
51
52
53 private final Logger log = LoggerFactory.getLogger(RDBMSDataConnector.class);
54
55
56 private boolean initialized;
57
58
59 private DataSource dataSource;
60
61
62 private boolean readOnlyConnection;
63
64
65 private boolean usesStoredProcedure;
66
67
68 private boolean noResultIsError;
69
70
71 private boolean cacheResults;
72
73
74 private String queryTemplateName;
75
76
77 private String queryTemplate;
78
79
80 private Map<String, RDBMSColumnDescriptor> columnDescriptors;
81
82
83 private Map<String, Map<String, SoftReference<Map<String, BaseAttribute>>>> resultsCache;
84
85
86 private TemplateEngine queryCreator;
87
88
89
90
91
92
93
94 public RDBMSDataConnector(DataSource source, boolean resultCaching) {
95 super();
96 initialized = false;
97 dataSource = source;
98 cacheResults = resultCaching;
99 usesStoredProcedure = false;
100 columnDescriptors = new HashMap<String, RDBMSColumnDescriptor>();
101 }
102
103
104 public void initialize() {
105 registerTemplate();
106 if (cacheResults) {
107 resultsCache = new HashMap<String, Map<String, SoftReference<Map<String, BaseAttribute>>>>();
108 }
109 initialized = true;
110 }
111
112
113
114
115
116
117 public boolean isConnectionReadOnly() {
118 return readOnlyConnection;
119 }
120
121
122
123
124
125
126 public void setConnectionReadOnly(boolean isReadOnly) {
127 readOnlyConnection = isReadOnly;
128 }
129
130
131
132
133
134
135 public boolean getUsesStoredProcedure() {
136 return usesStoredProcedure;
137 }
138
139
140
141
142
143
144 public void setUsesStoredProcedure(boolean storedProcedure) {
145 usesStoredProcedure = storedProcedure;
146 }
147
148
149
150
151
152
153 public boolean isNoResultIsError() {
154 return noResultIsError;
155 }
156
157
158
159
160
161
162 public void setNoResultIsError(boolean b) {
163 noResultIsError = b;
164 }
165
166
167
168
169
170
171 public boolean getCacheResults() {
172 return cacheResults;
173 }
174
175
176
177
178
179
180 public TemplateEngine getTemplateEngine() {
181 return queryCreator;
182 }
183
184
185
186
187
188
189 public void setTemplateEngine(TemplateEngine engine) {
190 queryCreator = engine;
191 registerTemplate();
192 }
193
194
195
196
197
198
199 public String getQueryTemplate() {
200 return queryTemplate;
201 }
202
203
204
205
206
207
208 public void setQueryTemplate(String template) {
209 queryTemplate = template;
210 }
211
212
213
214
215
216
217
218 public Map<String, RDBMSColumnDescriptor> getColumnDescriptor() {
219 return columnDescriptors;
220 }
221
222
223 public void onApplicationEvent(ApplicationEvent event) {
224 if (event instanceof LogoutEvent) {
225 LogoutEvent logoutEvent = (LogoutEvent) event;
226 if (cacheResults) {
227 resultsCache.remove(logoutEvent.getUserSession().getPrincipalName());
228 }
229 }
230 }
231
232
233 public Map<String, BaseAttribute> resolve(ShibbolethResolutionContext resolutionContext)
234 throws AttributeResolutionException {
235 String query = queryCreator.createStatement(queryTemplateName, resolutionContext, getDependencyIds(), null);
236 log.debug("Search Query: {}", query);
237
238 Map<String, BaseAttribute> resolvedAttributes = null;
239 resolvedAttributes = retrieveAttributesFromCache(resolutionContext.getAttributeRequestContext()
240 .getPrincipalName(), query);
241
242 if (resolvedAttributes == null) {
243 resolvedAttributes = retrieveAttributesFromDatabase(query);
244 }
245
246 if (cacheResults) {
247 Map<String, SoftReference<Map<String, BaseAttribute>>> individualCache = resultsCache.get(resolutionContext
248 .getAttributeRequestContext().getPrincipalName());
249 if (individualCache == null) {
250 individualCache = new HashMap<String, SoftReference<Map<String, BaseAttribute>>>();
251 resultsCache.put(resolutionContext.getAttributeRequestContext().getPrincipalName(), individualCache);
252 }
253 individualCache.put(query, new SoftReference<Map<String, BaseAttribute>>(resolvedAttributes));
254 }
255
256 return resolvedAttributes;
257 }
258
259
260 public void validate() throws AttributeResolutionException {
261 log.debug("Validating RDBMS data connector {} configuration.", getId());
262 Connection connection = null;
263 try {
264 connection = dataSource.getConnection();
265 if (connection == null) {
266 log.error("Unable to create connections for RDBMS data connector " + getId());
267 throw new AttributeResolutionException("Unable to create connections for RDBMS data connector "
268 + getId());
269 }
270
271 DatabaseMetaData dbmd = connection.getMetaData();
272 if (!dbmd.supportsStoredProcedures() && usesStoredProcedure) {
273 log.error("RDBMS data connector " + getId()
274 + " is configured to use stored procedures but database does not support them.");
275 throw new AttributeResolutionException("RDBMS data connector " + getId()
276 + " is configured to use stored procedures but database does not support them.");
277 }
278
279 log.debug("Validating RDBMS data connector {} configuration is valid.", getId());
280 } catch (SQLException e) {
281 log.error("Unable to validate RDBMS data connector " + getId() + " configuration", e);
282 throw new AttributeResolutionException("Unable to validate RDBMS data connector " + getId()
283 + " configuration", e);
284 } finally {
285 try {
286 if (connection != null && !connection.isClosed()) {
287 connection.close();
288 }
289 } catch (SQLException e) {
290 log.error("Error closing database connection", e);
291 }
292 }
293 }
294
295
296 public void clearCache() {
297 if (initialized && cacheResults) {
298 resultsCache.clear();
299 }
300 }
301
302
303 protected void registerTemplate() {
304 queryTemplateName = "shibboleth.resolver.dc." + getId();
305 queryCreator.registerTemplate(queryTemplateName, queryTemplate);
306 }
307
308
309
310
311
312
313
314
315
316
317
318 protected Map<String, BaseAttribute> retrieveAttributesFromCache(String princpal, String query)
319 throws AttributeResolutionException {
320 if (!cacheResults) {
321 return null;
322 }
323
324 Map<String, SoftReference<Map<String, BaseAttribute>>> queryCache = resultsCache.get(princpal);
325 if (queryCache != null) {
326 SoftReference<Map<String, BaseAttribute>> cachedAttributes = queryCache.get(query);
327 if (cachedAttributes != null) {
328 log.debug("RDBMS Data Connector {}: Fetched attributes from cache for principal {}", getId(), princpal);
329 return cachedAttributes.get();
330 }
331 }
332
333 return null;
334 }
335
336
337
338
339
340
341
342
343
344
345
346 protected Map<String, BaseAttribute> retrieveAttributesFromDatabase(String query)
347 throws AttributeResolutionException {
348 Map<String, BaseAttribute> resolvedAttributes;
349 Connection connection = null;
350 ResultSet queryResult = null;
351
352 try {
353 connection = dataSource.getConnection();
354 if (readOnlyConnection) {
355 connection.setReadOnly(true);
356 }
357 log.debug("RDBMS Data Connector {}: Querying database for attributes with query: {}", getId(), query);
358 queryResult = connection.createStatement().executeQuery(query);
359 resolvedAttributes = processResultSet(queryResult);
360 if (resolvedAttributes.isEmpty() && noResultIsError) {
361 log.error("RDBMS Data Connector {}: No attribtues from query", getId());
362 throw new AttributeResolutionException("RDBMS Data Connector " + getId()
363 + ": No attributes returned from query");
364 }
365 log.debug("RDBMS Data Connector {}: Retrieved attributes: {}", getId(), resolvedAttributes.keySet());
366 return resolvedAttributes;
367 } catch (SQLException e) {
368 log.error("RDBMS Data Connector " + getId() + ": Unable to execute SQL query\n" + query, e);
369 throw new AttributeResolutionException("RDBMS Data Connector " + getId() + ": Unable to execute SQL query",
370 e);
371 } finally {
372 try {
373 if (queryResult != null) {
374 queryResult.close();
375 }
376
377 if (connection != null && !connection.isClosed()) {
378 connection.close();
379 }
380
381 } catch (SQLException e) {
382 log.error("RDBMS Data Connector " + getId() + ": Unable to close connection to database", e);
383 }
384 }
385 }
386
387
388
389
390
391
392
393
394
395
396 protected Map<String, BaseAttribute> processResultSet(ResultSet resultSet) throws AttributeResolutionException {
397 Map<String, BaseAttribute> attributes = new HashMap<String, BaseAttribute>();
398
399 try {
400 if (!resultSet.next()) {
401 return attributes;
402 }
403
404 ResultSetMetaData resultMD = resultSet.getMetaData();
405 int numOfCols = resultMD.getColumnCount();
406 String columnName;
407 RDBMSColumnDescriptor columnDescriptor;
408 BaseAttribute attribute;
409 Collection attributeValues;
410 do {
411 for (int i = 1; i <= numOfCols; i++) {
412 columnName = resultMD.getColumnName(i);
413 columnDescriptor = columnDescriptors.get(columnName);
414
415 if (columnDescriptor == null || columnDescriptor.getAttributeID() == null) {
416 attribute = attributes.get(columnName);
417 if (attribute == null) {
418 attribute = new BasicAttribute(columnName);
419 }
420 } else {
421 attribute = attributes.get(columnDescriptor.getAttributeID());
422 if (attribute == null) {
423 attribute = new BasicAttribute(columnDescriptor.getAttributeID());
424 }
425 }
426
427 attributes.put(attribute.getId(), attribute);
428 attributeValues = attribute.getValues();
429 if (columnDescriptor == null || columnDescriptor.getDataType() == null) {
430 attributeValues.add(resultSet.getObject(i));
431 } else {
432 addValueByType(attributeValues, columnDescriptor.getDataType(), resultSet, i);
433 }
434 }
435 } while (resultSet.next());
436 } catch (SQLException e) {
437 log.error("RDBMS Data Connector " + getId() + ": Unable to read data from query result set", e);
438 }
439
440 return attributes;
441 }
442
443
444
445
446
447
448
449
450
451
452
453 protected void addValueByType(Collection values, DATA_TYPES type, ResultSet resultSet, int columnIndex)
454 throws SQLException {
455 switch (type) {
456 case BigDecimal:
457 values.add(resultSet.getBigDecimal(columnIndex));
458 break;
459 case Boolean:
460 values.add(resultSet.getBoolean(columnIndex));
461 break;
462 case Byte:
463 values.add(resultSet.getByte(columnIndex));
464 break;
465 case ByteArray:
466 values.add(resultSet.getBytes(columnIndex));
467 break;
468 case Date:
469 values.add(resultSet.getDate(columnIndex));
470 break;
471 case Double:
472 values.add(resultSet.getDouble(columnIndex));
473 break;
474 case Float:
475 values.add(resultSet.getFloat(columnIndex));
476 break;
477 case Integer:
478 values.add(resultSet.getInt(columnIndex));
479 break;
480 case Long:
481 values.add(resultSet.getLong(columnIndex));
482 break;
483 case Object:
484 values.add(resultSet.getObject(columnIndex));
485 break;
486 case Short:
487 values.add(resultSet.getShort(columnIndex));
488 break;
489 case Time:
490 values.add(resultSet.getTime(columnIndex));
491 break;
492 case Timestamp:
493 values.add(resultSet.getTimestamp(columnIndex));
494 break;
495 case URL:
496 values.add(resultSet.getURL(columnIndex));
497 break;
498 default:
499 values.add(resultSet.getString(columnIndex));
500 }
501 }
502 }