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