Revamped property/URL config, added parameter subst in queries.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / aa / attrresolv / provider / JDBCDataConnector.java
1 /*
2  * Copyright (c) 2003 National Research Council of Canada
3  *
4  * Permission is hereby granted, free of charge, to any person 
5  * obtaining a copy of this software and associated documentation 
6  * files (the "Software"), to deal in the Software without 
7  * restriction, including without limitation the rights to use, 
8  * copy, modify, merge, publish, distribute, sublicense, and/or 
9  * sell copies of the Software, and to permit persons to whom the 
10  * Software is furnished to do so, subject to the following conditions:
11  *
12  * The above copyright notice and this permission notice shall be 
13  * included in all copies or substantial portions of the Software.
14  *
15  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
16  * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
17  * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
18  * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
19  * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
20  * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
21  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 
22  * OTHER DEALINGS IN THE SOFTWARE.
23  *
24  */
25
26 package edu.internet2.middleware.shibboleth.aa.attrresolv.provider;
27
28 import java.lang.reflect.InvocationTargetException;
29 import java.lang.reflect.Method;
30 import java.security.Principal;
31 import java.sql.Connection;
32 import java.sql.DriverManager;
33 import java.sql.ResultSet;
34 import java.sql.ResultSetMetaData;
35 import java.sql.SQLException;
36 import java.sql.Statement;
37 import java.util.Iterator;
38 import java.util.Properties;
39 import java.util.regex.Matcher;
40 import java.util.regex.Pattern;
41
42 import javax.naming.NamingEnumeration;
43 import javax.naming.NamingException;
44 import javax.naming.directory.Attribute;
45 import javax.naming.directory.Attributes;
46 import javax.naming.directory.BasicAttribute;
47 import javax.naming.directory.BasicAttributes;
48
49 import org.apache.log4j.Logger;
50 import org.w3c.dom.Element;
51 import org.w3c.dom.Node;
52 import org.w3c.dom.NodeList;
53
54 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeResolver;
55 import edu.internet2.middleware.shibboleth.aa.attrresolv.DataConnectorPlugIn;
56 import edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies;
57 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolutionPlugInException;
58 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolverAttribute;
59
60 /*
61  * Built at the Canada Institute for Scientific and Technical Information (CISTI 
62  * <ahref="http://www.cisti-icist.nrc-cnrc.gc.ca/">http://www.cisti-icist.nrc-cnrc.gc.ca/</a>, 
63  * the National Research Council Canada 
64  * (NRC <a href="http://www.nrc-cnrc.gc.ca/">http://www.nrc-cnrc.gc.ca/</a>)
65  * by David Dearman, COOP student from Dalhousie University,
66  * under the direction of Glen Newton, Head research (IT)
67  * <ahref="mailto:glen.newton@nrc-cnrc.gc.ca">glen.newton@nrc-cnrc.gc.ca</a>. 
68  */
69
70 /**
71  * Data Connector that uses JDBC to access user attributes stored in databases.
72  *
73  * @author David Dearman (dearman@cs.dal.ca)
74  */
75
76 public class JDBCDataConnector extends BaseResolutionPlugIn implements DataConnectorPlugIn {
77
78     private static Logger log = Logger.getLogger(JDBCDataConnector.class.getName());
79     private Properties props = new Properties();
80     private String searchVal = null;
81     private String aeClassName = null;
82
83     final private static String QueryAtt = "query";
84     final private static String AttributeExtractorAtt = "attributeExtractor";
85     final private static String DBDriverAtt = "dbDriver";
86     final private static String AEInstanceMethodAtt = "instance";
87     final private static String URLAtt = "dbURL";
88
89     public JDBCDataConnector(Element e) throws ResolutionPlugInException {
90
91         super(e);
92
93         NodeList propertiesNode = e.getElementsByTagNameNS(AttributeResolver.resolverNamespace, "Property");
94         NodeList searchNode = e.getElementsByTagNameNS(AttributeResolver.resolverNamespace, "Search");
95
96         /**
97          * Gets and sets the search parameter and the attribute extractor
98          */
99         searchVal = ((Element) searchNode.item(0)).getAttribute(QueryAtt);
100         aeClassName = ((Element) searchNode.item(0)).getAttribute(AttributeExtractorAtt);
101
102         if (searchVal == null || searchVal.equals("")) {
103             Node tnode = searchNode.item(0).getFirstChild();
104             if (tnode != null && tnode.getNodeType() == Node.TEXT_NODE) {
105                 searchVal = tnode.getNodeValue();
106             }
107             if (searchVal == null || searchVal.equals("")) {
108                 log.error("Search requires a specified query field");
109                 throw new ResolutionPlugInException("mySQLDataConnection requires a \"Search\" specification");
110             }
111         }
112         else {
113             log.debug("Search Query: (" + searchVal + ")");
114         }
115
116         /**
117          * Assigns the property attribute name/value pairs to a hashtable
118          */
119         for (int i = 0; propertiesNode.getLength() > i; i++) {
120             Element property = (Element) propertiesNode.item(i);
121             String propertiesName = property.getAttribute("name");
122             String propertiesValue = property.getAttribute("value");
123
124             if (propertiesName != null
125                 && !propertiesName.equals("")
126                 && propertiesValue != null
127                 && !propertiesValue.equals("")) {
128                 props.setProperty(propertiesName, propertiesValue);
129                 log.debug("Property: (" + propertiesName + ")");
130                 log.debug("   Value: (" + propertiesValue + ")");
131             } else {
132                 log.error("Property is malformed.");
133                 throw new ResolutionPlugInException("Property is malformed.");
134             }
135         }
136         
137         if (props.getProperty(URLAtt) == null) {
138             log.error("JDBC connection requires a dbURL property");
139             throw new ResolutionPlugInException("JDBCDataConnection requires a \"dbURL\" property");
140         }
141     }
142
143     protected String substitute(String source, String pattern, boolean quote, Dependencies depends) {
144         Matcher m=Pattern.compile(pattern).matcher(source);
145         while (m.find()) {
146             String field = source.substring(m.start()+1, m.end()-1);
147             if (field != null && field.length() > 0) {
148                 StringBuffer buf = new StringBuffer();
149                 
150                 //Look for an attribute dependency.
151                 ResolverAttribute dep = depends.getAttributeResolution(field);
152                 if (dep != null) {
153                     Iterator iter=dep.getValues();
154                     while (iter.hasNext()) {
155                         if (buf.length() > 0)
156                             buf = buf.append(',');
157                         if (quote)
158                             buf = buf.append("'");
159                         buf = buf.append(iter.next());
160                         if (quote)
161                             buf = buf.append("'");
162                     }
163                 }
164                 
165                 //If no values found, cycle over the connectors.
166                 Iterator connDeps = connectorDependencyIds.iterator();
167                 while (buf.length() == 0 && connDeps.hasNext()) {
168                     Attributes attrs = depends.getConnectorResolution((String)connDeps.next());
169                     if (attrs != null) {
170                         Attribute attr = attrs.get(field);
171                         if (attr != null) {
172                             try {
173                                 NamingEnumeration vals = attr.getAll();
174                                 while (vals.hasMore()) {
175                                     if (buf.length() > 0)
176                                         buf = buf.append(',');
177                                     if (quote)
178                                         buf = buf.append("'");
179                                     buf = buf.append(vals.next());
180                                     if (quote)
181                                         buf = buf.append("'");
182                                 }
183                             }
184                             catch (NamingException e) {
185                                 // Auto-generated catch block
186                             }
187                         }
188                     }
189                 }
190                 
191                 if (buf.length() == 0) {
192                     log.warn("Unable to find any values to substitute in query for " + field + ", so using the empty string");
193                 }
194                 source = source.replaceAll(m.group(), buf.toString());
195                 m.reset(source);
196             }
197         }
198         return source;
199     }
200
201     public Attributes resolve(Principal principal, String requester, Dependencies depends) throws ResolutionPlugInException {
202         Connection conn = null;
203         ResultSet rs = null;
204         JDBCAttributeExtractor aeClassObj = null;
205
206         log.debug("Resolving connector: (" + getId() + ")");
207         log.debug(getId() + " resolving for principal: (" + principal.getName() + ")");
208         log.debug("The query string before inserting substitutions: " + searchVal);
209
210         //Replaces %PRINCIPAL% in the query string with its value
211         String convertedSearchVal = searchVal.replaceAll("%PRINCIPAL%", principal.getName());
212         convertedSearchVal = convertedSearchVal.replaceAll("@PRINCIPAL@", "'" + principal.getName() + "'");
213         
214         //Find all delimited substitutions and replace with the named attribute value(s).
215         convertedSearchVal = substitute(convertedSearchVal,"%.+%",false,depends);
216         convertedSearchVal = substitute(convertedSearchVal,"@.+@",true,depends);
217         
218         //Replace any escaped substitution delimiters.
219         convertedSearchVal = convertedSearchVal.replaceAll("\\%","%");
220         convertedSearchVal = convertedSearchVal.replaceAll("\\@","@");
221
222         log.debug("The query string after inserting substitutions: " + convertedSearchVal);
223
224         try {
225             //Loads the database driver
226             loadDriver((String) props.get(DBDriverAtt));
227         } catch (ClassNotFoundException e) {
228             log.error("An ClassNotFoundException occured while loading database driver");
229             throw new ResolutionPlugInException(
230                 "An ClassNotFoundException occured while loading database driver: " + e.getMessage());
231         } catch (IllegalAccessException e) {
232             log.error("An IllegalAccessException occured while loading database driver");
233             throw new ResolutionPlugInException(
234                 "An IllegalAccessException occured while loading database driver: " + e.getMessage());
235         } catch (InstantiationException e) {
236             log.error("An InstantionException occured while loading database driver");
237             throw new ResolutionPlugInException(
238                 "An InstantiationException occured while loading database driver: " + e.getMessage());
239         }
240
241         try {
242             //Makes the connection to the database
243             conn = connect();
244         } catch (SQLException e) {
245             log.error("An ERROR occured while connecting to database");
246             throw new ResolutionPlugInException("An ERROR occured while connecting to the database: " + e.getMessage());
247         }
248
249         try {
250             //Gets the results set for the query
251             rs = executeQuery(conn, convertedSearchVal);
252             if (!rs.next())
253                 return new BasicAttributes();
254
255         } catch (SQLException e) {
256             log.error("An ERROR occured while executing the query");
257             throw new ResolutionPlugInException("An ERROR occured while executing the query: " + e.getMessage());
258         }
259
260         /**
261          * If the user has supplied their own class for extracting the attributes from the 
262          * result set, then their class will be run.  A BasicAttributes object is expected as
263          * the result of the extraction.
264          *
265          * If the user has no supplied their own class for extracting the attributes then 
266          * the default extraction is run, which is specified in DefaultAEAtt.
267          */
268         if (aeClassName == null || aeClassName.equals("")) {
269             aeClassName = DefaultAE.class.getName();
270         }
271
272         try {
273             Class aeClass = Class.forName(aeClassName);
274             Method aeMethod = aeClass.getMethod(AEInstanceMethodAtt, null);
275
276             //runs the "instance" method returning and instance of the object
277             aeClassObj = (JDBCAttributeExtractor) (aeMethod.invoke(null, null));
278             log.debug("Supplied attributeExtractor class loaded.");
279
280         } catch (ClassNotFoundException e) {
281             log.error("The supplied attribute extractor class could not be found");
282             throw new ResolutionPlugInException(
283                 "The supplied attribute extractor class could not be found: " + e.getMessage());
284         } catch (NoSuchMethodException e) {
285             log.error("The requested method does not exist");
286             throw new ResolutionPlugInException("The requested method does not exist: " + e.getMessage());
287         } catch (IllegalAccessException e) {
288             log.error("Access is not permitted for invoking requested method");
289             throw new ResolutionPlugInException(
290                 "Access is not permitted for invoking requested method: " + e.getMessage());
291         } catch (InvocationTargetException e) {
292             log.error("An ERROR occured invoking requested method");
293             throw new ResolutionPlugInException("An ERROR occured involking requested method: " + e.getMessage());
294         }
295
296         try {
297             return aeClassObj.extractAttributes(rs);
298
299         } catch (JDBCAttributeExtractorException e) {
300             log.error("An ERROR occured while extracting attributes from result set");
301             throw new ResolutionPlugInException(
302                 "An ERROR occured while extracting attributes from result set: " + e.getMessage());
303         } finally {
304             try {
305                 //release result set
306                 rs.close();
307                 log.debug("Result set released");
308             } catch (SQLException e) {
309                 log.error("An error occured while closing the result set: " + e);
310                 throw new ResolutionPlugInException("An error occured while closing the result set: " + e);
311             }
312
313             try {
314                 //close the connection
315                 conn.close();
316                 log.debug("Connection to database closed");
317             } catch (SQLException e) {
318                 log.error("An error occured while closing the database connection: " + e);
319                 throw new ResolutionPlugInException("An error occured while closing the database connection: " + e);
320             }
321         }
322     }
323
324     /** 
325      * Loads the driver used to access the database
326      * @param driver The driver used to access the database
327      * @throws ResolutionPlugInException If there is a failure to load the driver
328      */
329     public void loadDriver(String driver)
330         throws ClassNotFoundException, IllegalAccessException, InstantiationException {
331         Class.forName(driver).newInstance();
332         log.debug("Loading driver: " + driver);
333     }
334
335     /** 
336      * Makes a connection to the database using the property set.
337      * @return Connection object
338      * @throws SQLException If there is a failure to make a database connection
339      */
340     public Connection connect()
341         throws SQLException {
342         String url = props.getProperty(URLAtt);
343         log.debug(url);
344         Connection conn = DriverManager.getConnection(url,props);
345         log.debug("Connection with database established");
346
347         return conn;
348     }
349
350     /**
351      * Execute the users query
352      * @param query The query the user wishes to execute
353      * @return The result of the users <code>query</code>
354      * @return null if an error occurs during execution
355      * @throws SQLException If an error occurs while executing the query
356     */
357     public ResultSet executeQuery(Connection conn, String query) throws SQLException {
358         log.debug("Users Query: " + query);
359         Statement stmt = conn.createStatement();
360         return stmt.executeQuery(query);
361     }
362 }
363
364 /**
365  * The default attribute extractor. 
366  */
367
368 class DefaultAE implements JDBCAttributeExtractor {
369     private static DefaultAE _instance = null;
370     private static Logger log = Logger.getLogger(DefaultAE.class.getName());
371
372     // Constructor
373     protected DefaultAE() {
374     }
375
376     // Ensures that only one istance of the class at a time
377     public static DefaultAE instance() {
378         if (_instance == null)
379             return new DefaultAE();
380         else
381             return _instance;
382     }
383
384     /**
385      * Method of extracting the attributes from the supplied result set.
386      *
387      * @param ResultSet The result set from the query which contains the attributes
388      * @return BasicAttributes as objects containing all the attributes
389      * @throws JDBCAttributeExtractorException If there is a complication in retrieving the attributes
390      */
391     public BasicAttributes extractAttributes(ResultSet rs) throws JDBCAttributeExtractorException {
392         BasicAttributes attributes = new BasicAttributes();
393
394         log.debug("Using default Attribute Extractor");
395
396         try {
397             ResultSetMetaData rsmd = rs.getMetaData();
398             int numColumns = rsmd.getColumnCount();
399             log.debug("Number of returned columns: " + numColumns);
400
401             for (int i = 1; i <= numColumns; i++) {
402                 String columnName = rsmd.getColumnName(i);
403                 String columnType = rsmd.getColumnTypeName(i);
404                 Object columnValue = rs.getObject(columnName);
405                 log.debug(
406                     "(" + i + ". ColumnType = " + columnType + ") " + columnName + " -> " + (columnValue!=null ? columnValue.toString() : "(null)"));
407                 attributes.put(new BasicAttribute(columnName, columnValue));
408             }
409         }
410         catch (SQLException e) {
411             log.error("An ERROR occured while retrieving result set meta data");
412             throw new JDBCAttributeExtractorException(
413                 "An ERROR occured while retrieving result set meta data: " + e.getMessage());
414         }
415
416         // Check for multiple rows.
417         try {
418             if (rs.next())
419                 throw new JDBCAttributeExtractorException("Query returned more than one row.");
420         }
421         catch (SQLException e) {
422         }
423
424         return attributes;
425     }
426 }