View Javadoc

1   /************************************************************************
2    * Copyright (c) 2000-2006 The Apache Software Foundation.             *
3    * All rights reserved.                                                *
4    * ------------------------------------------------------------------- *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you *
6    * may not use this file except in compliance with the License. You    *
7    * may obtain a copy of the License at:                                *
8    *                                                                     *
9    *     http://www.apache.org/licenses/LICENSE-2.0                      *
10   *                                                                     *
11   * Unless required by applicable law or agreed to in writing, software *
12   * distributed under the License is distributed on an "AS IS" BASIS,   *
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or     *
14   * implied.  See the License for the specific language governing       *
15   * permissions and limitations under the License.                      *
16   ***********************************************************************/
17  
18  package org.apache.james.util;
19  
20  import org.apache.oro.text.perl.MalformedPerl5PatternException;
21  import org.apache.oro.text.perl.Perl5Util;
22  import org.w3c.dom.Attr;
23  import org.w3c.dom.Document;
24  import org.w3c.dom.Element;
25  import org.w3c.dom.NamedNodeMap;
26  import org.w3c.dom.NodeList;
27  
28  import javax.xml.parsers.DocumentBuilder;
29  import javax.xml.parsers.DocumentBuilderFactory;
30  import java.io.File;
31  import java.sql.Connection;
32  import java.sql.SQLException;
33  import java.util.HashMap;
34  import java.util.Iterator;
35  import java.util.Map;
36  
37  
38  /***
39   * Provides a set of SQL String resources (eg SQL Strings)
40   * to use for a database connection.
41   * This class allows SQL strings to be customised to particular
42   * database products, by detecting product information from the
43   * jdbc DatabaseMetaData object.
44   * 
45   */
46  public class SqlResources
47  {
48      /***
49       * A map of statement types to SQL statements
50       */
51      private Map m_sql = new HashMap();
52  
53      /***
54       * A map of engine specific options
55       */
56      private Map m_dbOptions = new HashMap();
57  
58      /***
59       * A set of all used String values
60       */
61      static private Map stringTable = java.util.Collections.synchronizedMap(new HashMap());
62  
63      /***
64       * A Perl5 regexp matching helper class
65       */
66      private Perl5Util m_perl5Util = new Perl5Util();
67  
68      /***
69       * Configures a DbResources object to provide SQL statements from a file.
70       * 
71       * SQL statements returned may be specific to the particular type
72       * and version of the connected database, as well as the database driver.
73       * 
74       * Parameters encoded as $(parameter} in the input file are
75       * replace by values from the parameters Map, if the named parameter exists.
76       * Parameter values may also be specified in the resourceSection element.
77       * 
78       * @param sqlFile    the input file containing the string definitions
79       * @param sqlDefsSection
80       *                   the xml element containing the strings to be used
81       * @param conn the Jdbc DatabaseMetaData, taken from a database connection
82       * @param configParameters a map of parameters (name-value string pairs) which are
83       *                   replaced where found in the input strings
84       */
85      public void init(File sqlFile, String sqlDefsSection,
86                       Connection conn, Map configParameters)
87          throws Exception
88      {
89          // Parse the sqlFile as an XML document.
90          DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
91          DocumentBuilder builder = factory.newDocumentBuilder();
92          Document sqlDoc = builder.parse(sqlFile);
93  
94          // First process the database matcher, to determine the
95          //  sql statements to use.
96          Element dbMatcherElement = 
97              (Element)(sqlDoc.getElementsByTagName("dbMatchers").item(0));
98          String dbProduct = null;
99          if ( dbMatcherElement != null ) {
100             dbProduct = matchDbConnection(conn, dbMatcherElement);
101             m_perl5Util = null;     // release the PERL matcher!
102         }
103 
104         // Now get the options valid for the database product used.
105         Element dbOptionsElement = 
106             (Element)(sqlDoc.getElementsByTagName("dbOptions").item(0));
107         if ( dbOptionsElement != null ) {
108             // First populate the map with default values 
109             populateDbOptions("", dbOptionsElement, m_dbOptions);
110             // Now update the map with specific product values
111             if ( dbProduct != null ) {
112                 populateDbOptions(dbProduct, dbOptionsElement, m_dbOptions);
113             }
114         }
115 
116         
117         // Now get the section defining sql for the repository required.
118         NodeList sections = sqlDoc.getElementsByTagName("sqlDefs");
119         int sectionsCount = sections.getLength();
120         Element sectionElement = null;
121         boolean found = false;
122         for (int i = 0; i < sectionsCount; i++ ) {
123             sectionElement = (Element)(sections.item(i));
124             String sectionName = sectionElement.getAttribute("name");
125             if ( sectionName != null && sectionName.equals(sqlDefsSection) ) {
126                 found = true;
127                 break;
128             }
129 
130         }
131         if ( !found ) {
132             StringBuffer exceptionBuffer =
133                 new StringBuffer(64)
134                         .append("Error loading sql definition file. ")
135                         .append("The element named \'")
136                         .append(sqlDefsSection)
137                         .append("\' does not exist.");
138             throw new RuntimeException(exceptionBuffer.toString());
139         }
140 
141         // Get parameters defined within the file as defaults,
142         // and use supplied parameters as overrides.
143         Map parameters = new HashMap();
144         // First read from the <params> element, if it exists.
145         Element parametersElement = 
146             (Element)(sectionElement.getElementsByTagName("parameters").item(0));
147         if ( parametersElement != null ) {
148             NamedNodeMap params = parametersElement.getAttributes();
149             int paramCount = params.getLength();
150             for (int i = 0; i < paramCount; i++ ) {
151                 Attr param = (Attr)params.item(i);
152                 String paramName = param.getName();
153                 String paramValue = param.getValue();
154                 parameters.put(paramName, paramValue);
155             }
156         }
157         // Then copy in the parameters supplied with the call.
158         parameters.putAll(configParameters);
159 
160         // 2 maps - one for storing default statements,
161         // the other for statements with a "db" attribute matching this 
162         // connection.
163         Map defaultSqlStatements = new HashMap();
164         Map dbProductSqlStatements = new HashMap();
165 
166         // Process each sql statement, replacing string parameters,
167         // and adding to the appropriate map..
168         NodeList sqlDefs = sectionElement.getElementsByTagName("sql");
169         int sqlCount = sqlDefs.getLength();
170         for ( int i = 0; i < sqlCount; i++ ) {
171             // See if this needs to be processed (is default or product specific)
172             Element sqlElement = (Element)(sqlDefs.item(i));
173             String sqlDb = sqlElement.getAttribute("db");
174             Map sqlMap;
175             if ( sqlDb.equals("")) {
176                 // default
177                 sqlMap = defaultSqlStatements;
178             }
179             else if (sqlDb.equals(dbProduct) ) {
180                 // Specific to this product
181                 sqlMap = dbProductSqlStatements;
182             }
183             else {
184                 // for a different product
185                 continue;
186             }
187 
188             // Get the key and value for this SQL statement.
189             String sqlKey = sqlElement.getAttribute("name");
190             if ( sqlKey == null ) {
191                 // ignore statements without a "name" attribute.
192                 continue;
193             }
194             String sqlString = sqlElement.getFirstChild().getNodeValue();
195 
196             // Do parameter replacements for this sql string.
197             Iterator paramNames = parameters.keySet().iterator();
198             while ( paramNames.hasNext() ) {
199                 String paramName = (String)paramNames.next();
200                 String paramValue = (String)parameters.get(paramName);
201 
202                 StringBuffer replaceBuffer =
203                     new StringBuffer(64)
204                             .append("${")
205                             .append(paramName)
206                             .append("}");
207                 sqlString = substituteSubString(sqlString, replaceBuffer.toString(), paramValue);
208             }
209 
210             // See if we already have registered a string of this value
211             String shared = (String) stringTable.get(sqlString);
212             // If not, register it -- we will use it next time
213             if (shared == null) {
214                 stringTable.put(sqlString, sqlString);
215             } else {
216                 sqlString = shared;
217             }
218 
219             // Add to the sqlMap - either the "default" or the "product" map
220             sqlMap.put(sqlKey, sqlString);
221         }
222 
223         // Copy in default strings, then overwrite product-specific ones.
224         m_sql.putAll(defaultSqlStatements);
225         m_sql.putAll(dbProductSqlStatements);
226     }
227 
228     /***
229      * Compares the DatabaseProductName value for a jdbc Connection
230      * against a set of regular expressions defined in XML.
231      * The first successful match defines the name of the database product
232      * connected to. This value is then used to choose the specific SQL 
233      * expressions to use.
234      *
235      * @param conn the JDBC connection being tested
236      * @param dbMatchersElement the XML element containing the database type information
237      *
238      * @return the type of database to which James is connected
239      *
240      */
241     private String matchDbConnection(Connection conn, 
242                                      Element dbMatchersElement)
243         throws MalformedPerl5PatternException, SQLException
244     {
245         String dbProductName = conn.getMetaData().getDatabaseProductName();
246     
247         NodeList dbMatchers = 
248             dbMatchersElement.getElementsByTagName("dbMatcher");
249         for ( int i = 0; i < dbMatchers.getLength(); i++ ) {
250             // Get the values for this matcher element.
251             Element dbMatcher = (Element)dbMatchers.item(i);
252             String dbMatchName = dbMatcher.getAttribute("db");
253             StringBuffer dbProductPatternBuffer =
254                 new StringBuffer(64)
255                         .append("/")
256                         .append(dbMatcher.getAttribute("databaseProductName"))
257                         .append("/i");
258 
259             // If the connection databaseProcuctName matches the pattern,
260             // use the match name from this matcher.
261             if ( m_perl5Util.match(dbProductPatternBuffer.toString(), dbProductName) ) {
262                 return dbMatchName;
263             }
264         }
265         return null;
266     }
267 
268     /***
269      * Gets all the name/value pair db option couples related to the dbProduct,
270      * and put them into the dbOptionsMap.
271      *
272      * @param dbProduct the db product used
273      * @param dbOptionsElement the XML element containing the options
274      * @param dbOptionsMap the <CODE>Map</CODE> to populate
275      *
276      */
277     private void populateDbOptions(String dbProduct, Element dbOptionsElement, Map dbOptionsMap)
278     {
279         NodeList dbOptions = 
280             dbOptionsElement.getElementsByTagName("dbOption");
281         for ( int i = 0; i < dbOptions.getLength(); i++ ) {
282             // Get the values for this option element.
283             Element dbOption = (Element)dbOptions.item(i);
284             // Check is this element is pertinent to the dbProduct
285             // Notice that a missing attribute returns "", good for defaults
286             if (!dbProduct.equalsIgnoreCase(dbOption.getAttribute("db"))) {
287                 continue;
288             }
289             // Put into the map
290             dbOptionsMap.put(dbOption.getAttribute("name"), dbOption.getAttribute("value"));
291         }
292     }
293 
294     /***
295      * Replace substrings of one string with another string and return altered string.
296      * @param input input string
297      * @param find the string to replace
298      * @param replace the string to replace with
299      * @return the substituted string
300      */
301     private String substituteSubString( String input, 
302                                         String find,
303                                         String replace )
304     {
305         int find_length = find.length();
306         int replace_length = replace.length();
307 
308         StringBuffer output = new StringBuffer(input);
309         int index = input.indexOf(find);
310         int outputOffset = 0;
311 
312         while ( index > -1 ) {
313             output.replace(index + outputOffset, index + outputOffset + find_length, replace);
314             outputOffset = outputOffset + (replace_length - find_length);
315 
316             index = input.indexOf(find, index + find_length);
317         }
318 
319         String result = output.toString();
320         return result;
321     }
322 
323     /***
324      * Returns a named SQL string for the specified connection,
325      * replacing parameters with the values set.
326      * 
327      * @param name   the name of the SQL resource required.
328      * @return the requested resource
329      */
330     public String getSqlString(String name)
331     {
332         return (String)m_sql.get(name);
333     }
334 
335     /***
336      * Returns a named SQL string for the specified connection,
337      * replacing parameters with the values set.
338      * 
339      * @param name     the name of the SQL resource required.
340      * @param required true if the resource is required
341      * @return the requested resource
342      * @throws ConfigurationException
343      *         if a required resource cannot be found.
344      */
345     public String getSqlString(String name, boolean required)
346     {
347         String sql = getSqlString(name);
348 
349         if (sql == null && required) {
350             StringBuffer exceptionBuffer =
351                 new StringBuffer(64)
352                         .append("Required SQL resource: '")
353                         .append(name)
354                         .append("' was not found.");
355             throw new RuntimeException(exceptionBuffer.toString());
356         }
357         return sql;
358     }
359     
360     /***
361      * Returns the dbOption string value set for the specified dbOption name.
362      * 
363      * @param name the name of the dbOption required.
364      * @return the requested dbOption value
365      */
366     public String getDbOption(String name)
367     {
368         return (String)m_dbOptions.get(name);
369     }
370 
371 }