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("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     /** {@inheritDoc} */
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     /** Clears the result cache. */
296     public void clearCache() {
297         if (initialized && cacheResults) {
298             resultsCache.clear();
299         }
300     }
301 
302     /** Registers the query template with template engine. */
303     protected void registerTemplate() {
304         queryTemplateName = "shibboleth.resolver.dc." + getId();
305         queryCreator.registerTemplate(queryTemplateName, queryTemplate);
306     }
307 
308     /**
309      * Attempts to retrieve the attributes from the cache.
310      * 
311      * @param princpal the principal name of the user the attributes are for
312      * @param query query used to generate the attributes
313      * 
314      * @return cached attributes
315      * 
316      * @throws AttributeResolutionException thrown if there is a problem retrieving data from the cache
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      * Attempts to retrieve the attribute from the database.
338      * 
339      * @param query query used to get the attributes
340      * 
341      * @return attributes gotten from the database
342      * 
343      * @throws AttributeResolutionException thrown if there is a problem retrieving data from the database or
344      *             transforming that data into {@link BaseAttribute}s
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      * Converts a SQL query results set into a set of {@link BaseAttribute}s.
389      * 
390      * @param resultSet the result set to convert
391      * 
392      * @return the resultant set of attributes
393      * 
394      * @throws AttributeResolutionException thrown if there is a problem converting the result set into attributes
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      * Adds a value extracted from the result set as a specific type into the value set.
445      * 
446      * @param values set to add values into
447      * @param type type the value should be extracted as
448      * @param resultSet result set, on the current row, to extract the value from
449      * @param columnIndex index of the column from which to extract the attribute
450      * 
451      * @throws SQLException thrown if value can not retrieved from the result set
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 }