Clover coverage report -
Coverage timestamp: Thu Jul 5 2007 20:02:32 EDT
file stats: LOC: 1,142   Methods: 42
NCLOC: 790   Classes: 1
 
 Source file Conditionals Statements Methods TOTAL
BdbjeCacheLoader.java 83.3% 90.6% 97.6% 89.5%
coverage coverage
 1    package org.jboss.cache.loader.bdbje;
 2   
 3    import com.sleepycat.bind.serial.SerialBinding;
 4    import com.sleepycat.bind.serial.StoredClassCatalog;
 5    import com.sleepycat.bind.tuple.TupleBinding;
 6    import com.sleepycat.bind.tuple.TupleInput;
 7    import com.sleepycat.bind.tuple.TupleOutput;
 8    import com.sleepycat.je.Cursor;
 9    import com.sleepycat.je.Database;
 10    import com.sleepycat.je.DatabaseConfig;
 11    import com.sleepycat.je.DatabaseEntry;
 12    import com.sleepycat.je.DeadlockException;
 13    import com.sleepycat.je.Environment;
 14    import com.sleepycat.je.EnvironmentConfig;
 15    import com.sleepycat.je.JEVersion;
 16    import com.sleepycat.je.LockMode;
 17    import com.sleepycat.je.OperationStatus;
 18    import com.sleepycat.je.Transaction;
 19    import net.jcip.annotations.ThreadSafe;
 20    import org.apache.commons.logging.Log;
 21    import org.apache.commons.logging.LogFactory;
 22    import org.jboss.cache.CacheSPI;
 23    import org.jboss.cache.Fqn;
 24    import org.jboss.cache.Modification;
 25    import org.jboss.cache.config.CacheLoaderConfig.IndividualCacheLoaderConfig;
 26    import org.jboss.cache.loader.AbstractCacheLoader;
 27   
 28    import java.io.File;
 29    import java.io.IOException;
 30    import java.io.Serializable;
 31    import java.util.Collections;
 32    import java.util.HashMap;
 33    import java.util.HashSet;
 34    import java.util.List;
 35    import java.util.Map;
 36    import java.util.Set;
 37    import java.util.concurrent.ConcurrentHashMap;
 38   
 39   
 40    /**
 41    * A persistent <code>CacheLoader</code> based on Berkeley DB Java Edition.
 42    * <p/>
 43    * <p>The configuration string format is:</p>
 44    * <pre>environmentDirectoryName[#databaseName]</pre>
 45    * <p>where databaseName, if omitted, defaults to the ClusterName property
 46    * of the CacheImpl.</p>
 47    * <p/>
 48    * <p>A je.properties file may optionally be placed in the JE environment
 49    * directory and used to customize the default JE configuration.</p>
 50    *
 51    * @author Mark Hayes May 16, 2004
 52    * @author Bela Ban
 53    * @version $Id: BdbjeCacheLoader.java,v 1.30 2007/06/19 17:46:50 msurtani Exp $
 54    */
 55    @ThreadSafe
 56    public class BdbjeCacheLoader extends AbstractCacheLoader
 57    {
 58   
 59    private static final int MAX_TXN_RETRIES = 10;
 60    private static final char LOWEST_UTF_CHAR = '\u0001';
 61   
 62    private static final Log log = LogFactory.getLog(BdbjeCacheLoader.class);
 63   
 64    private BdbjeCacheLoaderConfig config;
 65    private Environment env;
 66    private String cacheDbName;
 67    private String catalogDbName;
 68    private Database cacheDb;
 69    private Database catalogDb;
 70    private StoredClassCatalog catalog;
 71    private SerialBinding serialBinding;
 72    private Map<Object, Transaction> txnMap;
 73    private boolean transactional;
 74   
 75    /*
 76    * Service implementation -- lifecycle methods.
 77    * Note that setConfig() and setCache() are called before create().
 78    */
 79   
 80    /**
 81    * Does nothing since start() does all the work.
 82    */
 83  128 public void create() throws Exception
 84    {
 85  128 String license = "\n*************************************************************************************\n" +
 86    "Berkeley DB Java Edition version: " + JEVersion.CURRENT_VERSION.toString() + "\n" +
 87    "JBoss Cache can use Berkeley DB Java Edition from Oracle \n" +
 88    "(http://www.oracle.com/database/berkeley-db/je/index.html)\n" +
 89    "for persistent, reliable and transaction-protected data storage.\n" +
 90    "If you choose to use Berkeley DB Java Edition with JBoss Cache, you must comply with the terms\n" +
 91    "of Oracle's public license, included in the file LICENSE.txt.\n" +
 92    "If you prefer not to release the source code for your own application in order to comply\n" +
 93    "with the Oracle public license, you may purchase a different license for use of\n" +
 94    "Berkeley DB Java Edition with JBoss Cache.\n" +
 95    "See http://www.oracle.com/database/berkeley-db/je/index.html for pricing and license terms\n" +
 96    "*************************************************************************************";
 97  128 System.out.println(license);
 98   
 99  128 log.trace("Creating BdbjeCacheLoader instance.");
 100  128 checkNotOpen();
 101    }
 102   
 103    /**
 104    * Does nothing since stop() does all the work.
 105    */
 106  128 public void destroy()
 107    {
 108    }
 109   
 110    /**
 111    * Opens the JE environment and the database specified by the configuration
 112    * string. The environment and databases are created if necessary.
 113    */
 114  128 public void start()
 115    throws Exception
 116    {
 117   
 118  128 log.trace("Starting BdbjeCacheLoader instance.");
 119  128 checkNotOpen();
 120   
 121  128 if (cache == null)
 122    {
 123  0 throw new IllegalStateException(
 124    "A non-null Cache property (CacheSPI object) is required");
 125    }
 126  128 String configStr = config.getLocation();
 127  128 if (config.getLocation() == null)
 128    {
 129  45 configStr = System.getProperty("java.io.tmpdir");
 130  45 config.setLocation(configStr);
 131    }
 132   
 133    // test location
 134  128 File location = new File(configStr);
 135  128 if (!location.exists())
 136    {
 137  62 boolean created = location.mkdirs();
 138  0 if (!created) throw new IOException("Unable to create cache loader location " + location);
 139   
 140    }
 141  128 if (!location.isDirectory())
 142    {
 143  0 throw new IOException("Cache loader location [" + location + "] is not a directory!");
 144    }
 145   
 146    /* Parse config string. */
 147  128 File homeDir;
 148  128 int offset = configStr.indexOf('#');
 149  128 if (offset >= 0 && offset < configStr.length() - 1)
 150    {
 151  1 homeDir = new File(configStr.substring(0, offset));
 152  1 cacheDbName = configStr.substring(offset + 1);
 153    }
 154    else
 155    {
 156  127 homeDir = new File(configStr);
 157  127 cacheDbName = cache.getClusterName();
 158    }
 159  128 catalogDbName = cacheDbName + "_class_catalog";
 160   
 161    /*
 162    * If the CacheImpl is transactional, we will create transactional
 163    * databases. However, we always create a transactional environment
 164    * since it may be shared by transactional and non-transactional caches.
 165    */
 166  128 transactional = cache.getTransactionManager() != null;
 167   
 168  128 try
 169    {
 170    /* Open the environment, creating it if it doesn't exist. */
 171  128 EnvironmentConfig envConfig = new EnvironmentConfig();
 172  128 envConfig.setAllowCreate(true);
 173  128 envConfig.setTransactional(true);
 174  0 if (log.isTraceEnabled()) log.trace("Creating JE environment with home dir " + homeDir);
 175  128 env = new Environment(homeDir, envConfig);
 176  0 if (log.isDebugEnabled()) log.debug("Created JE environment " + env + " for cache loader " + this);
 177    /* Open cache and catalog databases. */
 178  128 openDatabases();
 179    }
 180    catch (Exception e)
 181    {
 182  0 destroy();
 183  0 throw e;
 184    }
 185    }
 186   
 187    /**
 188    * Opens all databases and initializes database related information.
 189    */
 190  128 private void openDatabases()
 191    throws Exception
 192    {
 193   
 194    /* Use a generic database config, with no duplicates allowed. */
 195  128 DatabaseConfig dbConfig = new DatabaseConfig();
 196  128 dbConfig.setAllowCreate(true);
 197  128 dbConfig.setTransactional(transactional);
 198   
 199    /* Create/open the cache database and associated catalog database. */
 200  128 cacheDb = env.openDatabase(null, cacheDbName, dbConfig);
 201  128 catalogDb = env.openDatabase(null, catalogDbName, dbConfig);
 202   
 203    /* Use the catalog for the serial binding. */
 204  128 catalog = new StoredClassCatalog(catalogDb);
 205  128 serialBinding = new SerialBinding(catalog, null);
 206   
 207    /* Start with a fresh transaction map. */
 208  128 txnMap = new ConcurrentHashMap<Object, Transaction>();
 209    }
 210   
 211    /**
 212    * Closes all databases, ignoring exceptions, and nulls references to all
 213    * database related information.
 214    */
 215  141 private void closeDatabases()
 216    {
 217   
 218  141 if (cacheDb != null)
 219    {
 220  128 try
 221    {
 222  128 cacheDb.close();
 223    }
 224    catch (Exception shouldNotOccur)
 225    {
 226  0 log.warn("Caught unexpected exception", shouldNotOccur);
 227    }
 228    }
 229  141 if (catalogDb != null)
 230    {
 231  128 try
 232    {
 233  128 catalogDb.close();
 234    }
 235    catch (Exception shouldNotOccur)
 236    {
 237  0 log.warn("Caught unexpected exception", shouldNotOccur);
 238    }
 239    }
 240  141 cacheDb = null;
 241  141 catalogDb = null;
 242  141 catalog = null;
 243  141 serialBinding = null;
 244  141 txnMap = null;
 245    }
 246   
 247    /**
 248    * Closes the JE databases and environment, and nulls references to them.
 249    * The environment and databases are not removed from the file system.
 250    * Exceptions during close are ignored.
 251    */
 252  141 public void stop()
 253    {
 254   
 255  141 closeDatabases();
 256   
 257  141 if (env != null)
 258    {
 259  128 try
 260    {
 261  128 env.close();
 262    }
 263    catch (Exception shouldNotOccur)
 264    {
 265  0 log.warn("Unexpected exception", shouldNotOccur);
 266    }
 267    }
 268  141 env = null;
 269    }
 270   
 271    /*
 272    * CacheLoader implementation.
 273    */
 274   
 275    /**
 276    * Sets the configuration string for this cache loader.
 277    */
 278  128 public void setConfig(IndividualCacheLoaderConfig base)
 279    {
 280  128 checkNotOpen();
 281   
 282  128 if (base instanceof BdbjeCacheLoaderConfig)
 283    {
 284  14 this.config = (BdbjeCacheLoaderConfig) base;
 285    }
 286    else
 287    {
 288  114 config = new BdbjeCacheLoaderConfig(base);
 289    }
 290   
 291  0 if (log.isTraceEnabled()) log.trace("Configuring cache loader with location = " + config.getLocation());
 292    }
 293   
 294  114 public IndividualCacheLoaderConfig getConfig()
 295    {
 296  114 return config;
 297    }
 298   
 299    /**
 300    * Sets the CacheImpl owner of this cache loader.
 301    */
 302  128 public void setCache(CacheSPI c)
 303    {
 304  128 super.setCache(c);
 305  128 checkNotOpen();
 306    }
 307   
 308    /**
 309    * Returns an unmodifiable set of relative children names (strings), or
 310    * returns null if the parent node is not found or if no children are found.
 311    * This is a fairly expensive operation, and is assumed to be performed by
 312    * browser applications. Calling this method as part of a run-time
 313    * transaction is not recommended.
 314    */
 315  2160 public Set<String> getChildrenNames(Fqn name)
 316    throws Exception
 317    {
 318   
 319  2160 checkOpen();
 320  2160 checkNonNull(name, "name");
 321   
 322  2160 DatabaseEntry prefixEntry = makeKeyEntry(name);
 323  2160 DatabaseEntry dataEntry = new DatabaseEntry();
 324  2160 dataEntry.setPartial(0, 0, true);
 325   
 326  2160 String namePart = "";
 327  2160 int namePartIndex = name.size();
 328  2160 Set<String> set = null;
 329   
 330  2160 Cursor cursor = cacheDb.openCursor(null, null);
 331  2160 try
 332    {
 333  2160 while (true)
 334    {
 335  2345 DatabaseEntry keyEntry = makeKeyEntry(prefixEntry, namePart);
 336  2345 OperationStatus status =
 337    cursor.getSearchKeyRange(keyEntry, dataEntry, null);
 338  2345 if (status != OperationStatus.SUCCESS ||
 339    !startsWith(keyEntry, prefixEntry))
 340    {
 341  2160 break;
 342    }
 343  185 if (set == null)
 344    {
 345  76 set = new HashSet<String>();
 346    }
 347  185 Fqn childName = makeKeyObject(keyEntry);
 348  185 namePart = childName.get(namePartIndex).toString();
 349  185 set.add(namePart);
 350  185 namePart += LOWEST_UTF_CHAR;
 351    }
 352    }
 353    finally
 354    {
 355  2160 cursor.close();
 356    }
 357  2160 if (set != null)
 358    {
 359  76 return Collections.unmodifiableSet(set);
 360    }
 361    else
 362    {
 363  2084 return null;
 364    }
 365    }
 366   
 367    /**
 368    * Returns a map containing all key-value pairs for the given FQN, or null
 369    * if the node is not present.
 370    * This operation is always non-transactional, even in a transactional
 371    * environment.
 372    */
 373  2840 public Map get(Fqn name)
 374    throws Exception
 375    {
 376   
 377  2840 checkOpen();
 378  2840 checkNonNull(name, "name");
 379   
 380  2840 DatabaseEntry keyEntry = makeKeyEntry(name);
 381  2840 DatabaseEntry foundData = new DatabaseEntry();
 382  2840 OperationStatus status = cacheDb.get(null, keyEntry, foundData, null);
 383  2839 if (status == OperationStatus.SUCCESS)
 384    {
 385    // changed createIfNull param to true
 386    // See http://jira.jboss.com/jira/browse/JBCACHE-118
 387  1832 return makeDataObject(foundData, true);
 388    }
 389    else
 390    {
 391  1007 return null;
 392    }
 393    }
 394   
 395    // See http://jira.jboss.com/jira/browse/JBCACHE-118 for why this is commented out.
 396   
 397    /**
 398    * Returns the data object stored under the given FQN and key, or null if
 399    * the FQN and key are not present.
 400    * This operation is always non-transactional, even in a transactional
 401    * environment.
 402    */
 403    // public Object get(Fqn name, Object key)
 404    // throws Exception {
 405    //
 406    // Map map = get(name);
 407    // if (map != null) {
 408    // return map.get(key);
 409    // } else {
 410    // return null;
 411    // }
 412    // }
 413   
 414    /**
 415    * Returns whether the given node exists.
 416    */
 417  427 public boolean exists(Fqn name)
 418    throws Exception
 419    {
 420   
 421  427 checkOpen();
 422  427 checkNonNull(name, "name");
 423   
 424  427 DatabaseEntry keyEntry = makeKeyEntry(name);
 425  427 DatabaseEntry foundData = new DatabaseEntry();
 426  427 foundData.setPartial(0, 0, true);
 427  427 OperationStatus status = cacheDb.get(null, keyEntry, foundData, null);
 428  427 return (status == OperationStatus.SUCCESS);
 429    }
 430   
 431   
 432    /**
 433    * Stores a single FQN-key-value record.
 434    * Intended to be used in a non-transactional environment, but will use
 435    * auto-commit in a transactional environment.
 436    */
 437  2243 public Object put(Fqn name, Object key, Object value) throws Exception
 438    {
 439   
 440  2243 checkOpen();
 441  2243 checkNonNull(name, "name");
 442   
 443  2243 Object oldVal;
 444  2243 if (transactional)
 445    {
 446  2215 Modification mod =
 447    new Modification(Modification.ModificationType.PUT_KEY_VALUE, name, key, value);
 448  2215 commitModification(mod);
 449  2215 oldVal = mod.getOldValue();
 450    }
 451    else
 452    {
 453  28 oldVal = doPut(null, name, key, value);
 454    }
 455  2243 return oldVal;
 456    }
 457   
 458   
 459    /**
 460    * Internal version of store(String,Object,Object) that allows passing a
 461    * transaction.
 462    */
 463  2281 private Object doPut(Transaction txn, Fqn name, Object key, Object value)
 464    throws Exception
 465    {
 466   
 467  2281 Object oldVal = null;
 468    /* To update-or-insert, try putNoOverwrite first, then a RMW cycle. */
 469  2281 Map<Object, Object> map = new HashMap<Object, Object>();
 470  2281 map.put(key, value);
 471  2281 DatabaseEntry dataEntry = makeDataEntry(map);
 472  2281 DatabaseEntry keyEntry = makeKeyEntry(name);
 473  2281 Cursor cursor = cacheDb.openCursor(txn, null);
 474  2281 try
 475    {
 476  2281 OperationStatus status = cursor.putNoOverwrite(keyEntry, dataEntry);
 477  2281 if (status == OperationStatus.SUCCESS)
 478    {
 479  976 createParentNodes(cursor, name);
 480    }
 481    else
 482    {
 483  1305 DatabaseEntry foundData = new DatabaseEntry();
 484  1305 status = cursor.getSearchKey(keyEntry, foundData, LockMode.RMW);
 485  1305 if (status == OperationStatus.SUCCESS)
 486    {
 487  1305 map = makeDataObject(foundData, true);
 488  1305 oldVal = map.put(key, value);
 489  1305 cursor.putCurrent(makeDataEntry(map));
 490    }
 491    }
 492    }
 493    finally
 494    {
 495  2281 cursor.close();
 496    }
 497  2281 return oldVal;
 498    }
 499   
 500   
 501    /**
 502    * Stores a map of key-values for a given FQN, but does not delete existing
 503    * key-value pairs (that is, it does not erase).
 504    * Intended to be used in a non-transactional environment, but will use
 505    * auto-commit in a transactional environment.
 506    */
 507  2163 public void put(Fqn name, Map values)
 508    throws Exception
 509    {
 510   
 511  2163 checkOpen();
 512  2163 checkNonNull(name, "name");
 513   
 514  2163 if (transactional)
 515    {
 516  2130 commitModification(
 517    new Modification(Modification.ModificationType.PUT_DATA, name, values));
 518    }
 519    else
 520    {
 521  33 doPut(null, name, values);
 522    }
 523    }
 524   
 525   
 526    /**
 527    * Internal version of put(Fqn,Map) that allows passing a
 528    * transaction.
 529    */
 530  2230 private void doPut(Transaction txn, Fqn name, Map values)
 531    throws Exception
 532    {
 533   
 534    // JBCACHE-769 -- make a defensive copy
 535  2230 values = (values == null ? null : new HashMap(values));
 536   
 537    /* To update-or-insert, try putNoOverwrite first, then a RMW cycle. */
 538  2230 DatabaseEntry dataEntry = makeDataEntry(values);
 539  2230 DatabaseEntry keyEntry = makeKeyEntry(name);
 540  2230 Cursor cursor = cacheDb.openCursor(txn, null);
 541  2230 try
 542    {
 543  2230 OperationStatus status = cursor.putNoOverwrite(keyEntry, dataEntry);
 544  2230 if (status == OperationStatus.SUCCESS)
 545    {
 546  1024 createParentNodes(cursor, name);
 547    }
 548    else
 549    {
 550  1206 DatabaseEntry foundData = new DatabaseEntry();
 551  1206 status = cursor.getSearchKey(keyEntry, foundData, LockMode.RMW);
 552  1206 if (status == OperationStatus.SUCCESS)
 553    {
 554  1206 Map map = makeDataObject(foundData, true);
 555  1206 if (values != null)
 556    {
 557  1205 map.putAll(values);
 558    }
 559  1206 cursor.putCurrent(makeDataEntry(map));
 560    }
 561    }
 562    }
 563    finally
 564    {
 565  2230 cursor.close();
 566    }
 567    }
 568   
 569    /**
 570    * Internal version of put(Fqn,Map) that allows passing a
 571    * transaction and erases existing data.
 572    */
 573  0 private void doPutErase(Transaction txn, Fqn name, Map values)
 574    throws Exception
 575    {
 576   
 577    // JBCACHE-769 -- make a defensive copy
 578  0 values = (values == null ? null : new HashMap(values));
 579   
 580  0 DatabaseEntry dataEntry = makeDataEntry(values);
 581  0 DatabaseEntry keyEntry = makeKeyEntry(name);
 582  0 Cursor cursor = cacheDb.openCursor(txn, null);
 583  0 try
 584    {
 585  0 cursor.put(keyEntry, dataEntry);
 586  0 createParentNodes(cursor, name);
 587    }
 588    finally
 589    {
 590  0 cursor.close();
 591    }
 592    }
 593   
 594    /**
 595    * Applies the given modifications.
 596    * Intended to be used in a non-transactional environment, but will use
 597    * auto-commit in a transactional environment.
 598    */
 599  73 public void put(List<Modification> modifications) throws Exception
 600    {
 601  73 checkOpen();
 602  73 checkNonNull(modifications, "modifications");
 603   
 604  73 if (transactional)
 605    {
 606  69 commitModifications(modifications);
 607    }
 608    else
 609    {
 610  4 doPut(null, modifications);
 611    }
 612    }
 613   
 614    /**
 615    * Internal version of put(List) that allows passing a transaction.
 616    */
 617  8815 private void doPut(Transaction txn, List<Modification> modifications)
 618    throws Exception
 619    {
 620   
 621    /* This could be optimized by grouping modifications by Fqn, and
 622    * performing a single database operation for each Fqn (record). */
 623   
 624  8815 for (Modification mod : modifications)
 625    {
 626  8852 Fqn name = mod.getFqn();
 627  8852 Object oldVal;
 628  8852 switch (mod.getType())
 629    {
 630  2253 case PUT_KEY_VALUE:
 631  2253 oldVal = doPut(txn, name, mod.getKey(), mod.getValue());
 632  2253 mod.setOldValue(oldVal);
 633  2253 break;
 634  2197 case PUT_DATA:
 635  2197 doPut(txn, name, mod.getData());
 636  2197 break;
 637  0 case PUT_DATA_ERASE:
 638  0 doPutErase(txn, name, mod.getData());
 639  0 break;
 640  2056 case REMOVE_KEY_VALUE:
 641  2056 oldVal = doRemove(txn, name, mod.getKey());
 642  2056 mod.setOldValue(oldVal);
 643  2056 break;
 644  2323 case REMOVE_NODE:
 645  2323 doRemove(txn, name);
 646  2323 break;
 647  23 case REMOVE_DATA:
 648  23 doRemoveData(txn, name);
 649  23 break;
 650  0 default:
 651  0 throw new IllegalArgumentException(
 652    "Unknown Modification type: " + mod.getType());
 653    }
 654    }
 655    }
 656   
 657    /**
 658    * Creates parent nodes of the given Fqn, moving upward until an existing
 659    * node is found.
 660    */
 661  2000 private void createParentNodes(Cursor cursor, Fqn name)
 662    throws Exception
 663    {
 664   
 665  2000 DatabaseEntry dataEntry = makeDataEntry(null);
 666  2000 for (int nParts = name.size() - 1; nParts >= 1; nParts -= 1)
 667    {
 668  1956 DatabaseEntry keyEntry = makeKeyEntry(name, nParts);
 669  1956 OperationStatus status = cursor.putNoOverwrite(keyEntry, dataEntry);
 670  1956 if (status != OperationStatus.SUCCESS)
 671    {
 672  1818 break;
 673    }
 674    }
 675    }
 676   
 677    /**
 678    * Deletes the node for a given FQN and all its descendent nodes.
 679    * Intended to be used in a non-transactional environment, but will use
 680    * auto-commit in a transactional environment.
 681    */
 682  2339 public void remove(Fqn name)
 683    throws Exception
 684    {
 685   
 686  2339 checkOpen();
 687  2339 checkNonNull(name, "name");
 688   
 689  2339 if (transactional)
 690    {
 691  2316 commitModification(
 692    new Modification(Modification.ModificationType.REMOVE_NODE, name));
 693    }
 694    else
 695    {
 696  23 doRemove(null, name);
 697    }
 698    }
 699   
 700    /**
 701    * Internal version of remove(Fqn) that allows passing a transaction.
 702    */
 703  2346 private void doRemove(Transaction txn, Fqn name)
 704    throws Exception
 705    {
 706   
 707  2346 DatabaseEntry keyEntry = makeKeyEntry(name);
 708  2346 DatabaseEntry foundKey = new DatabaseEntry();
 709  2346 DatabaseEntry foundData = new DatabaseEntry();
 710  2346 foundData.setPartial(0, 0, true);
 711  2346 Cursor cursor = cacheDb.openCursor(txn, null);
 712  2346 try
 713    {
 714  2346 OperationStatus status =
 715    cursor.getSearchKey(keyEntry, foundData, LockMode.RMW);
 716  2346 while (status == OperationStatus.SUCCESS)
 717    {
 718  2010 cursor.delete();
 719  2010 status = cursor.getNext(foundKey, foundData, LockMode.RMW);
 720  2010 if (status == OperationStatus.SUCCESS &&
 721    !startsWith(foundKey, keyEntry))
 722    {
 723  678 status = OperationStatus.NOTFOUND;
 724    }
 725    }
 726    }
 727    finally
 728    {
 729  2346 cursor.close();
 730    }
 731    }
 732   
 733    /**
 734    * Deletes a single FQN-key-value record.
 735    * Intended to be used in a non-transactional environment, but will use
 736    * auto-commit in a transactional environment.
 737    */
 738  2060 public Object remove(Fqn name, Object key)
 739    throws Exception
 740    {
 741   
 742  2060 checkOpen();
 743  2060 checkNonNull(name, "name");
 744   
 745  2060 Object oldVal;
 746  2060 if (transactional)
 747    {
 748  2050 Modification mod =
 749    new Modification(Modification.ModificationType.REMOVE_KEY_VALUE, name, key);
 750  2050 commitModification(mod);
 751  2050 oldVal = mod.getOldValue();
 752    }
 753    else
 754    {
 755  10 oldVal = doRemove(null, name, key);
 756    }
 757  2060 return oldVal;
 758    }
 759   
 760    /**
 761    * Internal version of remove(String,Object) that allows passing a
 762    * transaction.
 763    */
 764  2066 private Object doRemove(Transaction txn, Fqn name, Object key)
 765    throws Exception
 766    {
 767   
 768  2066 Object oldVal = null;
 769  2066 DatabaseEntry keyEntry = makeKeyEntry(name);
 770  2066 DatabaseEntry foundData = new DatabaseEntry();
 771  2066 Cursor cursor = cacheDb.openCursor(txn, null);
 772  2066 try
 773    {
 774  2066 OperationStatus status =
 775    cursor.getSearchKey(keyEntry, foundData, LockMode.RMW);
 776  2066 if (status == OperationStatus.SUCCESS)
 777    {
 778  870 Map map = makeDataObject(foundData, true);
 779  870 oldVal = map.remove(key);
 780  870 cursor.putCurrent(makeDataEntry(map));
 781    }
 782    }
 783    finally
 784    {
 785  2066 cursor.close();
 786    }
 787  2066 return oldVal;
 788    }
 789   
 790    /**
 791    * Clears the map for the given node, but does not remove the node.
 792    */
 793  18 public void removeData(Fqn name)
 794    throws Exception
 795    {
 796   
 797  18 checkOpen();
 798  18 checkNonNull(name, "name");
 799   
 800  18 if (transactional)
 801    {
 802  17 commitModification(
 803    new Modification(Modification.ModificationType.REMOVE_DATA, name));
 804    }
 805    else
 806    {
 807  1 doRemoveData(null, name);
 808    }
 809    }
 810   
 811    /**
 812    * Internal version of removeData(Fqn) that allows passing a transaction.
 813    */
 814  24 private void doRemoveData(Transaction txn, Fqn name)
 815    throws Exception
 816    {
 817   
 818  24 DatabaseEntry dataEntry = new DatabaseEntry();
 819  24 dataEntry.setPartial(0, 0, true);
 820  24 DatabaseEntry keyEntry = makeKeyEntry(name);
 821  24 Cursor cursor = cacheDb.openCursor(txn, null);
 822  24 try
 823    {
 824  24 OperationStatus status =
 825    cursor.getSearchKey(keyEntry, dataEntry, LockMode.RMW);
 826  24 if (status == OperationStatus.SUCCESS)
 827    {
 828  12 cursor.putCurrent(makeDataEntry(null));
 829    }
 830    }
 831    finally
 832    {
 833  24 cursor.close();
 834    }
 835    }
 836   
 837    /**
 838    * Begins a transaction and applies the given modifications.
 839    * <p/>
 840    * <p>If onePhase is true, commits the transaction; otherwise, associates
 841    * the txn value with the transaction and expects commit() or rollback() to
 842    * be called later with the same tx value. Performs retries if necessary to
 843    * resolve deadlocks.</p>
 844    */
 845  15 public void prepare(Object tx, List<Modification> modifications, boolean onePhase) throws Exception
 846    {
 847  15 checkOpen();
 848  15 checkNonNull(modifications, "modifications");
 849  15 if (!onePhase)
 850    {
 851  12 checkNonNull(tx, "tx");
 852    }
 853  15 if (!transactional)
 854    {
 855  1 throw new UnsupportedOperationException(
 856    "prepare() not allowed with a non-transactional cache loader");
 857    }
 858  14 Transaction txn = performTransaction(modifications);
 859  14 if (onePhase)
 860    {
 861  3 txn.commit();
 862    }
 863    else
 864    {
 865  11 txnMap.put(tx, txn);
 866    }
 867    }
 868   
 869    /**
 870    * Performs and commits a single modification. The loader must be
 871    * transactional. Commits the transaction if successful, or aborts the
 872    * transaction and throws an exception if not successful.
 873    */
 874  8728 private void commitModification(Modification mod)
 875    throws Exception
 876    {
 877   
 878  8728 commitModifications(Collections.singletonList(mod));
 879    }
 880   
 881    /**
 882    * Performs and commits a list of modifications. The loader must be
 883    * transactional. Commits the transaction if successful, or aborts the
 884    * transaction and throws an exception if not successful.
 885    */
 886  8797 private void commitModifications(List<Modification> mods)
 887    throws Exception
 888    {
 889   
 890  0 if (!transactional) throw new IllegalStateException();
 891  8797 Transaction txn = performTransaction(mods);
 892  8797 txn.commit();
 893    }
 894   
 895    /**
 896    * Performs the given operation, starting a transaction and performing
 897    * retries. Returns the transaction if successful; aborts the transaction
 898    * and throws an exception if not successful.
 899    */
 900  8811 private Transaction performTransaction(List<Modification> modifications)
 901    throws Exception
 902    {
 903   
 904    /*
 905    * Note that we can't use TransactionRunner here since if onePhase=false
 906    * in the call to prepare(), we do not want to commit. TransactionRunner
 907    * always commits or aborts.
 908    */
 909   
 910  8811 int retries = MAX_TXN_RETRIES;
 911  8811 while (true)
 912    {
 913  8811 Transaction txn = env.beginTransaction(null, null);
 914  8811 try
 915    {
 916  8811 doPut(txn, modifications);
 917  8811 return txn;
 918    }
 919    catch (Exception e)
 920    {
 921  0 txn.abort();
 922  0 if (e instanceof DeadlockException && retries > 0)
 923    {
 924  0 retries -= 1;
 925    }
 926    else
 927    {
 928  0 throw e;
 929    }
 930    }
 931    }
 932    }
 933   
 934    /**
 935    * Commits the given transaction, or throws IllegalArgumentException if the
 936    * given key is not associated with an uncommited transaction.
 937    */
 938  8 public void commit(Object tx)
 939    throws Exception
 940    {
 941   
 942  8 checkOpen();
 943  8 checkNonNull(tx, "tx");
 944   
 945  8 Transaction txn = txnMap.remove(tx);
 946  8 if (txn != null)
 947    {
 948  6 txn.commit();
 949    }
 950  2 else if (transactional)
 951    {
 952  2 throw new IllegalArgumentException("Unknown txn key: " + tx);
 953    }
 954    }
 955   
 956    /**
 957    * Commits the given transaction, or throws IllegalArgumentException if the
 958    * given key is not associated with an uncommited transaction.
 959    */
 960  9 public void rollback(Object tx)
 961    {
 962   
 963  9 checkOpen();
 964  9 checkNonNull(tx, "tx");
 965   
 966  9 Transaction txn = txnMap.remove(tx);
 967  9 if (txn != null)
 968    {
 969  5 try
 970    {
 971  5 txn.abort();
 972    }
 973    catch (Exception ignored)
 974    {
 975    }
 976    }
 977  4 else if (transactional)
 978    {
 979  4 throw new IllegalArgumentException("Unknown txn key: " + tx);
 980    }
 981    }
 982   
 983    /**
 984    * Returns whether the given entry starts with the given prefix bytes.
 985    * Used to determine whether a database key starts with a given FQN.
 986    */
 987  2010 private boolean startsWith(DatabaseEntry entry,
 988    DatabaseEntry prefix)
 989    {
 990  2010 int size = prefix.getSize();
 991  2010 if (size > entry.getSize())
 992    {
 993  109 return false;
 994    }
 995  1901 byte[] d1 = entry.getData();
 996  1901 byte[] d2 = prefix.getData();
 997  1901 int o1 = entry.getOffset();
 998  1901 int o2 = prefix.getOffset();
 999  1901 for (int i = 0; i < size; i += 1)
 1000    {
 1001  12892 if (d1[o1 + i] != d2[o2 + i])
 1002    {
 1003  1556 return false;
 1004    }
 1005    }
 1006  345 return true;
 1007    }
 1008   
 1009    /**
 1010    * Converts a database entry to an Fqn.
 1011    */
 1012  185 private Fqn makeKeyObject(DatabaseEntry entry)
 1013    {
 1014   
 1015  185 Fqn name = Fqn.ROOT;
 1016  185 TupleInput tupleInput = TupleBinding.entryToInput(entry);
 1017  185 while (tupleInput.available() > 0)
 1018    {
 1019  395 String part = tupleInput.readString();
 1020  395 name = new Fqn(name, part);
 1021    }
 1022  185 return name;
 1023    }
 1024   
 1025    /**
 1026    * Converts an Fqn to a database entry.
 1027    */
 1028  14374 private DatabaseEntry makeKeyEntry(Fqn name)
 1029    {
 1030  14374 return makeKeyEntry(name, name.size());
 1031    }
 1032   
 1033    /**
 1034    * Converts an Fqn to a database entry, outputing the given number of name
 1035    * parts.
 1036    */
 1037  16330 private DatabaseEntry makeKeyEntry(Fqn name, int nParts)
 1038    {
 1039    /* Write the sequence of name parts. */
 1040  16330 TupleOutput tupleOutput = new TupleOutput();
 1041  16330 for (int i = 0; i < nParts; i += 1)
 1042    {
 1043  50084 tupleOutput.writeString(name.get(i).toString());
 1044    }
 1045   
 1046    /* Return the tuple as an entry. */
 1047  16330 DatabaseEntry entry = new DatabaseEntry();
 1048  16330 TupleBinding.outputToEntry(tupleOutput, entry);
 1049  16330 return entry;
 1050    }
 1051   
 1052    /**
 1053    * Creates a key database entry from a parent database entry (prefix) and
 1054    * a child name part.
 1055    */
 1056  2345 private DatabaseEntry makeKeyEntry(DatabaseEntry prefix, String namePart)
 1057    {
 1058   
 1059    /* Write the bytes of the prefix followed by the child name. */
 1060  2345 TupleOutput tupleOutput = new TupleOutput();
 1061  2345 tupleOutput.writeFast(prefix.getData(),
 1062    prefix.getOffset(),
 1063    prefix.getSize());
 1064  2345 tupleOutput.writeString(namePart);
 1065   
 1066    /* Return the tuple as an entry. */
 1067  2345 DatabaseEntry entry = new DatabaseEntry();
 1068  2345 TupleBinding.outputToEntry(tupleOutput, entry);
 1069  2345 return entry;
 1070    }
 1071   
 1072    /**
 1073    * Converts a database entry to a Map.
 1074    */
 1075  5213 private Map<Object, Object> makeDataObject(DatabaseEntry entry, boolean createIfNull)
 1076    {
 1077  5213 Map<Object, Object> map = (Map<Object, Object>) serialBinding.entryToObject(entry);
 1078  5213 if (createIfNull && map == null)
 1079    {
 1080  1538 map = new HashMap<Object, Object>();
 1081    }
 1082  5213 return map;
 1083    }
 1084   
 1085    /**
 1086    * Converts a Map to a database entry.
 1087    */
 1088  9904 private DatabaseEntry makeDataEntry(Map map)
 1089    {
 1090   
 1091  9904 if (map != null)
 1092    {
 1093  7778 if (map.size() == 0)
 1094    {
 1095  3157 map = null;
 1096    }
 1097  4621 else if (!(map instanceof Serializable))
 1098    {
 1099  0 map = new HashMap(map);
 1100    }
 1101    }
 1102  9904 DatabaseEntry entry = new DatabaseEntry();
 1103  9904 serialBinding.objectToEntry(map, entry);
 1104  9904 return entry;
 1105    }
 1106   
 1107    /**
 1108    * Throws an exception if the environment is not open.
 1109    */
 1110  14355 private void checkOpen()
 1111    {
 1112  14355 if (env == null)
 1113    {
 1114  0 throw new IllegalStateException(
 1115    "Operation not allowed before calling create()");
 1116    }
 1117    }
 1118   
 1119    /**
 1120    * Throws an exception if the environment is not open.
 1121    */
 1122  512 private void checkNotOpen()
 1123    {
 1124  512 if (env != null)
 1125    {
 1126  0 throw new IllegalStateException(
 1127    "Operation not allowed after calling create()");
 1128    }
 1129    }
 1130   
 1131    /**
 1132    * Throws an exception if the parameter is null.
 1133    */
 1134  14367 private void checkNonNull(Object param, String paramName)
 1135    {
 1136  14367 if (param == null)
 1137    {
 1138  0 throw new NullPointerException(
 1139    "Parameter must not be null: " + paramName);
 1140    }
 1141    }
 1142    }