/**
*
* Copyright 2004 Protique Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
**/
package org.activemq.store.bdb;
import java.io.IOException;
import javax.jms.JMSException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.activemq.AlreadyClosedException;
import org.activemq.io.WireFormat;
import org.activemq.message.ActiveMQMessage;
import org.activemq.message.MessageAck;
import org.activemq.service.MessageContainer;
import org.activemq.service.MessageIdentity;
import org.activemq.store.MessageStore;
import org.activemq.store.RecoveryListener;
import org.activemq.util.JMSExceptionHelper;
import com.sleepycat.je.CursorConfig;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.je.SecondaryConfig;
import com.sleepycat.je.SecondaryCursor;
import com.sleepycat.je.SecondaryDatabase;
import com.sleepycat.je.Transaction;
/**
* @version $Revision: 1.1 $
*/
public class BDbMessageStore implements MessageStore {
private static final Log log = LogFactory.getLog(BDbMessageStore.class);
private Database database;
private WireFormat wireFormat;
private SecondaryDatabase secondaryDatabase;
private SecondaryConfig secondaryConfig;
private SequenceNumberCreator sequenceNumberCreator;
private MessageContainer container;
private CursorConfig cursorConfig;
public BDbMessageStore(Database database, SecondaryDatabase secondaryDatabase, SecondaryConfig secondaryConfig, SequenceNumberCreator sequenceNumberCreator, WireFormat wireFormat) {
this.database = database;
this.secondaryDatabase = secondaryDatabase;
this.secondaryConfig = secondaryConfig;
this.sequenceNumberCreator = sequenceNumberCreator;
this.wireFormat = wireFormat;
}
public void setMessageContainer(MessageContainer container) {
this.container = container;
}
public void addMessage(ActiveMQMessage message) throws JMSException {
checkClosed();
String messageID = message.getJMSMessageID();
try {
Transaction transaction = BDbHelper.getTransaction();
DatabaseEntry key = createKey(messageID);
DatabaseEntry value = new DatabaseEntry(asBytes(message));
database.put(transaction, key, value);
MessageIdentity answer = message.getJMSMessageIdentity();
answer.setSequenceNumber(sequenceNumberCreator.getLastKey());
}
catch (DatabaseException e) {
throw JMSExceptionHelper.newJMSException("Failed to broker message: " + messageID + " in container: " + e, e);
}
catch (IOException e) {
throw JMSExceptionHelper.newJMSException("Failed to broker message: " + messageID + " in container: " + e, e);
}
}
public ActiveMQMessage getMessage(MessageIdentity identity) throws JMSException {
checkClosed();
ActiveMQMessage answer = null;
String messageID = identity.getMessageID();
try {
DatabaseEntry key = createKey(messageID);
DatabaseEntry value = new DatabaseEntry();
if (database.get(null, key, value, null) == OperationStatus.SUCCESS) {
answer = extractMessage(value);
}
return answer;
}
catch (DatabaseException e) {
throw JMSExceptionHelper.newJMSException("Failed to peek next message after: " + messageID + " from container: " + e, e);
}
catch (IOException e) {
throw JMSExceptionHelper.newJMSException("Failed to broker message: " + messageID + " in container: " + e, e);
}
}
public void removeMessage(MessageAck ack) throws JMSException {
checkClosed();
MessageIdentity identity = ack.getMessageIdentity();
String messageID = identity.getMessageID();
try {
Transaction transaction = BDbHelper.getTransaction();
// we need to find the alternative primary key for the given messageID
DatabaseEntry sequenceNumber = getSequenceNumberKey(identity);
//System.out.println("Deleting sequenceNumber: " + BDbHelper.longFromBytes(sequenceNumber.getData()));
sequenceNumberCreator.setDeleteKey(sequenceNumber);
OperationStatus status = secondaryDatabase.delete(transaction, sequenceNumber);
if (status != OperationStatus.SUCCESS) {
log.error("Could not delete sequenece number for: " + identity + " status: " + status);
}
}
catch (DatabaseException e) {
throw JMSExceptionHelper.newJMSException("Failed to delete message: " + messageID + " from container: " + e, e);
}
}
public void recover(RecoveryListener listener) throws JMSException {
checkClosed();
SecondaryCursor cursor = null;
try {
cursor = secondaryDatabase.openSecondaryCursor(BDbHelper.getTransaction(), cursorConfig);
DatabaseEntry sequenceNumberEntry = new DatabaseEntry();
DatabaseEntry keyEntry = new DatabaseEntry();
DatabaseEntry valueEntry = new DatabaseEntry();
OperationStatus status = cursor.getFirst(sequenceNumberEntry, keyEntry, valueEntry, LockMode.DEFAULT);
while (status == OperationStatus.SUCCESS) {
String messageID = extractString(keyEntry);
listener.recoverMessage(new MessageIdentity(messageID, sequenceNumberEntry));
status = cursor.getNext(sequenceNumberEntry, keyEntry, valueEntry, LockMode.DEFAULT);
}
if (status != OperationStatus.NOTFOUND) {
log.warn("Unexpected status code while recovering: " + status);
}
}
catch (DatabaseException e) {
throw JMSExceptionHelper.newJMSException("Failed to recover container. Reason: " + e, e);
}
finally {
if (cursor != null) {
try {
cursor.close();
}
catch (DatabaseException e) {
log.warn("Caught exception closing cursor: " + e, e);
}
}
}
}
public void start() throws JMSException {
}
public void stop() throws JMSException {
JMSException firstException = BDbPersistenceAdapter.closeDatabase(secondaryDatabase, null);
firstException = BDbPersistenceAdapter.closeDatabase(database, firstException);
secondaryDatabase = null;
database = null;
if (firstException != null) {
throw firstException;
}
}
// Implementation methods
//-------------------------------------------------------------------------
protected SecondaryDatabase getSecondaryDatabase() {
return secondaryDatabase;
}
protected Database getDatabase() {
return database;
}
public CursorConfig getCursorConfig() {
return cursorConfig;
}
public MessageContainer getContainer() {
return container;
}
protected void checkClosed() throws AlreadyClosedException {
if (database == null) {
throw new AlreadyClosedException("Berkeley DB MessageStore");
}
}
/**
* Returns the sequence number key for the given message identity. If the
* sequence number is not available it will be queried (which is slow & will generate a warning
* as it is not recommended) and then it'll be cached inside the MessageIdentity
*
* @param identity
* @return
* @throws DatabaseException
*/
protected DatabaseEntry getSequenceNumberKey(MessageIdentity identity) throws DatabaseException {
DatabaseEntry sequenceNumber = (DatabaseEntry) identity.getSequenceNumber();
if (sequenceNumber == null) {
sequenceNumber = findSequenceNumber(identity.getMessageID());
}
return sequenceNumber;
}
protected DatabaseEntry createKey(String messageID) {
DatabaseEntry key = new DatabaseEntry(asBytes(messageID));
return key;
}
/**
* Iterates through from the start of the collection until the given message ID is found
*
* @param messageID
* @return
*/
protected DatabaseEntry findSequenceNumber(String messageID) throws DatabaseException {
log.warn("Having to table scan to find the sequence number for messageID: " + messageID);
SecondaryCursor cursor = null;
try {
cursor = secondaryDatabase.openSecondaryCursor(BDbHelper.getTransaction(), cursorConfig);
DatabaseEntry sequenceNumberEntry = new DatabaseEntry();
DatabaseEntry keyEntry = new DatabaseEntry();
DatabaseEntry valueEntry = new DatabaseEntry();
OperationStatus status = cursor.getFirst(sequenceNumberEntry, keyEntry, valueEntry, LockMode.DEFAULT);
while (status == OperationStatus.SUCCESS) {
String value = extractString(keyEntry);
if (messageID.equals(value)) {
return sequenceNumberEntry;
}
status = cursor.getNext(sequenceNumberEntry, keyEntry, valueEntry, LockMode.DEFAULT);
}
}
finally {
if (cursor != null) {
try {
cursor.close();
}
catch (DatabaseException e) {
log.warn("Caught exception closing cursor: " + e, e);
}
}
}
return null;
}
protected String extractString(DatabaseEntry entry) {
return new String(entry.getData(), entry.getOffset(), entry.getSize());
}
protected ActiveMQMessage extractMessage(DatabaseEntry value) throws IOException {
// we must synchronize access to wireFormat
synchronized (wireFormat) {
return (ActiveMQMessage) wireFormat.fromBytes(value.getData(), value.getOffset(), value.getSize());
}
}
protected byte[] asBytes(ActiveMQMessage message) throws IOException, JMSException {
// we must synchronize access to wireFormat
synchronized (wireFormat) {
return wireFormat.toBytes(message);
}
}
protected byte[] asBytes(String messageID) {
return messageID.getBytes();
}
/**
* @see org.activemq.store.MessageStore#removeAllMessages()
*/
public void removeAllMessages() throws JMSException {
//TODO: implement me.
}
}