| 1 | /** |
| 2 | * Copyright (C) 2006 NetMind Consulting Bt. |
| 3 | * |
| 4 | * This library is free software; you can redistribute it and/or |
| 5 | * modify it under the terms of the GNU Lesser General Public |
| 6 | * License as published by the Free Software Foundation; either |
| 7 | * version 3 of the License, or (at your option) any later version. |
| 8 | * |
| 9 | * This library is distributed in the hope that it will be useful, |
| 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 12 | * Lesser General Public License for more details. |
| 13 | * |
| 14 | * You should have received a copy of the GNU Lesser General Public |
| 15 | * License along with this library; if not, write to the Free Software |
| 16 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| 17 | */ |
| 18 | |
| 19 | package hu.netmind.beankeeper.db.impl; |
| 20 | |
| 21 | import java.util.*; |
| 22 | import java.io.*; |
| 23 | import java.sql.Connection; |
| 24 | import java.sql.DatabaseMetaData; |
| 25 | import org.apache.log4j.Logger; |
| 26 | import hu.netmind.beankeeper.service.StoreContext; |
| 27 | import hu.netmind.beankeeper.parser.*; |
| 28 | import hu.netmind.beankeeper.common.StoreException; |
| 29 | import hu.netmind.beankeeper.transaction.*; |
| 30 | import hu.netmind.beankeeper.transaction.event.TransactionEvent; |
| 31 | import hu.netmind.beankeeper.transaction.event.TransactionCommittedEvent; |
| 32 | import hu.netmind.beankeeper.transaction.event.TransactionRolledbackEvent; |
| 33 | import hu.netmind.beankeeper.db.*; |
| 34 | import hu.netmind.beankeeper.management.ManagementTracker; |
| 35 | import hu.netmind.beankeeper.event.EventDispatcher; |
| 36 | import hu.netmind.beankeeper.event.PersistenceEventListener; |
| 37 | import hu.netmind.beankeeper.event.PersistenceEvent; |
| 38 | |
| 39 | /** |
| 40 | * This is a database superclass offers basic functions that |
| 41 | * will need to be addressed in every database implementation. |
| 42 | * The following tasks are currently handled by this superclass:<br> |
| 43 | * <ul> |
| 44 | * <li>Transaction handling. Implementations only receive connections |
| 45 | * from here on.</li> |
| 46 | * <li>Table name handling. All table names are transformed to match |
| 47 | * the maximum length supported by database software. Implementations |
| 48 | * are guaranteed to receive only good table names.</li> |
| 49 | * <li>Table column name handling. All attribute names are transformed |
| 50 | * to suitable column names. Reserved words will be escaped.</li> |
| 51 | * <li>Keeping track of transaction statistics.</li> |
| 52 | * </ul> |
| 53 | * @author Brautigam Robert |
| 54 | * @version Revision: $Revision$ |
| 55 | */ |
| 56 | public abstract class DatabaseBase implements PersistenceEventListener, Database |
| 57 | { |
| 58 | private static Logger logger = Logger.getLogger(DatabaseBase.class); |
| 59 | |
| 60 | private Map reservedWords; // Reserved words of database |
| 61 | private Map reverseReservedWords; // Reverse of translated words |
| 62 | |
| 63 | private ConnectionSource connectionSource; |
| 64 | private int maxTableNameLength; |
| 65 | |
| 66 | private Object tableNameMutex = new Object(); // Mutex for accessing table names |
| 67 | private Map tableNames; // Contains alias->realname mappings |
| 68 | private Map transactionNames; // Contains mapping for specific transaction |
| 69 | |
| 70 | private SQLStatistics sqlStatistics = null; |
| 71 | private EventDispatcher eventDispatcher = null; // Injected |
| 72 | private ManagementTracker managementTracker = null; // Injected |
| 73 | |
| 74 | /** |
| 75 | * Initialize this implementation. |
| 76 | */ |
| 77 | public void init(Map parameters) |
| 78 | { |
| 79 | connectionSource=(ConnectionSource) parameters.get( |
| 80 | StoreContext.PARAM_CONNECTIONSOURCE); |
| 81 | // Init reserved words |
| 82 | readReservedWords(); |
| 83 | // Register listener |
| 84 | eventDispatcher.registerListener(this); |
| 85 | // Create database table name mappings, or read them, if |
| 86 | // they exist |
| 87 | Connection connection = null; |
| 88 | try |
| 89 | { |
| 90 | // Determine max lengths |
| 91 | connection = connectionSource.getConnection(); |
| 92 | DatabaseMetaData dmd = connection.getMetaData(); |
| 93 | maxTableNameLength = dmd.getMaxTableNameLength(); |
| 94 | logger.debug("database says it can handle "+maxTableNameLength+" character table names."); |
| 95 | if ( maxTableNameLength == 0 ) |
| 96 | maxTableNameLength = Integer.MAX_VALUE; |
| 97 | if ( maxTableNameLength < 10 ) |
| 98 | throw new StoreException("database can't handle 10 charachter length table names (only "+maxTableNameLength+"). Must be Oracle or something."); |
| 99 | } catch ( StoreException e ) { |
| 100 | throw e; |
| 101 | } catch ( Exception e ) { |
| 102 | throw new StoreException("database table name mapping table could not be created.",e); |
| 103 | } finally { |
| 104 | if ( connection != null ) |
| 105 | connectionSource.releaseConnection(connection); |
| 106 | } |
| 107 | // Create and register mbean |
| 108 | sqlStatistics = new SQLStatistics(); |
| 109 | managementTracker.registerBean("SQLStatistics",sqlStatistics); |
| 110 | } |
| 111 | |
| 112 | /** |
| 113 | * Release all resources. |
| 114 | */ |
| 115 | public void release() |
| 116 | { |
| 117 | logger.debug("releasing all connections..."); |
| 118 | eventDispatcher.unregisterListener(this); |
| 119 | connectionSource.release(); |
| 120 | managementTracker.deregisterBean("SQLStatistics"); |
| 121 | } |
| 122 | |
| 123 | /** |
| 124 | * Get the connection source of this database. |
| 125 | */ |
| 126 | public ConnectionSource getConnectionSource() |
| 127 | { |
| 128 | return connectionSource; |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * Modifies an object already in database with given fields. |
| 133 | * @param tableName The table to save attributes to. |
| 134 | * @param keys The keys of object to save (All object entries have keys). |
| 135 | * @param attributes The attributes in form of name:value pairs. |
| 136 | */ |
| 137 | public void save(Transaction transaction, String tableName, |
| 138 | Map keys, Map attributes) |
| 139 | { |
| 140 | if ( attributes.size() != 0 ) |
| 141 | { |
| 142 | TransactionStatistics stats=save(transaction.getConnection(), |
| 143 | transformTableName(transaction,tableName), transformAttributes(keys), transformAttributes(attributes)); |
| 144 | transaction.getStats().add(stats); |
| 145 | sqlStatistics.add(stats); // To accumulated sql stats |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | /** |
| 150 | * Insert an object into the database. |
| 151 | * @param tableName The table to save attributes to. |
| 152 | * @param id The id of object to save (All object entries have an id). |
| 153 | * @param attributes The attributes in form of name:value pairs. |
| 154 | */ |
| 155 | public void insert(Transaction transaction, String tableName, |
| 156 | Map attributes) |
| 157 | { |
| 158 | if ( attributes.size() != 0 ) |
| 159 | { |
| 160 | TransactionStatistics stats=insert(transaction.getConnection(), |
| 161 | transformTableName(transaction,tableName), transformAttributes(attributes)); |
| 162 | transaction.getStats().add(stats); |
| 163 | sqlStatistics.add(stats); // To accumulated sql stats |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | /** |
| 168 | * Remove an entry from database. |
| 169 | * @param tableName The table to remove object from. |
| 170 | * @param attributes The attributes which identify the object. |
| 171 | * Equality is assumed with each attribute and it's value. |
| 172 | */ |
| 173 | public void remove(Transaction transaction, String tableName, |
| 174 | Map attributes) |
| 175 | { |
| 176 | TransactionStatistics stats=remove(transaction.getConnection(), |
| 177 | transformTableName(transaction,tableName), transformAttributes(attributes)); |
| 178 | transaction.getStats().add(stats); |
| 179 | sqlStatistics.add(stats); // To accumulated sql stats |
| 180 | } |
| 181 | |
| 182 | /** |
| 183 | * Get the reverse translation of a column name. |
| 184 | */ |
| 185 | private String reverseName(String name) |
| 186 | { |
| 187 | // If empty, nop |
| 188 | if ( name == null ) |
| 189 | return null; |
| 190 | name = name.toLowerCase(); |
| 191 | // If it's in the reverse map, return reverse name |
| 192 | String result = (String) reverseReservedWords.get(name); |
| 193 | if ( result != null ) |
| 194 | return result; |
| 195 | // Fall-through |
| 196 | return name; |
| 197 | } |
| 198 | |
| 199 | /** |
| 200 | * Translate name of a column in given table. |
| 201 | */ |
| 202 | private String translateName(String name) |
| 203 | { |
| 204 | // If name is empty, don't do anything |
| 205 | if ( name == null ) |
| 206 | return null; |
| 207 | name = name.toLowerCase(); |
| 208 | // If name is in the reverse map, than this name |
| 209 | // should not be used, because it would make the |
| 210 | // given reverse name not unique |
| 211 | if ( name.endsWith("_underscore") ) |
| 212 | throw new StoreException("can not use the field name: "+name+", it is reserved for translating names"); |
| 213 | // Translate name, if it's in the reserved words list |
| 214 | String result = (String) reservedWords.get(name); |
| 215 | if ( result != null ) |
| 216 | return result; |
| 217 | // Fall-through, return original string |
| 218 | return name; |
| 219 | } |
| 220 | |
| 221 | /** |
| 222 | * Ensure that table exists in database. |
| 223 | * @param tableName The table to check. |
| 224 | * @param attributeTypes The attribute names together with which |
| 225 | * java class they should hold. |
| 226 | * @param create If true, create table physically, if false, only |
| 227 | * update internal representations, but do not create table. |
| 228 | */ |
| 229 | public void ensureTable(Transaction transaction, String tableName, |
| 230 | Map attributeTypes, List keyAttributeNames, boolean create) |
| 231 | { |
| 232 | TransactionStatistics stats=ensureTable(transaction.getConnection(), |
| 233 | transformTableName(transaction,tableName), transformAttributes(attributeTypes), transformAttributes(keyAttributeNames),create); |
| 234 | transaction.getStats().add(stats); |
| 235 | sqlStatistics.add(stats); // To accumulated sql stats |
| 236 | } |
| 237 | |
| 238 | /** |
| 239 | * Select objects from database as ordered list of attribute maps. |
| 240 | * @param transaction The transaction to run in. |
| 241 | * @param stmt The query statement. |
| 242 | * @param limits The limits of the result. (Offset, maximum result count) |
| 243 | * @return The result object. |
| 244 | */ |
| 245 | public SearchResult search(Transaction transaction, |
| 246 | QueryStatement stmt, Limits limits) |
| 247 | { |
| 248 | QueryStatement newStmt = new QueryStatement(stmt); |
| 249 | newStmt.setSpecifiedTerms(new HashSet(replaceTableNames(transaction,newStmt.getSpecifiedTerms()))); |
| 250 | newStmt.setQueryExpression(replaceTableNames(transaction,newStmt.getQueryExpression())); |
| 251 | newStmt.setSelectTerms(replaceTableNames(transaction,newStmt.getSelectTerms())); |
| 252 | newStmt.setOrderByList(replaceOrderTableNames(transaction,newStmt.getOrderByList())); |
| 253 | if ( newStmt.getGroupByList() != null ) |
| 254 | newStmt.setGroupByList(replaceTableNames(transaction,newStmt.getGroupByList())); |
| 255 | if ( newStmt.getHavingExpression() != null ) |
| 256 | newStmt.setHavingExpression(replaceTableNames(transaction,newStmt.getHavingExpression())); |
| 257 | // Run query |
| 258 | SearchResult rawResult = new SearchResult(); |
| 259 | TransactionStatistics stats = search(transaction.getConnection(), |
| 260 | newStmt, limits, rawResult); |
| 261 | transaction.getStats().add(stats); |
| 262 | sqlStatistics.add(stats); // To accumulated sql stats |
| 263 | // Transform result. As the names of the select terms were altered, |
| 264 | // go through the original select terms and populate a new map based |
| 265 | // on the original names. |
| 266 | SearchResult result = new SearchResult(); |
| 267 | result.setResultSize(rawResult.getResultSize()); |
| 268 | List<Map> rawResultList = rawResult.getResult(); |
| 269 | List<Map> resultList = new ArrayList<Map>(); |
| 270 | for ( Map rawResultMap : rawResultList ) |
| 271 | { |
| 272 | Map resultMap = new HashMap(); |
| 273 | Set<Map.Entry<String,Object>> rawResultMapEntries = rawResultMap.entrySet(); |
| 274 | for ( Map.Entry<String,Object> rawResultEntry : rawResultMapEntries ) |
| 275 | resultMap.put(reverseName(rawResultEntry.getKey()),rawResultEntry.getValue()); |
| 276 | resultList.add(resultMap); |
| 277 | if ( logger.isTraceEnabled() ) |
| 278 | logger.trace("transforming result: "+rawResultMap+", into: "+resultMap); |
| 279 | } |
| 280 | result.setResult(resultList); |
| 281 | // Return result transformed |
| 282 | return result; |
| 283 | } |
| 284 | |
| 285 | /** |
| 286 | * Replace all table names in the list of terms. |
| 287 | */ |
| 288 | private List replaceTableNames(Transaction transaction, Collection tableTerms) |
| 289 | { |
| 290 | List result = new ArrayList(); |
| 291 | Iterator iterator = tableTerms.iterator(); |
| 292 | while ( iterator.hasNext() ) |
| 293 | { |
| 294 | TableTerm term = (TableTerm) iterator.next(); |
| 295 | result.add(replaceTableName(transaction,term)); |
| 296 | } |
| 297 | return result; |
| 298 | } |
| 299 | |
| 300 | /** |
| 301 | * Replace the term with a translated term. |
| 302 | */ |
| 303 | private TableTerm replaceTableName(Transaction transaction, TableTerm term) |
| 304 | { |
| 305 | TableTerm newTerm = null; |
| 306 | if ( term instanceof ReferenceTerm ) |
| 307 | { |
| 308 | newTerm = new ReferenceTerm((ReferenceTerm)term); |
| 309 | ((ReferenceTerm)newTerm).setColumnName(translateName(((ReferenceTerm)term).getColumnName())); |
| 310 | } else if ( term instanceof SpecifiedTableTerm ) { |
| 311 | SpecifiedTableTerm specifiedTerm = (SpecifiedTableTerm) term; |
| 312 | SpecifiedTableTerm specifiedNewTerm = new SpecifiedTableTerm(term); |
| 313 | specifiedNewTerm.setReferencedLeftTerms(new ArrayList()); |
| 314 | specifiedNewTerm.setRelatedLeftTerms(new ArrayList()); |
| 315 | newTerm=specifiedNewTerm; |
| 316 | // Recursively replace left terms |
| 317 | for ( int i=0; (specifiedTerm.getRelatedLeftTerms()!=null) && |
| 318 | (i<specifiedTerm.getRelatedLeftTerms().size()); i++ ) |
| 319 | { |
| 320 | SpecifiedTableTerm.LeftjoinEntry entry = (SpecifiedTableTerm.LeftjoinEntry) |
| 321 | specifiedTerm.getRelatedLeftTerms().get(i); |
| 322 | SpecifiedTableTerm.LeftjoinEntry newEntry = new SpecifiedTableTerm.LeftjoinEntry(); |
| 323 | newEntry.term = replaceTableName(transaction,entry.term); |
| 324 | newEntry.expression = replaceTableNames(transaction,entry.expression); |
| 325 | specifiedNewTerm.getRelatedLeftTerms().add(newEntry); |
| 326 | } |
| 327 | for ( int i=0; (specifiedTerm.getReferencedLeftTerms()!=null) && |
| 328 | (i<specifiedTerm.getReferencedLeftTerms().size()); i++ ) |
| 329 | { |
| 330 | SpecifiedTableTerm.LeftjoinEntry entry = (SpecifiedTableTerm.LeftjoinEntry) |
| 331 | specifiedTerm.getReferencedLeftTerms().get(i); |
| 332 | SpecifiedTableTerm.LeftjoinEntry newEntry = new SpecifiedTableTerm.LeftjoinEntry(); |
| 333 | newEntry.term = replaceTableName(transaction,entry.term); |
| 334 | newEntry.expression = replaceTableNames(transaction,entry.expression); |
| 335 | specifiedNewTerm.getReferencedLeftTerms().add(newEntry); |
| 336 | } |
| 337 | } else { |
| 338 | newTerm = new TableTerm(term); |
| 339 | } |
| 340 | newTerm.setTableName(transformTableName(transaction,term.getTableName())); |
| 341 | newTerm.setAlias(translateName(newTerm.getAlias())); |
| 342 | // Return with translated term |
| 343 | return newTerm; |
| 344 | } |
| 345 | |
| 346 | /** |
| 347 | * Replace all tables names in order by statement. |
| 348 | */ |
| 349 | private List replaceOrderTableNames(Transaction transaction, List orderbys) |
| 350 | { |
| 351 | if ( orderbys == null ) |
| 352 | return null; |
| 353 | ArrayList result = new ArrayList(); |
| 354 | for ( int i=0; i<orderbys.size(); i++ ) |
| 355 | { |
| 356 | OrderBy orderby = (OrderBy) orderbys.get(i); |
| 357 | ReferenceTerm refTerm = (ReferenceTerm) orderby.getReferenceTerm(); |
| 358 | ReferenceTerm newTerm = new ReferenceTerm(refTerm); |
| 359 | newTerm.setTableName(transformTableName(transaction,refTerm.getTableName())); |
| 360 | newTerm.setColumnName(translateName(refTerm.getColumnName())); |
| 361 | result.add(new OrderBy(newTerm,orderby.getDirection())); |
| 362 | } |
| 363 | return result; |
| 364 | } |
| 365 | |
| 366 | /** |
| 367 | * Replace all table names in the expression recursively. |
| 368 | */ |
| 369 | private Expression replaceTableNames(Transaction transaction, Expression expr) |
| 370 | { |
| 371 | if ( expr == null ) |
| 372 | return null; |
| 373 | Expression result = new Expression(); |
| 374 | for ( int i=0; i<expr.size(); i++ ) |
| 375 | { |
| 376 | Object term = expr.get(i); |
| 377 | if ( term instanceof ReferenceTerm ) |
| 378 | { |
| 379 | ReferenceTerm refTerm = (ReferenceTerm) term; |
| 380 | result.add(new ReferenceTerm( |
| 381 | transformTableName(transaction,refTerm.getTableName()), |
| 382 | translateName(refTerm.getAlias()), |
| 383 | translateName(refTerm.getColumnName()), |
| 384 | refTerm.getFunction())); |
| 385 | } else if ( term instanceof Expression ) { |
| 386 | result.add(replaceTableNames(transaction,(Expression) term)); |
| 387 | } else |
| 388 | result.add(term); |
| 389 | } |
| 390 | return result; |
| 391 | } |
| 392 | |
| 393 | /** |
| 394 | * Transform the keys of the given list as if they were attribute names |
| 395 | * for the given table. |
| 396 | * @return A list with names transformed. |
| 397 | */ |
| 398 | private List transformAttributes(List attributes) |
| 399 | { |
| 400 | ArrayList result = new ArrayList(); |
| 401 | Iterator entryIterator = attributes.iterator(); |
| 402 | while ( entryIterator.hasNext() ) |
| 403 | { |
| 404 | String entry = (String) entryIterator.next(); |
| 405 | result.add(translateName(entry)); |
| 406 | } |
| 407 | return result; |
| 408 | } |
| 409 | |
| 410 | /** |
| 411 | * Transform the keys of the given map as if they were attribute names |
| 412 | * for the given table. |
| 413 | * @return A map with the same values as the given map, but the keys |
| 414 | * transformed possibly to new names. |
| 415 | */ |
| 416 | private Map transformAttributes(Map attributes) |
| 417 | { |
| 418 | Map result = new HashMap(); |
| 419 | Iterator entryIterator = attributes.entrySet().iterator(); |
| 420 | while ( entryIterator.hasNext() ) |
| 421 | { |
| 422 | Map.Entry entry = (Map.Entry) entryIterator.next(); |
| 423 | result.put(translateName((String) entry.getKey()),entry.getValue()); |
| 424 | } |
| 425 | return result; |
| 426 | } |
| 427 | |
| 428 | /** |
| 429 | * Get the real table name for use with database. This method |
| 430 | * transforms the name to fit database table max name length, and |
| 431 | * makes the name lower case. |
| 432 | */ |
| 433 | private String transformTableName(Transaction transaction, String tableName) |
| 434 | { |
| 435 | if ( tableName.indexOf('_') < 0 ) |
| 436 | return tableName; // Must be already translated, because no packages |
| 437 | // Check if table exists, and load |
| 438 | synchronized ( tableNameMutex ) |
| 439 | { |
| 440 | if ( (tableNames==null) || (transactionNames==null) ) |
| 441 | { |
| 442 | // No table yet, so check if it exists |
| 443 | HashMap tableMapAttributes = new HashMap(); |
| 444 | tableMapAttributes.put("realname",String.class); |
| 445 | tableMapAttributes.put("alias",String.class); |
| 446 | ArrayList tableMapKeys = new ArrayList(); |
| 447 | tableMapKeys.add("alias"); |
| 448 | TransactionStatistics stats = ensureTable(transaction.getConnection(),"tablemap",tableMapAttributes, |
| 449 | tableMapKeys, true); |
| 450 | transaction.getStats().add(stats); |
| 451 | sqlStatistics.add(stats); // To accumulated sql stats |
| 452 | // Now read the whole thing |
| 453 | QueryStatement stmt = new QueryStatement("tablemap",null,null); |
| 454 | SearchResult result = new SearchResult(); |
| 455 | stats = search(transaction.getConnection(),stmt,null,result); |
| 456 | transaction.getStats().add(stats); |
| 457 | sqlStatistics.add(stats); // To accumulated sql stats |
| 458 | tableNames = new HashMap(); |
| 459 | for ( int i=0; i<result.getResult().size(); i++ ) |
| 460 | { |
| 461 | Map attributes = (Map) result.getResult().get(i); |
| 462 | tableNames.put(attributes.get("alias"),attributes.get("realname")); |
| 463 | } |
| 464 | // Add self |
| 465 | tableNames.put("tablemap","tablemap"); |
| 466 | transactionNames = new HashMap(); |
| 467 | } |
| 468 | } |
| 469 | // Check table, whether this alias exists |
| 470 | // First check in transaction table map, |
| 471 | // then in global transaction map |
| 472 | String tableNameCooked = tableName.toLowerCase(); |
| 473 | String realName = getTableName(transaction,tableNameCooked); |
| 474 | if ( realName != null ) |
| 475 | return realName; |
| 476 | if ( logger.isDebugEnabled() ) |
| 477 | { |
| 478 | synchronized ( tableNames ) |
| 479 | { |
| 480 | logger.debug("could not find table alias: "+tableNameCooked+" from: "+tableNames+", will create it."); |
| 481 | } |
| 482 | } |
| 483 | // Ok, name does not exist yet, so create real name |
| 484 | // for this alias. |
| 485 | // First check whether simple names are approriate |
| 486 | // so hu.netmind.beankeeper_Book becomes simply 'book'. |
| 487 | logger.debug("could not find computed name for preliminary table name: "+tableNameCooked+", calculating one."); |
| 488 | String tableNameSimple; |
| 489 | int lastIndex = tableNameCooked.length(); |
| 490 | if ( tableNameCooked.endsWith("_") ) |
| 491 | { |
| 492 | // This is a subtable, so inlcude the previous tag too |
| 493 | if ( tableNameCooked.length() < 2 ) |
| 494 | throw new StoreException("table name too short: "+tableNameCooked); |
| 495 | lastIndex = tableNameCooked.lastIndexOf('_',tableNameCooked.length()-2); |
| 496 | if ( lastIndex <= 0 ) |
| 497 | throw new StoreException("table name ends with '_', but has no parent: "+tableNameCooked); |
| 498 | } |
| 499 | lastIndex = tableNameCooked.lastIndexOf('_',lastIndex-1); |
| 500 | if ( lastIndex != -1 ) |
| 501 | tableNameSimple = tableNameCooked.substring(lastIndex+1); |
| 502 | else |
| 503 | tableNameSimple = tableNameCooked; |
| 504 | // Check, whether simple name is a reserved word. If it |
| 505 | // is, then translate it. |
| 506 | tableNameSimple = translateName(tableNameSimple); |
| 507 | // Check now, whether simple name is good, if not, then |
| 508 | // extend it with package names. |
| 509 | // If name becomes too long, then use numbers to distinguish |
| 510 | logger.debug("trying simple name: "+tableNameSimple); |
| 511 | while ( (tableNameSimple.length()<maxTableNameLength) && |
| 512 | (!tableNameSimple.startsWith("_")) && |
| 513 | (isRealTableNameTaken(transaction,tableNameSimple)) && |
| 514 | (lastIndex > 0) ) |
| 515 | { |
| 516 | // This means table name is still not unambigous, |
| 517 | // but at least it's short, so add another package |
| 518 | // back to the simple name |
| 519 | lastIndex = tableNameCooked.lastIndexOf('_',lastIndex-1); |
| 520 | tableNameSimple = tableNameCooked.substring(lastIndex+1); |
| 521 | } |
| 522 | logger.debug("final simple name: "+tableNameSimple); |
| 523 | // If name became too long, or still not unambigous, then |
| 524 | // add number to the end |
| 525 | String newTableName = tableNameSimple; |
| 526 | for ( int index=0; |
| 527 | (newTableName.length()>maxTableNameLength) || |
| 528 | (getTableName(transaction,newTableName)!=null) ; |
| 529 | index++ ) |
| 530 | { |
| 531 | if ( index>1000 ) |
| 532 | throw new StoreException("something is wrong, could not calculate unabigous name for: "+tableNameCooked); |
| 533 | newTableName = tableNameSimple.substring(0,maxTableNameLength-3)+index; |
| 534 | } |
| 535 | tableNameSimple = newTableName; |
| 536 | // Ok, so far so good. tableNameSimple now contains an unambigous |
| 537 | // appropriately short name for given alias, now only insert, then |
| 538 | // append to table map and return. |
| 539 | // There is a little dirty trick though. Before inserting a class, |
| 540 | // first remove it from the table. This work arounds a problem: |
| 541 | // if two nodes are active, both start without knowning a class, |
| 542 | // then the first inserts it, the second can not, because now it |
| 543 | // already is contained in the database. |
| 544 | logger.debug("translated table name: "+tableNameCooked+" to: "+tableNameSimple); |
| 545 | Map insertTableName = new HashMap(); |
| 546 | insertTableName.put("alias",tableNameCooked); |
| 547 | TransactionStatistics stats = remove(transaction.getConnection(),"tablemap",insertTableName); |
| 548 | transaction.getStats().add(stats); |
| 549 | sqlStatistics.add(stats); // To accumulated sql stats |
| 550 | insertTableName.put("realname",tableNameSimple); |
| 551 | stats = insert(transaction.getConnection(),"tablemap",insertTableName); |
| 552 | transaction.getStats().add(stats); |
| 553 | sqlStatistics.add(stats); // To accumulated sql stats |
| 554 | synchronized ( tableNameMutex ) |
| 555 | { |
| 556 | Map transactionTable = (Map) transactionNames.get(transaction); |
| 557 | if ( transactionTable == null ) |
| 558 | { |
| 559 | transactionTable = new HashMap(); |
| 560 | transactionNames.put(transaction,transactionTable); |
| 561 | } |
| 562 | transactionTable.put(tableNameCooked,tableNameSimple); |
| 563 | } |
| 564 | // Return already |
| 565 | return tableNameSimple; |
| 566 | } |
| 567 | |
| 568 | /** |
| 569 | * Check if table name is taken. |
| 570 | */ |
| 571 | private boolean isRealTableNameTaken(Transaction transaction, String tableName) |
| 572 | { |
| 573 | synchronized ( tableNameMutex ) |
| 574 | { |
| 575 | if ( tableNames.containsValue(tableName) ) |
| 576 | return true; |
| 577 | Map transactionTable = (Map) transactionNames.get(transaction); |
| 578 | if ( (transactionTable!=null) && (transactionTable.containsValue(tableName)) ) |
| 579 | return true; |
| 580 | return false; |
| 581 | } |
| 582 | } |
| 583 | |
| 584 | /** |
| 585 | * Check whether that alias is already assigned a real table name, |
| 586 | * and returns that name. |
| 587 | */ |
| 588 | private String getTableName(Transaction transaction, String alias) |
| 589 | { |
| 590 | synchronized ( tableNameMutex ) |
| 591 | { |
| 592 | Map transactionTable = (Map) transactionNames.get(transaction); |
| 593 | if ( (transactionTable!=null) && (transactionTable.get(alias)!=null) ) |
| 594 | return (String) transactionTable.get(alias); |
| 595 | if ( tableNames.get(alias) != null ) |
| 596 | return (String) tableNames.get(alias); |
| 597 | return null; |
| 598 | } |
| 599 | } |
| 600 | |
| 601 | /** |
| 602 | * Activate or discard table names added in the transaction. |
| 603 | */ |
| 604 | public void handle(PersistenceEvent event) |
| 605 | { |
| 606 | if ( ! (event instanceof TransactionEvent) ) |
| 607 | return; // Quick exit |
| 608 | Transaction transaction = ((TransactionEvent) event).getTransaction(); |
| 609 | if ( event instanceof TransactionCommittedEvent ) |
| 610 | { |
| 611 | synchronized ( tableNameMutex ) |
| 612 | { |
| 613 | Map transactionTables = (Map) transactionNames.get(transaction); |
| 614 | if ( transactionTables == null ) |
| 615 | return; |
| 616 | tableNames.putAll(transactionTables); |
| 617 | transactionNames.remove(transaction); |
| 618 | } |
| 619 | } |
| 620 | if ( event instanceof TransactionRolledbackEvent ) |
| 621 | { |
| 622 | synchronized ( tableNameMutex ) |
| 623 | { |
| 624 | transactionNames.remove(transaction); |
| 625 | } |
| 626 | } |
| 627 | } |
| 628 | |
| 629 | /** |
| 630 | * Read the reserved word list. |
| 631 | */ |
| 632 | private void readReservedWords() |
| 633 | { |
| 634 | reverseReservedWords = new HashMap(); |
| 635 | reservedWords = new HashMap(); |
| 636 | // Read from list |
| 637 | try |
| 638 | { |
| 639 | ClassLoader loader = Database.class.getClassLoader(); |
| 640 | BufferedReader reader = new BufferedReader(new InputStreamReader(loader.getResourceAsStream("reserved.words"))); |
| 641 | String line = null; |
| 642 | String source; |
| 643 | String target; |
| 644 | String obscure; |
| 645 | while ( (line=reader.readLine()) != null ) |
| 646 | { |
| 647 | source = line.toLowerCase(); |
| 648 | target = source+"_"; |
| 649 | // Put in the normal fields |
| 650 | reservedWords.put(source,target); |
| 651 | reverseReservedWords.put(target,source); |
| 652 | // Make a transation for the reverse words, maybe user wants |
| 653 | // to use those, escape them to an obscure name. If user wants |
| 654 | // to use the obscure name, she'll get an error. |
| 655 | obscure = source+"_underscore"; |
| 656 | reservedWords.put(target,obscure); |
| 657 | reverseReservedWords.put(obscure,target); |
| 658 | } |
| 659 | } catch ( Exception e ) { |
| 660 | throw new StoreException("error while reading reserved words list",e); |
| 661 | } |
| 662 | } |
| 663 | |
| 664 | |
| 665 | /** |
| 666 | * Modifies an object already in database with given fields. |
| 667 | * @param tableName The table to save attributes to. |
| 668 | * @param id The id of object to save (All object entries have an id). |
| 669 | * @param attributes The attributes in form of name:value pairs. |
| 670 | */ |
| 671 | protected abstract TransactionStatistics save(Connection connection, String tableName, |
| 672 | Map keys, Map attributes); |
| 673 | |
| 674 | /** |
| 675 | * Insert an object into the database. |
| 676 | * @param tableName The table to save attributes to. |
| 677 | * @param attributes The attributes in form of name:value pairs. |
| 678 | */ |
| 679 | protected abstract TransactionStatistics insert(Connection connection, String tableName, |
| 680 | Map attributes); |
| 681 | |
| 682 | /** |
| 683 | * Remove an entry from database. |
| 684 | * @param tableName The table to remove object from. |
| 685 | * @param attributes The attributes which identify the object. |
| 686 | * Equality is assumed with each attribute and it's value. |
| 687 | */ |
| 688 | protected abstract TransactionStatistics remove(Connection connection, String tableName, |
| 689 | Map attributes); |
| 690 | |
| 691 | /** |
| 692 | * Ensure that table exists in database. It is the responsibility of |
| 693 | * the implementation to ensure that the named table with given |
| 694 | * parameters exists. If the table exists, but is not defined |
| 695 | * as in 'attributeTypes', the implementation <strong>must</strong> |
| 696 | * retain all common attributes during the re-structuring of that |
| 697 | * table. Of course, if no common attributes exist, the implementation |
| 698 | * is free to drop the table and recreate it.<br> |
| 699 | * In every database, the field named 'persistence_id' is the primary |
| 700 | * key if it exists, or no primary key if the attributeTypes do not |
| 701 | * contain it.<br> |
| 702 | * All columns will be indexed by default. |
| 703 | * @param tableName The table to check. |
| 704 | * @param attributeTypes The attribute names together with which |
| 705 | * java class they should hold. |
| 706 | * @param create If true, create table physically, if false, only |
| 707 | * update internal representations, but do not create table. |
| 708 | */ |
| 709 | protected abstract TransactionStatistics ensureTable(Connection connection, String tableName, |
| 710 | Map attributeTypes, List keyAttributeNames, boolean create); |
| 711 | |
| 712 | /** |
| 713 | * Select objects from database as ordered list of attribute maps. |
| 714 | * @param connection The connection to use. |
| 715 | * @param stmt The query statement. |
| 716 | * @param limits The limits of the result. (Offset, maximum result count) |
| 717 | * @param result The result object. It will be filled with data. |
| 718 | */ |
| 719 | protected abstract TransactionStatistics search(Connection connection, QueryStatement stmt, |
| 720 | Limits limits, SearchResult result); |
| 721 | } |
| 722 | |
| 723 | |