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 }