001: /****************************************************************
002: * Licensed to the Apache Software Foundation (ASF) under one *
003: * or more contributor license agreements. See the NOTICE file *
004: * distributed with this work for additional information *
005: * regarding copyright ownership. The ASF licenses this file *
006: * to you under the Apache License, Version 2.0 (the *
007: * "License"); you may not use this file except in compliance *
008: * with the License. You may obtain a copy of the License at *
009: * *
010: * http://www.apache.org/licenses/LICENSE-2.0 *
011: * *
012: * Unless required by applicable law or agreed to in writing, *
013: * software distributed under the License is distributed on an *
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
015: * KIND, either express or implied. See the License for the *
016: * specific language governing permissions and limitations *
017: * under the License. *
018: ****************************************************************/package org.apache.james.util;
019:
020: import org.apache.oro.text.perl.MalformedPerl5PatternException;
021: import org.apache.oro.text.perl.Perl5Util;
022: import org.w3c.dom.Attr;
023: import org.w3c.dom.Document;
024: import org.w3c.dom.Element;
025: import org.w3c.dom.NamedNodeMap;
026: import org.w3c.dom.NodeList;
027:
028: import javax.xml.parsers.DocumentBuilder;
029: import javax.xml.parsers.DocumentBuilderFactory;
030: import java.io.File;
031: import java.sql.Connection;
032: import java.sql.SQLException;
033: import java.util.HashMap;
034: import java.util.Iterator;
035: import java.util.Map;
036:
037: /**
038: * Provides a set of SQL String resources (eg SQL Strings)
039: * to use for a database connection.
040: * This class allows SQL strings to be customised to particular
041: * database products, by detecting product information from the
042: * jdbc DatabaseMetaData object.
043: *
044: */
045: public class SqlResources {
046: /**
047: * A map of statement types to SQL statements
048: */
049: private Map m_sql = new HashMap();
050:
051: /**
052: * A map of engine specific options
053: */
054: private Map m_dbOptions = new HashMap();
055:
056: /**
057: * A set of all used String values
058: */
059: static private Map stringTable = java.util.Collections
060: .synchronizedMap(new HashMap());
061:
062: /**
063: * A Perl5 regexp matching helper class
064: */
065: private Perl5Util m_perl5Util = new Perl5Util();
066:
067: /**
068: * Configures a DbResources object to provide SQL statements from a file.
069: *
070: * SQL statements returned may be specific to the particular type
071: * and version of the connected database, as well as the database driver.
072: *
073: * Parameters encoded as $(parameter} in the input file are
074: * replace by values from the parameters Map, if the named parameter exists.
075: * Parameter values may also be specified in the resourceSection element.
076: *
077: * @param sqlFile the input file containing the string definitions
078: * @param sqlDefsSection
079: * the xml element containing the strings to be used
080: * @param conn the Jdbc DatabaseMetaData, taken from a database connection
081: * @param configParameters a map of parameters (name-value string pairs) which are
082: * replaced where found in the input strings
083: */
084: public void init(File sqlFile, String sqlDefsSection,
085: Connection conn, Map configParameters) throws Exception {
086: // Parse the sqlFile as an XML document.
087: DocumentBuilderFactory factory = DocumentBuilderFactory
088: .newInstance();
089: DocumentBuilder builder = factory.newDocumentBuilder();
090: Document sqlDoc = builder.parse(sqlFile);
091:
092: // First process the database matcher, to determine the
093: // sql statements to use.
094: Element dbMatcherElement = (Element) (sqlDoc
095: .getElementsByTagName("dbMatchers").item(0));
096: String dbProduct = null;
097: if (dbMatcherElement != null) {
098: dbProduct = matchDbConnection(conn, dbMatcherElement);
099: m_perl5Util = null; // release the PERL matcher!
100: }
101:
102: // Now get the options valid for the database product used.
103: Element dbOptionsElement = (Element) (sqlDoc
104: .getElementsByTagName("dbOptions").item(0));
105: if (dbOptionsElement != null) {
106: // First populate the map with default values
107: populateDbOptions("", dbOptionsElement, m_dbOptions);
108: // Now update the map with specific product values
109: if (dbProduct != null) {
110: populateDbOptions(dbProduct, dbOptionsElement,
111: m_dbOptions);
112: }
113: }
114:
115: // Now get the section defining sql for the repository required.
116: NodeList sections = sqlDoc.getElementsByTagName("sqlDefs");
117: int sectionsCount = sections.getLength();
118: Element sectionElement = null;
119: boolean found = false;
120: for (int i = 0; i < sectionsCount; i++) {
121: sectionElement = (Element) (sections.item(i));
122: String sectionName = sectionElement.getAttribute("name");
123: if (sectionName != null
124: && sectionName.equals(sqlDefsSection)) {
125: found = true;
126: break;
127: }
128:
129: }
130: if (!found) {
131: StringBuffer exceptionBuffer = new StringBuffer(64).append(
132: "Error loading sql definition file. ").append(
133: "The element named \'").append(sqlDefsSection)
134: .append("\' does not exist.");
135: throw new RuntimeException(exceptionBuffer.toString());
136: }
137:
138: // Get parameters defined within the file as defaults,
139: // and use supplied parameters as overrides.
140: Map parameters = new HashMap();
141: // First read from the <params> element, if it exists.
142: Element parametersElement = (Element) (sectionElement
143: .getElementsByTagName("parameters").item(0));
144: if (parametersElement != null) {
145: NamedNodeMap params = parametersElement.getAttributes();
146: int paramCount = params.getLength();
147: for (int i = 0; i < paramCount; i++) {
148: Attr param = (Attr) params.item(i);
149: String paramName = param.getName();
150: String paramValue = param.getValue();
151: parameters.put(paramName, paramValue);
152: }
153: }
154: // Then copy in the parameters supplied with the call.
155: parameters.putAll(configParameters);
156:
157: // 2 maps - one for storing default statements,
158: // the other for statements with a "db" attribute matching this
159: // connection.
160: Map defaultSqlStatements = new HashMap();
161: Map dbProductSqlStatements = new HashMap();
162:
163: // Process each sql statement, replacing string parameters,
164: // and adding to the appropriate map..
165: NodeList sqlDefs = sectionElement.getElementsByTagName("sql");
166: int sqlCount = sqlDefs.getLength();
167: for (int i = 0; i < sqlCount; i++) {
168: // See if this needs to be processed (is default or product specific)
169: Element sqlElement = (Element) (sqlDefs.item(i));
170: String sqlDb = sqlElement.getAttribute("db");
171: Map sqlMap;
172: if (sqlDb.equals("")) {
173: // default
174: sqlMap = defaultSqlStatements;
175: } else if (sqlDb.equals(dbProduct)) {
176: // Specific to this product
177: sqlMap = dbProductSqlStatements;
178: } else {
179: // for a different product
180: continue;
181: }
182:
183: // Get the key and value for this SQL statement.
184: String sqlKey = sqlElement.getAttribute("name");
185: if (sqlKey == null) {
186: // ignore statements without a "name" attribute.
187: continue;
188: }
189: String sqlString = sqlElement.getFirstChild()
190: .getNodeValue();
191:
192: // Do parameter replacements for this sql string.
193: Iterator paramNames = parameters.keySet().iterator();
194: while (paramNames.hasNext()) {
195: String paramName = (String) paramNames.next();
196: String paramValue = (String) parameters.get(paramName);
197:
198: StringBuffer replaceBuffer = new StringBuffer(64)
199: .append("${").append(paramName).append("}");
200: sqlString = substituteSubString(sqlString,
201: replaceBuffer.toString(), paramValue);
202: }
203:
204: // See if we already have registered a string of this value
205: String shared = (String) stringTable.get(sqlString);
206: // If not, register it -- we will use it next time
207: if (shared == null) {
208: stringTable.put(sqlString, sqlString);
209: } else {
210: sqlString = shared;
211: }
212:
213: // Add to the sqlMap - either the "default" or the "product" map
214: sqlMap.put(sqlKey, sqlString);
215: }
216:
217: // Copy in default strings, then overwrite product-specific ones.
218: m_sql.putAll(defaultSqlStatements);
219: m_sql.putAll(dbProductSqlStatements);
220: }
221:
222: /**
223: * Compares the DatabaseProductName value for a jdbc Connection
224: * against a set of regular expressions defined in XML.
225: * The first successful match defines the name of the database product
226: * connected to. This value is then used to choose the specific SQL
227: * expressions to use.
228: *
229: * @param conn the JDBC connection being tested
230: * @param dbMatchersElement the XML element containing the database type information
231: *
232: * @return the type of database to which James is connected
233: *
234: */
235: private String matchDbConnection(Connection conn,
236: Element dbMatchersElement)
237: throws MalformedPerl5PatternException, SQLException {
238: String dbProductName = conn.getMetaData()
239: .getDatabaseProductName();
240:
241: NodeList dbMatchers = dbMatchersElement
242: .getElementsByTagName("dbMatcher");
243: for (int i = 0; i < dbMatchers.getLength(); i++) {
244: // Get the values for this matcher element.
245: Element dbMatcher = (Element) dbMatchers.item(i);
246: String dbMatchName = dbMatcher.getAttribute("db");
247: StringBuffer dbProductPatternBuffer = new StringBuffer(64)
248: .append("/")
249: .append(
250: dbMatcher
251: .getAttribute("databaseProductName"))
252: .append("/i");
253:
254: // If the connection databaseProcuctName matches the pattern,
255: // use the match name from this matcher.
256: if (m_perl5Util.match(dbProductPatternBuffer.toString(),
257: dbProductName)) {
258: return dbMatchName;
259: }
260: }
261: return null;
262: }
263:
264: /**
265: * Gets all the name/value pair db option couples related to the dbProduct,
266: * and put them into the dbOptionsMap.
267: *
268: * @param dbProduct the db product used
269: * @param dbOptionsElement the XML element containing the options
270: * @param dbOptionsMap the <CODE>Map</CODE> to populate
271: *
272: */
273: private void populateDbOptions(String dbProduct,
274: Element dbOptionsElement, Map dbOptionsMap) {
275: NodeList dbOptions = dbOptionsElement
276: .getElementsByTagName("dbOption");
277: for (int i = 0; i < dbOptions.getLength(); i++) {
278: // Get the values for this option element.
279: Element dbOption = (Element) dbOptions.item(i);
280: // Check is this element is pertinent to the dbProduct
281: // Notice that a missing attribute returns "", good for defaults
282: if (!dbProduct
283: .equalsIgnoreCase(dbOption.getAttribute("db"))) {
284: continue;
285: }
286: // Put into the map
287: dbOptionsMap.put(dbOption.getAttribute("name"), dbOption
288: .getAttribute("value"));
289: }
290: }
291:
292: /**
293: * Replace substrings of one string with another string and return altered string.
294: * @param input input string
295: * @param find the string to replace
296: * @param replace the string to replace with
297: * @return the substituted string
298: */
299: private String substituteSubString(String input, String find,
300: String replace) {
301: int find_length = find.length();
302: int replace_length = replace.length();
303:
304: StringBuffer output = new StringBuffer(input);
305: int index = input.indexOf(find);
306: int outputOffset = 0;
307:
308: while (index > -1) {
309: output.replace(index + outputOffset, index + outputOffset
310: + find_length, replace);
311: outputOffset = outputOffset
312: + (replace_length - find_length);
313:
314: index = input.indexOf(find, index + find_length);
315: }
316:
317: String result = output.toString();
318: return result;
319: }
320:
321: /**
322: * Returns a named SQL string for the specified connection,
323: * replacing parameters with the values set.
324: *
325: * @param name the name of the SQL resource required.
326: * @return the requested resource
327: */
328: public String getSqlString(String name) {
329: return (String) m_sql.get(name);
330: }
331:
332: /**
333: * Returns a named SQL string for the specified connection,
334: * replacing parameters with the values set.
335: *
336: * @param name the name of the SQL resource required.
337: * @param required true if the resource is required
338: * @return the requested resource
339: * @throws ConfigurationException
340: * if a required resource cannot be found.
341: */
342: public String getSqlString(String name, boolean required) {
343: String sql = getSqlString(name);
344:
345: if (sql == null && required) {
346: StringBuffer exceptionBuffer = new StringBuffer(64).append(
347: "Required SQL resource: '").append(name).append(
348: "' was not found.");
349: throw new RuntimeException(exceptionBuffer.toString());
350: }
351: return sql;
352: }
353:
354: /**
355: * Returns the dbOption string value set for the specified dbOption name.
356: *
357: * @param name the name of the dbOption required.
358: * @return the requested dbOption value
359: */
360: public String getDbOption(String name) {
361: return (String) m_dbOptions.get(name);
362: }
363:
364: }
|