001: /**
002: * Copyright (C) 2006 NetMind Consulting Bt.
003: *
004: * This library is free software; you can redistribute it and/or
005: * modify it under the terms of the GNU Lesser General Public
006: * License as published by the Free Software Foundation; either
007: * version 3 of the License, or (at your option) any later version.
008: *
009: * This library is distributed in the hope that it will be useful,
010: * but WITHOUT ANY WARRANTY; without even the implied warranty of
011: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012: * Lesser General Public License for more details.
013: *
014: * You should have received a copy of the GNU Lesser General Public
015: * License along with this library; if not, write to the Free Software
016: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017: */package hu.netmind.persistence;
018:
019: import java.util.Map;
020: import java.util.HashMap;
021: import java.util.Set;
022: import java.util.HashSet;
023: import java.util.List;
024: import java.util.Vector;
025: import java.util.Collections;
026: import java.util.Collection;
027: import java.util.Iterator;
028: import java.io.BufferedReader;
029: import java.io.InputStreamReader;
030: import java.sql.Connection;
031: import java.sql.DatabaseMetaData;
032: import org.apache.log4j.Logger;
033: import hu.netmind.persistence.parser.QueryStatement;
034: import hu.netmind.persistence.parser.TableTerm;
035: import hu.netmind.persistence.parser.ReferenceTerm;
036: import hu.netmind.persistence.parser.Expression;
037: import hu.netmind.persistence.parser.OrderBy;
038:
039: /**
040: * This is the frontend class for databases. All common functions are
041: * here regarding database handling, and the specific database calls
042: * are mostly in database implementations. The following tasks are
043: * currently handled by this frontend:<br>
044: * <ul>
045: * <li>Transaction handling. Implementations only receive connections
046: * from here on</li>
047: * <li>Table name handling. All table names are transformed to match
048: * the maximum length supported by database software. Implementations
049: * are guaranteed to receive only good table names.</li>
050: * <li>Table column name handling. All attribute names are transformed
051: * to suitable column names. Reserved words will be escaped.</li>
052: * <li>Keeping track of transaction statistics.</li>
053: * </ul>
054: * @author Brautigam Robert
055: * @version Revision: $Revision$
056: */
057: public class Database implements TransactionListener {
058: private static Logger logger = Logger.getLogger(Database.class);
059:
060: private static Map reservedWords; // Reserved words of database
061:
062: private ConnectionSource connectionSource;
063: private DatabaseImplementation implementation;
064: private int maxTableNameLength;
065:
066: private Object tableNameMutex = new Object(); // Mutex for accessing table names
067: private Map tableNames; // Contains alias->realname mappings
068: private Map transactionNames; // Contains mapping for specific transaction
069:
070: Database(ConnectionSource connectionSource,
071: DatabaseImplementation implementation) {
072: this .connectionSource = connectionSource;
073: this .implementation = implementation;
074: // Create database table name mappings, or read them, if
075: // they exist
076: Connection connection = null;
077: try {
078: // Determine max lengths
079: connection = connectionSource.getConnection();
080: DatabaseMetaData dmd = connection.getMetaData();
081: maxTableNameLength = dmd.getMaxTableNameLength();
082: logger.debug("database says it can handle "
083: + maxTableNameLength + " character table names.");
084: if (maxTableNameLength == 0)
085: maxTableNameLength = Integer.MAX_VALUE;
086: if (maxTableNameLength < 10)
087: throw new StoreException(
088: "database can't handle 10 charachter length table names (only "
089: + maxTableNameLength
090: + "). Must be Oracle or something.");
091: } catch (StoreException e) {
092: throw e;
093: } catch (Exception e) {
094: throw new StoreException(
095: "database table name mapping table could not be created.",
096: e);
097: } finally {
098: if (connection != null)
099: connectionSource.releaseConnection(connection);
100: }
101: }
102:
103: /**
104: * Release all resources.
105: */
106: public void release() {
107: logger.debug("releasing all connections...");
108: implementation.release(connectionSource);
109: connectionSource.release();
110: }
111:
112: /**
113: * Get the connection source of this database.
114: */
115: public ConnectionSource getConnectionSource() {
116: return connectionSource;
117: }
118:
119: /**
120: * Modifies an object already in database with given fields.
121: * @param tableName The table to save attributes to.
122: * @param keys The keys of object to save (All object entries have keys).
123: * @param attributes The attributes in form of name:value pairs.
124: */
125: public void save(Transaction transaction, String tableName,
126: Map keys, Map attributes) {
127: if (attributes.size() != 0) {
128: DatabaseStatistics stats = implementation.save(transaction
129: .getConnection(), transformTableName(transaction,
130: tableName), transformAttributes(keys),
131: transformAttributes(attributes));
132: transaction.addStats(stats);
133: }
134: }
135:
136: /**
137: * Insert an object into the database.
138: * @param tableName The table to save attributes to.
139: * @param id The id of object to save (All object entries have an id).
140: * @param attributes The attributes in form of name:value pairs.
141: */
142: public void insert(Transaction transaction, String tableName,
143: Map attributes) {
144: if (attributes.size() != 0) {
145: DatabaseStatistics stats = implementation.insert(
146: transaction.getConnection(), transformTableName(
147: transaction, tableName),
148: transformAttributes(attributes));
149: transaction.addStats(stats);
150: }
151: }
152:
153: /**
154: * Remove an entry from database.
155: * @param tableName The table to remove object from.
156: * @param attributes The attributes which identify the object.
157: * Equality is assumed with each attribute and it's value.
158: */
159: public void remove(Transaction transaction, String tableName,
160: Map attributes) {
161: DatabaseStatistics stats = implementation.remove(transaction
162: .getConnection(), transformTableName(transaction,
163: tableName), transformAttributes(attributes));
164: transaction.addStats(stats);
165: }
166:
167: /**
168: * Translate name of a column in given table.
169: */
170: private String translateName(String name) {
171: if (name == null)
172: return null;
173: String result = (String) reservedWords.get(name.toLowerCase());
174: if (result == null)
175: return name;
176: return result;
177: }
178:
179: /**
180: * Ensure that table exists in database.
181: * @param tableName The table to check.
182: * @param attributeTypes The attribute names together with which
183: * java class they should hold.
184: * @param create If true, create table physically, if false, only
185: * update internal representations, but do not create table.
186: */
187: public void ensureTable(Transaction transaction, String tableName,
188: Map attributeTypes, List keyAttributeNames, boolean create) {
189: DatabaseStatistics stats = implementation.ensureTable(
190: transaction.getConnection(), transformTableName(
191: transaction, tableName),
192: transformAttributes(attributeTypes),
193: transformAttributes(keyAttributeNames), create);
194: transaction.addStats(stats);
195: }
196:
197: /**
198: * Select objects from database as ordered list of attribute maps.
199: * @param transaction The transaction to run in.
200: * @param stmt The query statement.
201: * @param limits The limits of the result. (Offset, maximum result count)
202: * @return The result object.
203: */
204: public SearchResult search(Transaction transaction,
205: QueryStatement stmt, Limits limits) {
206: QueryStatement newStmt = new QueryStatement(stmt);
207: newStmt.setAllLeftTableTerms(replaceTableNames(transaction,
208: newStmt.getAllLeftTableTerms()));
209: newStmt.setQueryExpression(replaceTableNames(transaction,
210: newStmt.getQueryExpression()));
211: newStmt.setSelectTerms(replaceTableNames(transaction, newStmt
212: .getSelectTerms()));
213: newStmt.setOrderByList(replaceOrderTableNames(transaction,
214: newStmt.getOrderByList()));
215: // Run query
216: SearchResult rawResult = new SearchResult();
217: DatabaseStatistics stats = implementation.search(transaction
218: .getConnection(), newStmt, limits, rawResult);
219: transaction.addStats(stats);
220: // Transform result
221: SearchResult result = new SearchResult();
222: result.setResultSize(rawResult.getResultSize());
223: List rawResultList = rawResult.getResult();
224: Vector resultList = new Vector();
225: for (int i = 0; i < rawResultList.size(); i++) {
226: Map resultEntry = (Map) rawResultList.get(i);
227: resultList.add(new TranslatorMap(resultEntry));
228: }
229: result.setResult(resultList);
230: // Return result transformed
231: return result;
232: }
233:
234: /**
235: * Replace all table names in the list of terms.
236: */
237: private List replaceTableNames(Transaction transaction,
238: List tableTerms) {
239: Vector result = new Vector();
240: for (int i = 0; i < tableTerms.size(); i++) {
241: TableTerm term = (TableTerm) tableTerms.get(i);
242: // Replace table term itself
243: TableTerm newTerm = null;
244: if (term instanceof ReferenceTerm) {
245: newTerm = new ReferenceTerm((ReferenceTerm) term);
246: ((ReferenceTerm) newTerm)
247: .setColumnName(translateName(((ReferenceTerm) term)
248: .getColumnName()));
249: } else {
250: newTerm = new TableTerm(term);
251: }
252: newTerm.setTableName(transformTableName(transaction, term
253: .getTableName()));
254: result.add(newTerm);
255: // Recursively replace left terms
256: newTerm.setLeftTableTerms(replaceTableNames(transaction,
257: term.getLeftTableTerms()));
258: }
259: return result;
260: }
261:
262: /**
263: * Replace all tables names in order by statement.
264: */
265: private List replaceOrderTableNames(Transaction transaction,
266: List orderbys) {
267: if (orderbys == null)
268: return null;
269: Vector result = new Vector();
270: for (int i = 0; i < orderbys.size(); i++) {
271: OrderBy orderby = (OrderBy) orderbys.get(i);
272: ReferenceTerm refTerm = (ReferenceTerm) orderby
273: .getReferenceTerm();
274: result.add(new OrderBy(new ReferenceTerm(
275: transformTableName(transaction, refTerm
276: .getTableName()), refTerm.getAlias(),
277: translateName(refTerm.getColumnName())), orderby
278: .getDirection()));
279: }
280: return result;
281: }
282:
283: /**
284: * Replace all table names in the expression recursively.
285: */
286: private Expression replaceTableNames(Transaction transaction,
287: Expression expr) {
288: if (expr == null)
289: return null;
290: Expression result = new Expression();
291: for (int i = 0; i < expr.size(); i++) {
292: Object term = expr.get(i);
293: if (term instanceof ReferenceTerm) {
294: ReferenceTerm refTerm = (ReferenceTerm) term;
295: result.add(new ReferenceTerm(transformTableName(
296: transaction, refTerm.getTableName()),
297: translateName(refTerm.getAlias()),
298: translateName(refTerm.getColumnName())));
299: } else if (term instanceof Expression) {
300: result.add(replaceTableNames(transaction,
301: (Expression) term));
302: } else
303: result.add(term);
304: }
305: return result;
306: }
307:
308: /**
309: * Transform the keys of the given list as if they were attribute names
310: * for the given table.
311: * @return A list with names transformed.
312: */
313: private List transformAttributes(List attributes) {
314: Vector result = new Vector();
315: Iterator entryIterator = attributes.iterator();
316: while (entryIterator.hasNext()) {
317: String entry = (String) entryIterator.next();
318: result.add(translateName(entry));
319: }
320: return result;
321: }
322:
323: /**
324: * Transform the keys of the given map as if they were attribute names
325: * for the given table.
326: * @return A map with the same values as the given map, but the keys
327: * transformed possibly to new names.
328: */
329: private Map transformAttributes(Map attributes) {
330: Map result = new HashMap();
331: Iterator entryIterator = attributes.entrySet().iterator();
332: while (entryIterator.hasNext()) {
333: Map.Entry entry = (Map.Entry) entryIterator.next();
334: result.put(translateName((String) entry.getKey()), entry
335: .getValue());
336: }
337: return result;
338: }
339:
340: /**
341: * Get the real table name for use with database. This method
342: * transforms the name to fit database table max name length, and
343: * makes the name lower case.
344: */
345: private String transformTableName(Transaction transaction,
346: String tableName) {
347: // Check if table exists, and load
348: synchronized (tableNameMutex) {
349: if ((tableNames == null) || (transactionNames == null)) {
350: // No table yet, so check if it exists
351: HashMap tableMapAttributes = new HashMap();
352: tableMapAttributes.put("realname", String.class);
353: tableMapAttributes.put("alias", String.class);
354: Vector tableMapKeys = new Vector();
355: tableMapKeys.add("alias");
356: DatabaseStatistics stats = implementation.ensureTable(
357: transaction.getConnection(), "tablemap",
358: tableMapAttributes, tableMapKeys, true);
359: transaction.getStats().add(stats);
360: // Now read the whole thing
361: QueryStatement stmt = new QueryStatement("tablemap",
362: null, null);
363: SearchResult result = new SearchResult();
364: stats = implementation.search(transaction
365: .getConnection(), stmt, null, result);
366: transaction.getStats().add(stats);
367: tableNames = new HashMap();
368: for (int i = 0; i < result.getResult().size(); i++) {
369: Map attributes = (Map) result.getResult().get(i);
370: tableNames.put(attributes.get("alias"), attributes
371: .get("realname"));
372: }
373: // Add self
374: tableNames.put("tablemap", "tablemap");
375: transactionNames = new HashMap();
376: }
377: }
378: // Check table, whether this alias exists
379: // First check in transaction table map,
380: // then in global transaction map
381: String tableNameCooked = tableName.toLowerCase();
382: String realName = getTableName(transaction, tableNameCooked);
383: if (realName != null)
384: return realName;
385: if (logger.isDebugEnabled()) {
386: synchronized (tableNames) {
387: logger.debug("could not find table alias: "
388: + tableNameCooked + " from: " + tableNames
389: + ", will create it.");
390: }
391: }
392: // Ok, name does not exist yet, so create real name
393: // for this alias.
394: // First check whether simple names are approriate
395: // so hu_netmind_persistence_Book becomes simply 'book'.
396: logger
397: .debug("could not find computed name for preliminary table name: "
398: + tableNameCooked + ", calculating one.");
399: String tableNameSimple;
400: int lastIndex = tableNameCooked.length();
401: if (tableNameCooked.endsWith("_")) {
402: // This is a subtable, so inlcude the previous tag too
403: if (tableNameCooked.length() < 2)
404: throw new StoreException("table name too short: "
405: + tableNameCooked);
406: lastIndex = tableNameCooked.lastIndexOf('_',
407: tableNameCooked.length() - 2);
408: if (lastIndex <= 0)
409: throw new StoreException(
410: "table name ends with '_', but has no parent: "
411: + tableNameCooked);
412: }
413: lastIndex = tableNameCooked.lastIndexOf('_', lastIndex - 1);
414: if (lastIndex != -1)
415: tableNameSimple = tableNameCooked.substring(lastIndex + 1);
416: else
417: tableNameSimple = tableNameCooked;
418: // Check, whether simple name is a reserved word. If it
419: // is, then translate it.
420: tableNameSimple = translateName(tableNameSimple);
421: // Check now, whether simple name is good, if not, then
422: // extend it with package names.
423: // If name becomes too long, then use numbers to distinguish
424: logger.debug("trying simple name: " + tableNameSimple);
425: while ((tableNameSimple.length() < maxTableNameLength)
426: && (isRealTableNameTaken(transaction, tableNameSimple))
427: && (lastIndex > 0)) {
428: // This means table name is still not unambigous,
429: // but at least it's short, so add another package
430: // back to the simple name
431: lastIndex = tableNameCooked.lastIndexOf('_', lastIndex - 1);
432: tableNameSimple = tableNameCooked.substring(lastIndex + 1);
433: }
434: logger.debug("final simple name: " + tableNameSimple);
435: // If name became too long, or still not unambigous, then
436: // add number to the end
437: String newTableName = tableNameSimple;
438: for (int index = 0; (newTableName.length() > maxTableNameLength)
439: || (getTableName(transaction, newTableName) != null); index++) {
440: if (index > 1000)
441: throw new StoreException(
442: "something is wrong, could not calculate unabigous name for: "
443: + tableNameCooked);
444: newTableName = tableNameSimple.substring(0,
445: maxTableNameLength - 3)
446: + index;
447: }
448: tableNameSimple = newTableName;
449: // Ok, so far so good. tableNameSimple now contains an unambigous
450: // appropriately short name for given alias, now only insert, then
451: // append to table map and return.
452: // There is a little dirty trick though. Before inserting a class,
453: // first remove it from the table. This work arounds a problem:
454: // if two nodes are active, both start without knowning a class,
455: // then the first inserts it, the second can not, because now it
456: // already is contained in the database.
457: logger.debug("translated table name: " + tableNameCooked
458: + " to: " + tableNameSimple);
459: transaction.getTracker().addListener(this );
460: Map insertTableName = new HashMap();
461: insertTableName.put("alias", tableNameCooked);
462: DatabaseStatistics stats = implementation.remove(transaction
463: .getConnection(), "tablemap", insertTableName);
464: transaction.getStats().add(stats);
465: insertTableName.put("realname", tableNameSimple);
466: stats = implementation.insert(transaction.getConnection(),
467: "tablemap", insertTableName);
468: transaction.getStats().add(stats);
469: synchronized (tableNameMutex) {
470: Map transactionTable = (Map) transactionNames
471: .get(transaction);
472: if (transactionTable == null) {
473: transactionTable = new HashMap();
474: transactionNames.put(transaction, transactionTable);
475: }
476: transactionTable.put(tableNameCooked, tableNameSimple);
477: }
478: // Return already
479: return tableNameSimple;
480: }
481:
482: /**
483: * Check if table name is taken.
484: */
485: private boolean isRealTableNameTaken(Transaction transaction,
486: String tableName) {
487: synchronized (tableNameMutex) {
488: if (tableNames.containsValue(tableName))
489: return true;
490: Map transactionTable = (Map) transactionNames
491: .get(transaction);
492: if ((transactionTable != null)
493: && (transactionTable.containsValue(tableName)))
494: return true;
495: return false;
496: }
497: }
498:
499: /**
500: * Check whether that alias is already assigned a real table name,
501: * and returns that name.
502: */
503: private String getTableName(Transaction transaction, String alias) {
504: synchronized (tableNameMutex) {
505: Map transactionTable = (Map) transactionNames
506: .get(transaction);
507: if ((transactionTable != null)
508: && (transactionTable.get(alias) != null))
509: return (String) transactionTable.get(alias);
510: if (tableNames.get(alias) != null)
511: return (String) tableNames.get(alias);
512: return null;
513: }
514: }
515:
516: /**
517: * Activate table names added in the transaction.
518: */
519: public void transactionCommited(Transaction transaction) {
520: synchronized (tableNameMutex) {
521: Map transactionTables = (Map) transactionNames
522: .get(transaction);
523: if (transactionTables == null)
524: return;
525: tableNames.putAll(transactionTables);
526: transactionNames.remove(transaction);
527: }
528: }
529:
530: /**
531: * Discard table names added in the transaction.
532: */
533: public void transactionRolledback(Transaction transaction) {
534: synchronized (tableNameMutex) {
535: transactionNames.remove(transaction);
536: }
537: }
538:
539: /**
540: * An implementation of map, in which string arguments to the <code>get()</code>
541: * method will be translated as if they were attribute names.
542: */
543: public class TranslatorMap implements Map {
544: private Map m;
545:
546: public TranslatorMap(Map m) {
547: this .m = m;
548: }
549:
550: public String toString() {
551: return m.toString();
552: }
553:
554: public void clear() {
555: m.clear();
556: }
557:
558: public boolean containsKey(Object key) {
559: return m.containsKey(translateName((String) key));
560: }
561:
562: public boolean containsValue(Object value) {
563: return m.containsValue(value);
564: }
565:
566: public Set entrySet() {
567: return m.entrySet();
568: }
569:
570: public boolean equals(Object o) {
571: return m.equals(o);
572: }
573:
574: public Object get(Object key) {
575: return m.get(translateName((String) key));
576: }
577:
578: public int hashCode() {
579: return m.hashCode();
580: }
581:
582: public boolean isEmpty() {
583: return m.isEmpty();
584: }
585:
586: public Set keySet() {
587: return m.keySet();
588: }
589:
590: public Object put(Object key, Object value) {
591: return m.put(key, value);
592: }
593:
594: public void putAll(Map t) {
595: m.putAll(t);
596: }
597:
598: public Object remove(Object key) {
599: return m.remove(translateName((String) key));
600: }
601:
602: public int size() {
603: return m.size();
604: }
605:
606: public Collection values() {
607: return m.values();
608: }
609: }
610:
611: /**
612: * Read the reserved word list.
613: */
614: private static void readReservedWords() {
615: reservedWords = new HashMap();
616: // Read from list
617: try {
618: ClassLoader loader = Database.class.getClassLoader();
619: BufferedReader reader = new BufferedReader(
620: new InputStreamReader(loader
621: .getResourceAsStream("reserved.words")));
622: String line = null;
623: while ((line = reader.readLine()) != null)
624: reservedWords.put(line.toLowerCase(), line
625: .toLowerCase()
626: + "_");
627: } catch (Exception e) {
628: logger
629: .warn(
630: "error while reading reserved words list, will use some/no reserved words.",
631: e);
632: }
633: }
634:
635: static {
636: // Read reserved words
637: readReservedWords();
638: }
639: }
|