View Javadoc

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