001: /*
002:
003: This software is OSI Certified Open Source Software.
004: OSI Certified is a certification mark of the Open Source Initiative.
005:
006: The license (Mozilla version 1.0) can be read at the MMBase site.
007: See http://www.MMBase.org/license
008:
009: */
010: package org.mmbase.storage.implementation.database;
011:
012: import java.sql.*;
013: import java.util.StringTokenizer;
014:
015: import javax.naming.*;
016: import javax.sql.DataSource;
017: import java.io.*;
018: import javax.servlet.ServletContext;
019: import java.text.*;
020:
021: import org.mmbase.module.core.MMBaseContext;
022: import org.mmbase.storage.*;
023: import org.mmbase.storage.search.implementation.database.*;
024: import org.mmbase.storage.search.SearchQueryHandler;
025: import org.mmbase.storage.util.StorageReader;
026: import org.mmbase.util.logging.*;
027: import org.mmbase.util.ResourceLoader;
028: import org.xml.sax.InputSource;
029:
030: /**
031: * A storage manager factory for database storages.
032: * This factory sets up a datasource for connecting to the database.
033: * If you specify the datasource URI in the 'datasource' property in mmbaseroot.xml configuration file,
034: * the factory attempts to obtain the datasource from the application server. If this fails, or no datasource URI is given,
035: * it attempts to use the connectivity offered by the JDBC Module, which is then wrapped in a datasource.
036: * Note that if you provide a datasource you should make the JDBC Module inactive to prevent the module from
037: * interfering with the storage layer.
038: * @todo backward compatibility with the old supportclasses should be done by creating a separate Factory
039: * (LegacyStorageManagerFactory ?).
040: *
041: * @author Pierre van Rooden
042: * @since MMBase-1.7
043: * @version $Id: DatabaseStorageManagerFactory.java,v 1.49 2008/02/22 12:27:48 michiel Exp $
044: */
045: public class DatabaseStorageManagerFactory extends
046: StorageManagerFactory<DatabaseStorageManager> {
047:
048: private static final Logger log = Logging
049: .getLoggerInstance(DatabaseStorageManagerFactory.class);
050:
051: // standard sql reserved words
052: private final static String[] STANDARD_SQL_KEYWORDS = { "absolute",
053: "action", "add", "all", "allocate", "alter", "and", "any",
054: "are", "as", "asc", "assertion", "at", "authorization",
055: "avg", "begin", "between", "bit", "bit_length", "both",
056: "by", "cascade", "cascaded", "case", "cast", "catalog",
057: "char", "character", "char_length", "character_length",
058: "check", "close", "coalesce", "collate", "collation",
059: "column", "commit", "connect", "connection", "constraint",
060: "constraints", "continue", "convert", "corresponding",
061: "count", "create", "cross", "current", "current_date",
062: "current_time", "current_timestamp", "current_user",
063: "cursor", "date", "day", "deallocate", "dec", "decimal",
064: "declare", "default", "deferrable", "deferred", "delete",
065: "desc", "describe", "descriptor", "diagnostics",
066: "disconnect", "distinct", "domain", "double", "drop",
067: "else", "end", "end-exec", "escape", "except", "exception",
068: "exec", "execute", "exists", "external", "extract",
069: "false", "fetch", "first", "float", "for", "foreign",
070: "found", "from", "full", "get", "global", "go", "goto",
071: "grant", "group", "having", "hour", "identity",
072: "immediate", "in", "indicator", "initially", "inner",
073: "input", "insensitive", "insert", "int", "integer",
074: "intersect", "interval", "into", "is", "isolation", "join",
075: "key", "language", "last", "leading", "left", "level",
076: "like", "local", "lower", "match", "max", "min", "minute",
077: "module", "month", "names", "national", "natural", "nchar",
078: "next", "no", "not", "null", "nullif", "numeric",
079: "octet_length", "of", "on", "only", "open", "option", "or",
080: "order", "outer", "output", "overlaps", "pad", "partial",
081: "position", "precision", "prepare", "preserve", "primary",
082: "prior", "privileges", "procedure", "public", "read",
083: "real", "references", "relative", "restrict", "revoke",
084: "right", "rollback", "rows", "schema", "scroll", "second",
085: "section", "select", "session", "session_user", "set",
086: "size", "smallint", "some", "space", "sql", "sqlcode",
087: "sqlerror", "sqlstate", "substring", "sum", "system_user",
088: "table", "temporary", "then", "time", "timestamp",
089: "timezone_hour", "timezone_minute", "to", "trailing",
090: "transaction", "translate", "translation", "trim", "true",
091: "union", "unique", "unknown", "update", "upper", "usage",
092: "user", "using", "value", "values", "varchar", "varying",
093: "view", "when", "whenever", "where", "with", "work",
094: "write", "year", "zone" };
095:
096: // Default query handler class.
097: private final static Class DEFAULT_QUERY_HANDLER_CLASS = org.mmbase.storage.search.implementation.database.BasicSqlHandler.class;
098:
099: // Default storage manager class
100: private final static Class DEFAULT_STORAGE_MANAGER_CLASS = org.mmbase.storage.implementation.database.RelationalDatabaseStorageManager.class;
101:
102: /**
103: * The catalog used by this storage.
104: */
105: protected String catalog = null;
106: private String databaseName = null;
107: /**
108: * The datasource in use by this factory.
109: * The datasource is retrieved either from the application server, or by wrapping the JDBC Module in a generic datasource.
110: */
111: protected DataSource dataSource;
112:
113: /**
114: * The transaction isolation level available for this storage.
115: * Default TRANSACTION_NONE (no transaction support).
116: * The actual value is determined from the database metadata.
117: */
118: protected int transactionIsolation = Connection.TRANSACTION_NONE;
119:
120: /**
121: * Whether transactions and rollback are supported by this database
122: */
123: protected boolean supportsTransactions = false;
124:
125: private static final String BASE_PATH_UNSET = "UNSET";
126: /**
127: * Used by #getBinaryFileBasePath
128: */
129: private String basePath = BASE_PATH_UNSET;
130:
131: public double getVersion() {
132: return 0.1;
133: }
134:
135: public boolean supportsTransactions() {
136: return supportsTransactions;
137: }
138:
139: public String getCatalog() {
140: return catalog;
141: }
142:
143: // this is more or less common
144: private static final java.util.regex.Pattern JDBC_URL_DB = java.util.regex.Pattern
145: .compile("(?i)jdbc:.*;.*DatabaseName=([^;]+?)");
146:
147: // this too
148: private static final java.util.regex.Pattern JDBC_URL = java.util.regex.Pattern
149: .compile("(?i)jdbc:.*:(?:.*[/@])?(.*?)(?:[;\\?].*)?");
150:
151: private static String getDatabaseName(String url) {
152: if (url == null)
153: return null;
154: java.util.regex.Matcher matcher = JDBC_URL_DB.matcher(url);
155: if (matcher.matches()) {
156: return matcher.group(1);
157: }
158: matcher = JDBC_URL.matcher(url);
159: if (matcher.matches()) {
160: return matcher.group(1);
161: }
162: return null;
163: }
164:
165: /**
166: * Doing some best effort to get a 'database name'.
167: * @since MMBase-1.8
168: */
169: public String getDatabaseName() {
170: return databaseName;
171: }
172:
173: /**
174: * Returns the DataSource associated with this factory.
175: * @since MMBase-1.8
176: */
177: public DataSource getDataSource() {
178: return dataSource;
179: }
180:
181: /**
182: * @param binaryFileBasePath For some datasource a file base path may be needed (some configurations of hsql). It can be <code>null</code> during bootstrap. In lookup.xml an alternative URL may be configured then which does not need the file base path.
183: * @since MMBase-1.8
184: */
185: protected DataSource createDataSource(String binaryFileBasePath) {
186: DataSource ds = null;
187: // get the Datasource for the database to use
188: // the datasource uri (i.e. 'jdbc/xa/MMBase' )
189: // is stored in the mmbaseroot module configuration file
190: String dataSourceURI = mmbase.getInitParameter("datasource");
191: if (dataSourceURI != null) {
192: try {
193: String contextName = mmbase
194: .getInitParameter("datasource-context");
195: if (contextName == null) {
196: contextName = "java:comp/env";
197: }
198: log.service("Using configured datasource "
199: + dataSourceURI);
200: Context initialContext = new InitialContext();
201: Context environmentContext = (Context) initialContext
202: .lookup(contextName);
203: ds = (DataSource) environmentContext
204: .lookup(dataSourceURI);
205: } catch (NamingException ne) {
206: log
207: .warn("Datasource '"
208: + dataSourceURI
209: + "' not available. ("
210: + ne.getMessage()
211: + "). Attempt to use JDBC Module to access database.");
212: }
213: }
214: if (ds == null) {
215: log
216: .service("No data-source configured, using Generic data source");
217: // if no datasource is provided, try to obtain the generic datasource (which uses JDBC Module)
218: // This datasource should only be needed in cases were MMBase runs without application server.
219: if (binaryFileBasePath == null) {
220: ds = new GenericDataSource(mmbase); // one argument version triggers also 'meta' mode, which in case of hsql may give another mem-only URL (just for the meta-data).
221: } else {
222: ds = new GenericDataSource(mmbase, binaryFileBasePath);
223: }
224: }
225: //ds.setLogWriter(new LoggerPrintWriter(Logging.getInstance("datasource"));
226: return ds;
227:
228: }
229:
230: /**
231: * Opens and reads the storage configuration document.
232: * Obtain a datasource to the storage, and load configuration attributes.
233: * @throws StorageException if the storage could not be accessed or necessary configuration data is missing or invalid
234: */
235: protected synchronized void load() throws StorageException {
236: // default storagemanager class
237: storageManagerClass = DEFAULT_STORAGE_MANAGER_CLASS;
238:
239: // default searchquery handler class
240: queryHandlerClasses.add(DEFAULT_QUERY_HANDLER_CLASS);
241:
242: dataSource = createDataSource(null);
243: // temporary source only used once, for the meta data.
244:
245: String sqlKeywords;
246:
247: // test the datasource and retrieves options,
248: // which are stored as options in the factory's attribute
249: // this allows for easy retrieval of database options
250: {
251: Connection con = null;
252: try {
253: con = dataSource.getConnection();
254: if (con == null)
255: throw new StorageException(
256: "Did get 'null' connection from data source "
257: + dataSource);
258: catalog = con.getCatalog();
259: log.service("Connecting to catalog with name "
260: + catalog);
261:
262: DatabaseMetaData metaData = con.getMetaData();
263: String url = metaData.getURL();
264: String db = getDatabaseName(url);
265: if (db != null) {
266: databaseName = db;
267: } else {
268: log
269: .service("No db found in database connection meta data URL '"
270: + url + "'");
271: databaseName = catalog;
272: }
273: log.service("Connecting to database with name "
274: + getDatabaseName());
275:
276: // set transaction options
277: supportsTransactions = metaData.supportsTransactions()
278: && metaData.supportsMultipleTransactions();
279:
280: // determine transactionlevels
281: if (metaData
282: .supportsTransactionIsolationLevel(Connection.TRANSACTION_SERIALIZABLE)) {
283: transactionIsolation = Connection.TRANSACTION_SERIALIZABLE;
284: } else if (metaData
285: .supportsTransactionIsolationLevel(Connection.TRANSACTION_REPEATABLE_READ)) {
286: transactionIsolation = Connection.TRANSACTION_REPEATABLE_READ;
287: } else if (metaData
288: .supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_COMMITTED)) {
289: transactionIsolation = Connection.TRANSACTION_READ_COMMITTED;
290: } else if (metaData
291: .supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_UNCOMMITTED)) {
292: transactionIsolation = Connection.TRANSACTION_READ_UNCOMMITTED;
293: } else {
294: supportsTransactions = false;
295: }
296: sqlKeywords = ("" + metaData.getSQLKeywords())
297: .toLowerCase();
298:
299: } catch (SQLException se) {
300: // log.fatal(se.getMessage() + Logging.stackTrace(se)); will be logged in StorageManagerFactory already
301: throw new StorageInaccessibleException(se);
302: } finally {
303: if (con != null) {
304: try {
305: con.close();
306: } catch (SQLException se) {
307: log.error(se);
308: }
309: }
310: }
311: }
312:
313: // why is this not stored in real properties?
314:
315: setOption(Attributes.SUPPORTS_TRANSACTIONS,
316: supportsTransactions);
317: setAttribute(Attributes.TRANSACTION_ISOLATION_LEVEL,
318: transactionIsolation);
319: setOption(Attributes.SUPPORTS_COMPOSITE_INDEX, true);
320: setOption(Attributes.SUPPORTS_DATA_DEFINITION, true);
321:
322: for (String element : STANDARD_SQL_KEYWORDS) {
323: disallowedFields.put(element, null); // during super.load, the null values will be replaced by actual replace-values.
324: }
325:
326: // get the extra reserved sql keywords (according to the JDBC driver)
327: // not sure what case these are in ???
328: StringTokenizer tokens = new StringTokenizer(sqlKeywords, ", ");
329: while (tokens.hasMoreTokens()) {
330: String tok = tokens.nextToken();
331: disallowedFields.put(tok, null);
332: }
333:
334: // load configuration data (is also needing the temprary datasource in getDocumentReader..)
335: super .load();
336: log.service("Now creating the real data source");
337: dataSource = createDataSource(getBinaryFileBasePath(false));
338:
339: // store the datasource as an attribute
340: // mm: WTF. This seems to be a rather essential property, so why it is not wrapped by a normal, comprehensible getDataSource method or so.
341: setAttribute(Attributes.DATA_SOURCE, dataSource);
342:
343: // determine transaction support again (may be manually switched off)
344: supportsTransactions = hasOption(Attributes.SUPPORTS_TRANSACTIONS);
345: }
346:
347: /**
348: * {@inheritDoc}
349: * MMBase determine it using information gained from the datasource, and the lookup.xml file
350: * in the database configuration directory
351: * Storage configuration files should become resource files, and configurable using a storageresource property.
352: * The type of reader to return should be a StorageReader.
353: * @throws StorageException if the storage could not be accessed while determining the database type
354: * @return a StorageReader instance
355: */
356: public StorageReader getDocumentReader() throws StorageException {
357: StorageReader reader = super .getDocumentReader();
358: // if no storage reader configuration has been specified, auto-detect
359: if (reader == null) {
360: String databaseResourcePath;
361: // First, determine the database name from the parameter set in mmbaseroot
362: String databaseName = mmbase.getInitParameter("database");
363: if (databaseName != null && !"".equals(databaseName)) {
364: // if databasename is specified, attempt to use the database resource of that name
365: if (databaseName.endsWith(".xml")) {
366: databaseResourcePath = databaseName;
367: } else {
368: databaseResourcePath = "storage/databases/"
369: + databaseName + ".xml";
370: }
371: } else {
372: // WTF to configure storage, we need a connection already?!
373:
374: // otherwise, search for supported drivers using the lookup xml
375: DatabaseStorageLookup lookup = new DatabaseStorageLookup();
376: Connection con = null;
377: try {
378: con = dataSource.getConnection();
379: DatabaseMetaData metaData = con.getMetaData();
380: databaseResourcePath = lookup
381: .getResourcePath(metaData);
382: if (databaseResourcePath == null) {
383: // TODO: ask the lookup for a string containing all information on which the lookup could verify and display this instead of the classname
384: throw new StorageConfigurationException(
385: "No filter found in "
386: + lookup.getSystemId()
387: + " for driver class:"
388: + metaData.getConnection()
389: .getClass().getName()
390: + "\n");
391: }
392: } catch (SQLException sqle) {
393: throw new StorageInaccessibleException(sqle);
394: } finally {
395: // close connection
396: if (con != null) {
397: try {
398: con.close();
399: } catch (SQLException sqle) {
400: }
401: }
402: }
403: }
404: // get configuration
405: java.net.URL url = ResourceLoader.getConfigurationRoot()
406: .getResource(databaseResourcePath);
407: log.service("Configuration used for database storage: "
408: + url);
409: try {
410: InputSource in = ResourceLoader.getInputSource(url);
411: reader = new StorageReader(this , in);
412: } catch (java.io.IOException ioe) {
413: throw new StorageConfigurationException(ioe);
414: }
415:
416: }
417: return reader;
418: }
419:
420: /**
421: * As {@link #getBinaryFileBasePath(boolean)} with <code>true</code>
422: */
423: public String getBinaryFileBasePath() {
424: return getBinaryFileBasePath(true);
425: }
426:
427: /**
428: * Returns the base path for 'binary file'
429: * @param check If the path is only perhaps needed, you may want to provide 'false' here.
430: * @since MMBase-1.8.1
431: */
432: public String getBinaryFileBasePath(boolean check) {
433: if (basePath == BASE_PATH_UNSET) {
434: basePath = (String) getAttribute(Attributes.BINARY_FILE_PATH);
435: if (basePath == null || basePath.equals("")) {
436: basePath = getDataDir();
437: } else {
438: MessageFormat mf = new MessageFormat(basePath);
439: java.io.File baseFile = new java.io.File(mf
440: .format(getDataDir()));
441: if (!baseFile.isAbsolute()) {
442: ServletContext sc = MMBaseContext
443: .getServletContext();
444: String absolute = sc != null ? sc.getRealPath("/")
445: + File.separator : null;
446: if (absolute == null)
447: absolute = System.getProperty("user.dir")
448: + File.separator;
449: basePath = absolute + basePath;
450: }
451: }
452: if (basePath == null) {
453: log.warn("Cannot determin a Binary File Base Path");
454: return null;
455: } else {
456: log.service("Binary file base path " + basePath);
457: }
458: File baseDir = new File(basePath);
459: try {
460: basePath = baseDir.getCanonicalPath();
461: if (check)
462: checkBinaryFileBasePath(basePath);
463: } catch (IOException ioe) {
464: log.error(ioe);
465: }
466: if (!basePath.endsWith(File.separator)) {
467: basePath += File.separator;
468: }
469: }
470: return basePath;
471: }
472:
473: /**
474: * Tries to ensure that basePath existis and is writable. Logs error and returns false otherwise.
475: * @param basePath a Directory name
476: * @since MMBase-1.8.1
477: */
478: public static boolean checkBinaryFileBasePath(String basePath) {
479: File baseDir = new File(basePath);
480: if (!baseDir.mkdirs() && !baseDir.exists()) {
481: log.error("Cannot create the binary file path " + basePath);
482: }
483: if (!baseDir.canWrite()) {
484: log.error("Cannot write in the binary file path "
485: + basePath);
486: return false;
487: } else {
488: return true;
489: }
490: }
491:
492: protected Object instantiateBasicHandler(Class handlerClass) {
493: // first handler
494: try {
495: java.lang.reflect.Constructor constructor = handlerClass
496: .getConstructor();
497: SqlHandler sqlHandler = (SqlHandler) constructor
498: .newInstance();
499: log.service("Instantiated SqlHandler of type "
500: + handlerClass.getName());
501: return sqlHandler;
502: } catch (NoSuchMethodException nsme) {
503: throw new StorageConfigurationException(nsme);
504: } catch (java.lang.reflect.InvocationTargetException ite) {
505: throw new StorageConfigurationException(ite);
506: } catch (IllegalAccessException iae) {
507: throw new StorageConfigurationException(iae);
508: } catch (InstantiationException ie) {
509: throw new StorageConfigurationException(ie);
510: }
511: }
512:
513: protected Object instantiateChainedHandler(Class handlerClass,
514: Object handler) {
515: // Chained handlers
516: try {
517: java.lang.reflect.Constructor constructor = handlerClass
518: .getConstructor(new Class[] { SqlHandler.class });
519: ChainedSqlHandler sqlHandler = (ChainedSqlHandler) constructor
520: .newInstance(new Object[] { handler });
521: log.service("Instantiated chained SQLHandler of type "
522: + handlerClass.getName());
523: return sqlHandler;
524: } catch (NoSuchMethodException nsme) {
525: throw new StorageConfigurationException(nsme);
526: } catch (java.lang.reflect.InvocationTargetException ite) {
527: throw new StorageConfigurationException(ite);
528: } catch (IllegalAccessException iae) {
529: throw new StorageConfigurationException(iae);
530: } catch (InstantiationException ie) {
531: throw new StorageConfigurationException(ie);
532: }
533: }
534:
535: protected SearchQueryHandler instantiateQueryHandler(Object data) {
536: return new BasicQueryHandler((SqlHandler) data);
537: }
538:
539: public static void main(String[] args) {
540: String u = "jdbc:hsql:test;test=b";
541: if (args.length > 0)
542: u = args[0];
543: System.out.println("Database " + getDatabaseName(u));
544: }
545: }
|