View Javadoc

1   /*
2    * Copyright [2007] [University Corporation for Advanced Internet Development, Inc.]
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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   * A data connector that can retrieve information from a relational database through JDBC, version 3.
44   */
45  public class RDBMSDataConnector extends BaseDataConnector implements ApplicationListener {
46  
47      /** Data types understood by this connector. */
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      /** Class logger. */
53      private final Logger log = LoggerFactory.getLogger(RDBMSDataConnector.class);
54  
55      /** Indicates whether this connector has been initialized. */
56      private boolean initialized;
57  
58      /** JDBC data source for retrieving connections. */
59      private DataSource dataSource;
60  
61      /** Whether the JDBC connection is read-only. */
62      private boolean readOnlyConnection;
63  
64      /** Whether queries might use stored procedures. */
65      private boolean usesStoredProcedure;
66  
67      /** Whether an empty result set is an error. */
68      private boolean noResultIsError;
69  
70      /** Whether to cache query results. */
71      private boolean cacheResults;
72  
73      /** Name the query template is registered under with the statement creator. */
74      private String queryTemplateName;
75  
76      /** Template that produces the query to use. */
77      private String queryTemplate;
78  
79      /** Set of column descriptors for managing returned data. [columnName => colmentDescriptr] */
80      private Map<String, RDBMSColumnDescriptor> columnDescriptors;
81  
82      /** Query result cache. [principalName => [query => attributeResultSetReference]] */
83      private Map<String, Map<String, SoftReference<Map<String, BaseAttribute>>>> resultsCache;
84  
85      /** Template engine used to change query template into actual query. */
86      private TemplateEngine queryCreator;
87  
88      /**
89       * Constructor.
90       * 
91       * @param source data source used to retrieve connections
92       * @param resultCaching whether query results should be cached
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     /** Initializes the connector and prepares it for use. */
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      * Gets whether this data connector uses read-only connections.
114      * 
115      * @return whether this data connector uses read-only connections
116      */
117     public boolean isConnectionReadOnly() {
118         return readOnlyConnection;
119     }
120 
121     /**
122      * Sets whether this data connector uses read-only connections.
123      * 
124      * @param isReadOnly whether this data connector uses read-only connections
125      */
126     public void setConnectionReadOnly(boolean isReadOnly) {
127         readOnlyConnection = isReadOnly;
128     }
129 
130     /**
131      * Gets whether queries made use stored procedures.
132      * 
133      * @return whether queries made use stored procedures
134      */
135     public boolean getUsesStoredProcedure() {
136         return usesStoredProcedure;
137     }
138 
139     /**
140      * Sets whether queries made use stored procedures.
141      * 
142      * @param storedProcedure whether queries made use stored procedures
143      */
144     public void setUsesStoredProcedure(boolean storedProcedure) {
145         usesStoredProcedure = storedProcedure;
146     }
147 
148     /**
149      * This returns whether this connector will throw an exception if no search results are found. The default is false.
150      * 
151      * @return <code>boolean</code>
152      */
153     public boolean isNoResultIsError() {
154         return noResultIsError;
155     }
156 
157     /**
158      * This sets whether this connector will throw an exception if no search results are found.
159      * 
160      * @param b <code>boolean</code>
161      */
162     public void setNoResultIsError(boolean b) {
163         noResultIsError = b;
164     }
165 
166     /**
167      * Gets whether to cache query results.
168      * 
169      * @return whether to cache query results
170      */
171     public boolean getCacheResults() {
172         return cacheResults;
173     }
174 
175     /**
176      * Gets the engine used to evaluate the query template.
177      * 
178      * @return engine used to evaluate the query template
179      */
180     public TemplateEngine getTemplateEngine() {
181         return queryCreator;
182     }
183 
184     /**
185      * Sets the engine used to evaluate the query template.
186      * 
187      * @param engine engine used to evaluate the query template
188      */
189     public void setTemplateEngine(TemplateEngine engine) {
190         queryCreator = engine;
191         registerTemplate();
192     }
193 
194     /**
195      * Gets the template used to create queries.
196      * 
197      * @return template used to create queries
198      */
199     public String getQueryTemplate() {
200         return queryTemplate;
201     }
202 
203     /**
204      * Sets the template used to create queries.
205      * 
206      * @param template template used to create queries
207      */
208     public void setQueryTemplate(String template) {
209         queryTemplate = template;
210     }
211 
212     /**
213      * Gets the set of column descriptors used to deal with result set data. The name of the database column is the
214      * map's key. This list is unmodifiable.
215      * 
216      * @return column descriptors used to deal with result set data
217      */
218     public Map<String, RDBMSColumnDescriptor> getColumnDescriptor() {
219         return columnDescriptors;
220     }
221 
222     /** {@inheritDoc} */
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     /** {@inheritDoc} */
233     public Map<String, BaseAttribute> resolve(ShibbolethResolutionContext resolutionContext)
234             throws AttributeResolutionException {
235         String query = queryCreator.createStatement(queryTemplateName, resolutionContext, getDependencyIds(), null);
236         log.debug("RDBMS data connector {} - Search Query: {}", getId(), 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     /** {@inheritDoc} */
260     public void validate() throws AttributeResolutionException {
261         log.debug("RDBMS data connector {} - Validating configuration.", getId());
262         
263         if(dataSource == null){
264             log.error("RDBMS data connector {} - Datasource is null", getId());
265             throw new AttributeResolutionException("Datasource is null");
266         }
267         
268         Connection connection = null;
269         try {
270             connection = dataSource.getConnection();
271             if (connection == null) {
272                 log.error("RDBMS data connector {} - Unable to create connections", getId());
273                 throw new AttributeResolutionException("Unable to create connections for RDBMS data connector "
274                         + getId());
275             }
276 
277             DatabaseMetaData dbmd = connection.getMetaData();
278             if (!dbmd.supportsStoredProcedures() && usesStoredProcedure) {
279                 log.error("RDBMS data connector {} - Database does not support stored procedures.", getId());
280                 throw new AttributeResolutionException("Database does not support stored procedures.");
281             }
282 
283             log.debug("RDBMS data connector {} - Connector configuration is valid.", getId());
284         } catch (SQLException e) {
285             if (e.getSQLState() != null) {
286                 log.error("RDBMS data connector {} - Invalid connector configuration; SQL state: {}, SQL Code: {}",
287                         new Object[] { getId(), e.getSQLState(), e.getErrorCode() }, e);
288             } else {
289                 log.error("RDBMS data connector {} - Invalid connector configuration", new Object[] { getId() }, e);
290             }
291             throw new AttributeResolutionException("Invalid connector configuration", e);
292         } finally {
293             try {
294                 if (connection != null && !connection.isClosed()) {
295                     connection.close();
296                 }
297             } catch (SQLException e) {
298                 if (e.getSQLState() != null) {
299                     log.error(
300                             "RDBMS data connector {} - Error closing database connection; SQL State: {}, SQL Code: {}",
301                             new Object[] { getId(), e.getSQLState(), e.getErrorCode() }, e);
302                 } else {
303                     log.error("RDBMS data connector {} - Error closing database connection", new Object[] { getId() },
304                             e);
305                 }
306             }
307         }
308     }
309 
310     /** Clears the result cache. */
311     public void clearCache() {
312         if (initialized && cacheResults) {
313             resultsCache.clear();
314         }
315     }
316 
317     /** Registers the query template with template engine. */
318     protected void registerTemplate() {
319         queryTemplateName = "shibboleth.resolver.dc." + getId();
320         queryCreator.registerTemplate(queryTemplateName, queryTemplate);
321     }
322 
323     /**
324      * Attempts to retrieve the attributes from the cache.
325      * 
326      * @param princpal the principal name of the user the attributes are for
327      * @param query query used to generate the attributes
328      * 
329      * @return cached attributes
330      * 
331      * @throws AttributeResolutionException thrown if there is a problem retrieving data from the cache
332      */
333     protected Map<String, BaseAttribute> retrieveAttributesFromCache(String princpal, String query)
334             throws AttributeResolutionException {
335         if (!cacheResults) {
336             return null;
337         }
338 
339         Map<String, SoftReference<Map<String, BaseAttribute>>> queryCache = resultsCache.get(princpal);
340         if (queryCache != null) {
341             SoftReference<Map<String, BaseAttribute>> cachedAttributes = queryCache.get(query);
342             if (cachedAttributes != null) {
343                 log.debug("RDBMS data connector {} - Fetched attributes from cache for principal {}", getId(),
344                                 princpal);
345                 return cachedAttributes.get();
346             }
347         }
348 
349         return null;
350     }
351 
352     /**
353      * Attempts to retrieve the attribute from the database.
354      * 
355      * @param query query used to get the attributes
356      * 
357      * @return attributes gotten from the database
358      * 
359      * @throws AttributeResolutionException thrown if there is a problem retrieving data from the database or
360      *             transforming that data into {@link BaseAttribute}s
361      */
362     protected Map<String, BaseAttribute> retrieveAttributesFromDatabase(String query)
363             throws AttributeResolutionException {
364         Map<String, BaseAttribute> resolvedAttributes;
365         Connection connection = null;
366         ResultSet queryResult = null;
367 
368         try {
369             connection = dataSource.getConnection();
370             if (readOnlyConnection) {
371                 connection.setReadOnly(true);
372             }
373             log.debug("RDBMS data connector {} - Querying database for attributes with query {}", getId(), query);
374             queryResult = connection.createStatement().executeQuery(query);
375             resolvedAttributes = processResultSet(queryResult);
376             if (resolvedAttributes.isEmpty() && noResultIsError) {
377                 log.error("RDBMS data connector {} - No attribtues from query", getId());
378                 throw new AttributeResolutionException("No attributes returned from query");
379             }
380             log.debug("RDBMS data connector {} - Retrieved attributes: {}", getId(), resolvedAttributes.keySet());
381             return resolvedAttributes;
382         } catch (SQLException e) {
383             if (e.getSQLState() != null) {
384                 log.error("RDBMS data connector {} - Unable to execute SQL query {}; SQL State: {}, SQL Code: {}",
385                         new Object[] { getId(), query, e.getSQLState(), e.getErrorCode(), }, e);
386             } else {
387                 log.error("RDBMS data connector {} - Unable to execute SQL query {}", new Object[] { getId(), query },
388                         e);
389             }
390             throw new AttributeResolutionException("Unable to execute SQL query", e);
391         } finally {
392             try {
393                 if (queryResult != null) {
394                     queryResult.close();
395                 }
396 
397                 if (connection != null && !connection.isClosed()) {
398                     connection.close();
399                 }
400 
401             } catch (SQLException e) {
402                 if (e.getSQLState() != null) {
403                     log.error(
404                             "RDBMS data connector {} - Unable to close connection to database; SQL State: {}, SQL Code: {}",
405                             new Object[] { getId(), e.getSQLState(), e.getErrorCode() }, e);
406                 } else {
407                     log.error("RDBMS data connector {} - Unable to close connection to database",
408                             new Object[] { getId() }, e);
409                 }
410             }
411         }
412     }
413 
414     /**
415      * Converts a SQL query results set into a set of {@link BaseAttribute}s.
416      * 
417      * @param resultSet the result set to convert
418      * 
419      * @return the resultant set of attributes
420      * 
421      * @throws AttributeResolutionException thrown if there is a problem converting the result set into attributes
422      */
423     protected Map<String, BaseAttribute> processResultSet(ResultSet resultSet) throws AttributeResolutionException {
424         Map<String, BaseAttribute> attributes = new HashMap<String, BaseAttribute>();
425 
426         try {
427             if (!resultSet.next()) {
428                 return attributes;
429             }
430 
431             ResultSetMetaData resultMD = resultSet.getMetaData();
432             int numOfCols = resultMD.getColumnCount();
433             String columnName;
434             RDBMSColumnDescriptor columnDescriptor;
435             BaseAttribute attribute;
436             Collection attributeValues;
437             do {
438                 for (int i = 1; i <= numOfCols; i++) {
439                     columnName = resultMD.getColumnName(i);
440                     columnDescriptor = columnDescriptors.get(columnName);
441 
442                     if (columnDescriptor == null || columnDescriptor.getAttributeID() == null) {
443                         attribute = attributes.get(columnName);
444                         if (attribute == null) {
445                             attribute = new BasicAttribute(columnName);
446                         }
447                     } else {
448                         attribute = attributes.get(columnDescriptor.getAttributeID());
449                         if (attribute == null) {
450                             attribute = new BasicAttribute(columnDescriptor.getAttributeID());
451                         }
452                     }
453 
454                     attributes.put(attribute.getId(), attribute);
455                     attributeValues = attribute.getValues();
456                     if (columnDescriptor == null || columnDescriptor.getDataType() == null) {
457                         attributeValues.add(resultSet.getObject(i));
458                     } else {
459                         addValueByType(attributeValues, columnDescriptor.getDataType(), resultSet, i);
460                     }
461                 }
462             } while (resultSet.next());
463         } catch (SQLException e) {
464             if (e.getSQLState() != null) {
465                 log.error(
466                         "RDBMS data connector {} - Unable to read data from query result; SQL State: {}, SQL Code: {}",
467                         new Object[] { getId(), e.getSQLState(), e.getErrorCode() }, e);
468             } else {
469                 log.error("RDBMS data connector {} - Unable to read data from query result set",
470                         new Object[] { getId() }, e);
471             }
472         }
473 
474         return attributes;
475     }
476 
477     /**
478      * Adds a value extracted from the result set as a specific type into the value set.
479      * 
480      * @param values set to add values into
481      * @param type type the value should be extracted as
482      * @param resultSet result set, on the current row, to extract the value from
483      * @param columnIndex index of the column from which to extract the attribute
484      * 
485      * @throws SQLException thrown if value can not retrieved from the result set
486      */
487     protected void addValueByType(Collection values, DATA_TYPES type, ResultSet resultSet, int columnIndex)
488             throws SQLException {
489         switch (type) {
490             case BigDecimal:
491                 values.add(resultSet.getBigDecimal(columnIndex));
492                 break;
493             case Boolean:
494                 values.add(resultSet.getBoolean(columnIndex));
495                 break;
496             case Byte:
497                 values.add(resultSet.getByte(columnIndex));
498                 break;
499             case ByteArray:
500                 values.add(resultSet.getBytes(columnIndex));
501                 break;
502             case Date:
503                 values.add(resultSet.getDate(columnIndex));
504                 break;
505             case Double:
506                 values.add(resultSet.getDouble(columnIndex));
507                 break;
508             case Float:
509                 values.add(resultSet.getFloat(columnIndex));
510                 break;
511             case Integer:
512                 values.add(resultSet.getInt(columnIndex));
513                 break;
514             case Long:
515                 values.add(resultSet.getLong(columnIndex));
516                 break;
517             case Object:
518                 values.add(resultSet.getObject(columnIndex));
519                 break;
520             case Short:
521                 values.add(resultSet.getShort(columnIndex));
522                 break;
523             case Time:
524                 values.add(resultSet.getTime(columnIndex));
525                 break;
526             case Timestamp:
527                 values.add(resultSet.getTimestamp(columnIndex));
528                 break;
529             case URL:
530                 values.add(resultSet.getURL(columnIndex));
531                 break;
532             default:
533                 values.add(resultSet.getString(columnIndex));
534         }
535     }
536 }