package com.sleepycat.je; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import com.sleepycat.je.dbi.DatabaseImpl; import com.sleepycat.je.dbi.GetMode; import com.sleepycat.je.dbi.PutMode; import com.sleepycat.je.dbi.CursorImpl.SearchMode; import com.sleepycat.je.txn.Locker; import com.sleepycat.je.txn.LockerFactory; import de.ovgu.cide.jakutil.*; /** * Javadoc for this public class is generated via * the doc templates in the doc_src directory. */ public class SecondaryDatabase extends Database { private Database primaryDb; private SecondaryConfig secondaryConfig; private SecondaryTrigger secondaryTrigger; private ForeignKeyTrigger foreignKeyTrigger; /** * Creates a secondary database but does not open or fully initialize it. */ SecondaryDatabase( Environment env, SecondaryConfig secConfig, Database primaryDatabase) throws DatabaseException { super(env); DatabaseUtil.checkForNullParam(primaryDatabase,"primaryDatabase"); primaryDatabase.checkRequiredDbState(OPEN,"Can't use as primary:"); if (primaryDatabase.configuration.getSortedDuplicates()) { throw new IllegalArgumentException("Duplicates must not be allowed for a primary database: " + primaryDatabase.getDebugName()); } if (env.getEnvironmentImpl() != primaryDatabase.getEnvironment().getEnvironmentImpl()) { throw new IllegalArgumentException("Primary and secondary databases must be in the same" + " environment"); } if (secConfig.getKeyCreator() != null && secConfig.getMultiKeyCreator() != null) { throw new IllegalArgumentException("secConfig.getKeyCreator() and getMultiKeyCreator() may not" + " both be non-null"); } if (!primaryDatabase.configuration.getReadOnly() && secConfig.getKeyCreator() == null && secConfig.getMultiKeyCreator() == null) { throw new NullPointerException("secConfig and getKeyCreator()/getMultiKeyCreator()" + " may be null only if the primary database is read-only"); } if (secConfig.getForeignKeyNullifier() != null && secConfig.getForeignMultiKeyNullifier() != null) { throw new IllegalArgumentException("secConfig.getForeignKeyNullifier() and" + " getForeignMultiKeyNullifier() may not both be non-null"); } if (secConfig.getForeignKeyDeleteAction() == ForeignKeyDeleteAction.NULLIFY && secConfig.getForeignKeyNullifier() == null && secConfig.getForeignMultiKeyNullifier() == null) { throw new NullPointerException("ForeignKeyNullifier or ForeignMultiKeyNullifier must be" + " non-null when ForeignKeyDeleteAction is NULLIFY"); } if (secConfig.getForeignKeyNullifier() != null && secConfig.getMultiKeyCreator() != null) { throw new IllegalArgumentException("ForeignKeyNullifier may not be used with" + " SecondaryMultiKeyCreator -- use" + " ForeignMultiKeyNullifier instead"); } if (secConfig.getForeignKeyDatabase() != null) { Database foreignDb=secConfig.getForeignKeyDatabase(); if (foreignDb.getDatabaseImpl().getSortedDuplicates()) { throw new IllegalArgumentException("Duplicates must not be allowed for a foreign key " + " database: " + foreignDb.getDebugName()); } } primaryDb=primaryDatabase; secondaryTrigger=new SecondaryTrigger(this); if (secConfig.getForeignKeyDatabase() != null) { foreignKeyTrigger=new ForeignKeyTrigger(this); } } /** * Create a database, called by Environment */ void initNew( Environment env, Locker locker, String databaseName, DatabaseConfig dbConfig) throws DatabaseException { super.initNew(env,locker,databaseName,dbConfig); init(locker); } /** * Open a database, called by Environment */ void initExisting( Environment env, Locker locker, DatabaseImpl database, DatabaseConfig dbConfig) throws DatabaseException { Database otherPriDb=database.findPrimaryDatabase(); if (otherPriDb != null && otherPriDb.getDatabaseImpl() != primaryDb.getDatabaseImpl()) { throw new IllegalArgumentException("Secondary is already associated with a different primary: " + otherPriDb.getDebugName()); } super.initExisting(env,locker,database,dbConfig); init(locker); } /** * Adds secondary to primary's list, and populates the secondary if needed. */ private void init( Locker locker) throws DatabaseException { secondaryConfig=(SecondaryConfig)configuration; primaryDb.addTrigger(secondaryTrigger,false); Database foreignDb=secondaryConfig.getForeignKeyDatabase(); if (foreignDb != null) { foreignDb.addTrigger(foreignKeyTrigger,true); } if (secondaryConfig.getAllowPopulate()) { Cursor secCursor=null; Cursor priCursor=null; try { secCursor=new Cursor(this,locker,null); DatabaseEntry key=new DatabaseEntry(); DatabaseEntry data=new DatabaseEntry(); OperationStatus status=secCursor.position(key,data,LockMode.DEFAULT,true); if (status == OperationStatus.NOTFOUND) { priCursor=new Cursor(primaryDb,locker,null); status=priCursor.position(key,data,LockMode.DEFAULT,true); while (status == OperationStatus.SUCCESS) { updateSecondary(locker,secCursor,key,null,data); status=priCursor.retrieveNext(key,data,LockMode.DEFAULT,GetMode.NEXT); } } } finally { if (secCursor != null) { secCursor.close(); } if (priCursor != null) { priCursor.close(); } } } } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public synchronized void close() throws DatabaseException { if (primaryDb != null && secondaryTrigger != null) { primaryDb.removeTrigger(secondaryTrigger); } Database foreignDb=secondaryConfig.getForeignKeyDatabase(); if (foreignDb != null && foreignKeyTrigger != null) { foreignDb.removeTrigger(foreignKeyTrigger); } super.close(); } /** * Should be called by the secondaryTrigger while holding a write lock on * the trigger list. */ void clearPrimary(){ primaryDb=null; secondaryTrigger=null; } /** * Should be called by the foreignKeyTrigger while holding a write lock on * the trigger list. */ void clearForeignKeyTrigger(){ foreignKeyTrigger=null; } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public Database getPrimaryDatabase() throws DatabaseException { return primaryDb; } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public SecondaryConfig getSecondaryConfig() throws DatabaseException { return (SecondaryConfig)getConfig(); } /** * Returns the secondary config without cloning, for internal use. */ public SecondaryConfig getPrivateSecondaryConfig(){ return secondaryConfig; } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public SecondaryCursor openSecondaryCursor( Transaction txn, CursorConfig cursorConfig) throws DatabaseException { return (SecondaryCursor)openCursor(txn,cursorConfig); } /** * Overrides Database method. */ Cursor newDbcInstance( Transaction txn, CursorConfig cursorConfig) throws DatabaseException { return new SecondaryCursor(this,txn,cursorConfig); } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public OperationStatus delete( Transaction txn, DatabaseEntry key) throws DatabaseException { checkEnv(); DatabaseUtil.checkForNullDbt(key,"key",true); checkRequiredDbState(OPEN,"Can't call SecondaryDatabase.delete:"); this.hook79(txn,key); Locker locker=null; Cursor cursor=null; OperationStatus commitStatus=OperationStatus.NOTFOUND; try { locker=LockerFactory.getWritableLocker(envHandle,txn,isTransactional()); cursor=new Cursor(this,locker,null); DatabaseEntry pKey=new DatabaseEntry(); OperationStatus searchStatus=cursor.search(key,pKey,LockMode.RMW,SearchMode.SET); if (searchStatus == OperationStatus.SUCCESS) { commitStatus=primaryDb.deleteInternal(locker,pKey); } return commitStatus; } finally { if (cursor != null) { cursor.close(); } if (locker != null) { locker.operationEnd(commitStatus); } } } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public OperationStatus get( Transaction txn, DatabaseEntry key, DatabaseEntry data, LockMode lockMode) throws DatabaseException { return get(txn,key,new DatabaseEntry(),data,lockMode); } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public OperationStatus get( Transaction txn, DatabaseEntry key, DatabaseEntry pKey, DatabaseEntry data, LockMode lockMode) throws DatabaseException { checkEnv(); DatabaseUtil.checkForNullDbt(key,"key",true); DatabaseUtil.checkForNullDbt(pKey,"pKey",false); DatabaseUtil.checkForNullDbt(data,"data",false); checkRequiredDbState(OPEN,"Can't call SecondaryDatabase.get:"); this.hook80(txn,key,lockMode); CursorConfig cursorConfig=CursorConfig.DEFAULT; if (lockMode == LockMode.READ_COMMITTED) { cursorConfig=CursorConfig.READ_COMMITTED; lockMode=null; } SecondaryCursor cursor=null; try { cursor=new SecondaryCursor(this,txn,cursorConfig); return cursor.search(key,pKey,data,lockMode,SearchMode.SET); } finally { if (cursor != null) { cursor.close(); } } } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public OperationStatus getSearchBoth( Transaction txn, DatabaseEntry key, DatabaseEntry data, LockMode lockMode) throws DatabaseException { throw notAllowedException(); } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public OperationStatus getSearchBoth( Transaction txn, DatabaseEntry key, DatabaseEntry pKey, DatabaseEntry data, LockMode lockMode) throws DatabaseException { checkEnv(); DatabaseUtil.checkForNullDbt(key,"key",true); DatabaseUtil.checkForNullDbt(pKey,"pKey",true); DatabaseUtil.checkForNullDbt(data,"data",false); checkRequiredDbState(OPEN,"Can't call SecondaryDatabase.getSearchBoth:"); this.hook81(txn,key,data,lockMode); CursorConfig cursorConfig=CursorConfig.DEFAULT; if (lockMode == LockMode.READ_COMMITTED) { cursorConfig=CursorConfig.READ_COMMITTED; lockMode=null; } SecondaryCursor cursor=null; try { cursor=new SecondaryCursor(this,txn,cursorConfig); return cursor.search(key,pKey,data,lockMode,SearchMode.BOTH); } finally { if (cursor != null) { cursor.close(); } } } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public OperationStatus put( Transaction txn, DatabaseEntry key, DatabaseEntry data) throws DatabaseException { throw notAllowedException(); } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public OperationStatus putNoOverwrite( Transaction txn, DatabaseEntry key, DatabaseEntry data) throws DatabaseException { throw notAllowedException(); } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public OperationStatus putNoDupData( Transaction txn, DatabaseEntry key, DatabaseEntry data) throws DatabaseException { throw notAllowedException(); } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. */ public JoinCursor join( Cursor[] cursors, JoinConfig config) throws DatabaseException { throw notAllowedException(); } /** * Javadoc for this public method is generated via * the doc templates in the doc_src directory. * @deprecated */ public int truncate( Transaction txn, boolean countRecords) throws DatabaseException { throw notAllowedException(); } /** * Updates a single secondary when a put() or delete() is performed on * the primary. * @param locker the internal locker. * @param cursor secondary cursor to use, or null if this method should * open and close a cursor if one is needed. * @param priKey the primary key. * @param oldData the primary data before the change, or null if the record * did not previously exist. * @param newData the primary data after the change, or null if the record * has been deleted. */ void updateSecondary( Locker locker, Cursor cursor, DatabaseEntry priKey, DatabaseEntry oldData, DatabaseEntry newData) throws DatabaseException { if (secondaryConfig.getImmutableSecondaryKey() && oldData != null && newData != null) { return; } SecondaryKeyCreator keyCreator=secondaryConfig.getKeyCreator(); if (keyCreator != null) { assert secondaryConfig.getMultiKeyCreator() == null; DatabaseEntry oldSecKey=null; if (oldData != null) { oldSecKey=new DatabaseEntry(); if (!keyCreator.createSecondaryKey(this,priKey,oldData,oldSecKey)) { oldSecKey=null; } } DatabaseEntry newSecKey=null; if (newData != null) { newSecKey=new DatabaseEntry(); if (!keyCreator.createSecondaryKey(this,priKey,newData,newSecKey)) { newSecKey=null; } } if ((oldSecKey != null && !oldSecKey.equals(newSecKey)) || (newSecKey != null && !newSecKey.equals(oldSecKey))) { boolean localCursor=(cursor == null); if (localCursor) { cursor=new Cursor(this,locker,null); } try { if (oldSecKey != null) { deleteKey(cursor,priKey,oldSecKey); } if (newSecKey != null) { insertKey(locker,cursor,priKey,newSecKey); } } finally { if (localCursor && cursor != null) { cursor.close(); } } } } else { SecondaryMultiKeyCreator multiKeyCreator=secondaryConfig.getMultiKeyCreator(); assert multiKeyCreator != null; Set oldKeys=Collections.EMPTY_SET; Set newKeys=Collections.EMPTY_SET; if (oldData != null) { oldKeys=new HashSet(); multiKeyCreator.createSecondaryKeys(this,priKey,oldData,oldKeys); } if (newData != null) { newKeys=new HashSet(); multiKeyCreator.createSecondaryKeys(this,priKey,newData,newKeys); } if (!oldKeys.equals(newKeys)) { boolean localCursor=(cursor == null); if (localCursor) { cursor=new Cursor(this,locker,null); } try { Set oldKeysCopy=oldKeys; if (oldKeys != Collections.EMPTY_SET) { oldKeysCopy=new HashSet(oldKeys); oldKeys.removeAll(newKeys); for (Iterator i=oldKeys.iterator(); i.hasNext(); ) { DatabaseEntry oldKey=(DatabaseEntry)i.next(); deleteKey(cursor,priKey,oldKey); } } if (newKeys != Collections.EMPTY_SET) { newKeys.removeAll(oldKeysCopy); for (Iterator i=newKeys.iterator(); i.hasNext(); ) { DatabaseEntry newKey=(DatabaseEntry)i.next(); insertKey(locker,cursor,priKey,newKey); } } } finally { if (localCursor && cursor != null) { cursor.close(); } } } } } /** * Deletes an old secondary key. */ private void deleteKey( Cursor cursor, DatabaseEntry priKey, DatabaseEntry oldSecKey) throws DatabaseException { OperationStatus status=cursor.search(oldSecKey,priKey,LockMode.RMW,SearchMode.BOTH); if (status == OperationStatus.SUCCESS) { cursor.deleteInternal(); } else { throw new DatabaseException("Secondary " + getDebugName() + " is corrupt: the primary record contains a key"+ " that is not present in the secondary"); } } /** * Inserts a new secondary key. */ private void insertKey( Locker locker, Cursor cursor, DatabaseEntry priKey, DatabaseEntry newSecKey) throws DatabaseException { Database foreignDb=secondaryConfig.getForeignKeyDatabase(); if (foreignDb != null) { Cursor foreignCursor=null; try { foreignCursor=new Cursor(foreignDb,locker,null); DatabaseEntry tmpData=new DatabaseEntry(); OperationStatus status=foreignCursor.search(newSecKey,tmpData,LockMode.DEFAULT,SearchMode.SET); if (status != OperationStatus.SUCCESS) { throw new DatabaseException("Secondary " + getDebugName() + " foreign key not allowed: it is not"+ " present in the foreign database"); } } finally { if (foreignCursor != null) { foreignCursor.close(); } } } OperationStatus status; if (configuration.getSortedDuplicates()) { status=cursor.putInternal(newSecKey,priKey,PutMode.NODUP); } else { status=cursor.putInternal(newSecKey,priKey,PutMode.NOOVERWRITE); } if (status != OperationStatus.SUCCESS) { throw new DatabaseException("Could not insert secondary key in " + getDebugName() + ' '+ status); } } /** * Called by the ForeignKeyTrigger when a record in the foreign database is * deleted. * @param secKey is the primary key of the foreign database, which is the * secondary key (ordinary key) of this secondary database. */ void onForeignKeyDelete( Locker locker, DatabaseEntry secKey) throws DatabaseException { ForeignKeyDeleteAction deleteAction=secondaryConfig.getForeignKeyDeleteAction(); LockMode lockMode=(deleteAction == ForeignKeyDeleteAction.ABORT) ? LockMode.DEFAULT : LockMode.RMW; DatabaseEntry priKey=new DatabaseEntry(); Cursor cursor=null; OperationStatus status; try { cursor=new Cursor(this,locker,null); status=cursor.search(secKey,priKey,lockMode,SearchMode.SET); while (status == OperationStatus.SUCCESS) { if (deleteAction == ForeignKeyDeleteAction.ABORT) { throw new DatabaseException("Secondary " + getDebugName() + " refers to a foreign key that has been deleted"+ " (ForeignKeyDeleteAction.ABORT)"); } else if (deleteAction == ForeignKeyDeleteAction.CASCADE) { Cursor priCursor=null; try { DatabaseEntry data=new DatabaseEntry(); priCursor=new Cursor(primaryDb,locker,null); status=priCursor.search(priKey,data,LockMode.RMW,SearchMode.SET); if (status == OperationStatus.SUCCESS) { priCursor.delete(); } else { throw secondaryCorruptException(); } } finally { if (priCursor != null) { priCursor.close(); } } } else if (deleteAction == ForeignKeyDeleteAction.NULLIFY) { Cursor priCursor=null; try { DatabaseEntry data=new DatabaseEntry(); priCursor=new Cursor(primaryDb,locker,null); status=priCursor.search(priKey,data,LockMode.RMW,SearchMode.SET); if (status == OperationStatus.SUCCESS) { ForeignMultiKeyNullifier multiNullifier=secondaryConfig.getForeignMultiKeyNullifier(); if (multiNullifier != null) { if (multiNullifier.nullifyForeignKey(this,priKey,data,secKey)) { priCursor.putCurrent(data); } } else { ForeignKeyNullifier nullifier=secondaryConfig.getForeignKeyNullifier(); if (nullifier.nullifyForeignKey(this,data)) { priCursor.putCurrent(data); } } } else { throw secondaryCorruptException(); } } finally { if (priCursor != null) { priCursor.close(); } } } else { throw new IllegalStateException(); } status=cursor.retrieveNext(secKey,priKey,LockMode.DEFAULT,GetMode.NEXT_DUP); } } finally { if (cursor != null) { cursor.close(); } } } DatabaseException secondaryCorruptException() throws DatabaseException { throw new DatabaseException("Secondary " + getDebugName() + " is corrupt: it refers"+ " to a missing key in the primary database"); } static UnsupportedOperationException notAllowedException(){ throw new UnsupportedOperationException("Operation not allowed on a secondary"); } protected void hook79( Transaction txn, DatabaseEntry key) throws DatabaseException { } protected void hook80( Transaction txn, DatabaseEntry key, LockMode lockMode) throws DatabaseException { } protected void hook81( Transaction txn, DatabaseEntry key, DatabaseEntry data, LockMode lockMode) throws DatabaseException { } }