/*
* JBoss, Home of Professional Open Source
* Copyright 2006, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.internal.soa.esb.couriers;
import org.apache.log4j.Logger;
import org.jboss.internal.soa.esb.couriers.helpers.JDBCEprDBResourceFactory;
import org.jboss.internal.soa.esb.util.StreamUtils;
import org.jboss.soa.esb.addressing.Call;
import org.jboss.soa.esb.addressing.eprs.JDBCEpr;
import org.jboss.soa.esb.common.*;
import org.jboss.soa.esb.couriers.*;
import org.jboss.soa.esb.message.Message;
import org.jboss.soa.esb.util.Util;
import org.xml.sax.SAXException;
import org.jboss.soa.esb.client.ServiceInvoker;
import org.jboss.soa.esb.listeners.message.MessageDeliverException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.io.StringReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.sql.*;
import java.util.UUID;
import javax.xml.parsers.ParserConfigurationException;
public class SqlTableCourier implements PickUpOnlyCourier, DeliverOnlyCourier
{
public static final String SQL_RETRY_COUNT = "org.jboss.soa.esb.sql.retry.count";
protected long _pollLatency = 200;
protected long _sleepForRetries = 3000; // milliseconds
protected boolean deleteOnSuccess, deleteOnError;
protected boolean _isReceiver;
private boolean monitoringRetryCount;
private int messageType = Types.OTHER ;
private JDBCEprDBResourceFactory jdbcFactory;
protected static final Logger _logger = Logger.getLogger(SqlTableCourier.class);
/**
* Dead letter channel ServiceInvoker. Messages are delivered to the DLQ after the retry limit for a
* failed message has been exceeded.
*/
private static ServiceInvoker dlQueueInvoker;
/**
* Redelivery retry limit.
*/
private static int retryLimit;
static {
String retryLimitConfig = ModulePropertyManager.getPropertyManager(ModulePropertyManager.TRANSPORTS_MODULE).getProperty(Environment.SQL_RETRY_LIMIT, "5").trim();
try {
retryLimit = Integer.parseInt(retryLimitConfig);
} catch (NumberFormatException e) {
retryLimit = 5;
}
}
/**
* package protected constructor - Objects of Courier should only be
* instantiated by the Factory
*
* @param epr
*/
SqlTableCourier(JDBCEpr epr) throws CourierException
{
this(epr, false);
}
/**
* package protected constructor - Objects of Courier should only be
* instantiated by the Factory
*
* @param epr
*/
SqlTableCourier(JDBCEpr epr, boolean isReceiver) throws CourierException
{
_isReceiver = isReceiver;
_sleepForRetries = 3000; // TODO magic number - configurable?
deleteOnSuccess = Boolean.TRUE.equals(Boolean.valueOf(epr
.getPostDelete()));
deleteOnError = Boolean.TRUE.equals(Boolean.valueOf(epr
.getErrorDelete()));
jdbcFactory = new JDBCEprDBResourceFactory(epr);
monitoringRetryCount = (epr.getRetryCountColumn() != null);
}
public void cleanup() {
}
/**
* package the ESB message in a java.io.Serializable, and write it.
* Delivery occurs within its own transaction if there is no
* global transaction active.
*
* @param message
* Message - the message to deliverAsync
* @return boolean - the result of the delivery
* @throws CourierException -
* if problems were encountered
*/
public boolean deliver(Message message) throws CourierException
{
if (_isReceiver)
throw new CourierException("This is a read-only Courier");
if (null == message)
return false;
String msgId;
Call call = message.getHeader().getCall();
if (null==call)
message.getHeader().setCall(call=new Call());
try
{
if (null==call.getMessageID())
call.setMessageID(new URI(UUID.randomUUID().toString()));
msgId = call.getMessageID().toString();
}
catch (URISyntaxException e)
{
throw new CourierException("Problems with message header ",e);
}
boolean transactional = isTransactional();
Serializable serializedMessage;
try {
serializedMessage = Util.serialize(message);
} catch (Exception e) {
throw new CourierTransportException("Unable to serialize ESB Message.", e);
}
Connection connection = jdbcFactory.createConnection(transactional);
try
{
PreparedStatement insertStatement = jdbcFactory.createInsertStatement(connection);
try {
insertStatement.setString(1, msgId);
final int type = getMessageType(connection) ;
switch(type)
{
case Types.BLOB:
final byte[] blobData = serializedMessage.toString().getBytes() ;
final ByteArrayInputStream bais = new ByteArrayInputStream(blobData) ;
insertStatement.setBinaryStream(2, bais, blobData.length) ;
break ;
case Types.BINARY:
case Types.VARBINARY:
case Types.LONGVARBINARY:
final byte[] data = serializedMessage.toString().getBytes() ;
insertStatement.setBytes(2, data) ;
break ;
case Types.CLOB:
final String clobData = serializedMessage.toString() ;
final StringReader clobReader = new StringReader(clobData) ;
insertStatement.setCharacterStream(2, clobReader, clobData.length()) ;
break ;
case Types.CHAR:
case Types.VARCHAR:
case Types.LONGVARCHAR:
insertStatement.setString(2, serializedMessage.toString()) ;
break ;
default:
insertStatement.setObject(2, serializedMessage);
}
insertStatement.setString(3, State.Pending.getColumnValue());
insertStatement.setLong(4, System.currentTimeMillis());
insertStatement.executeUpdate();
} finally {
insertStatement.close();
}
if (!transactional) {
connection.commit();
}
return true;
}
catch (SQLException e)
{
try
{
if (!transactional) {
connection.rollback();
}
}
catch (Exception roll)
{
_logger.debug(roll);
}
_logger.debug("SQL exception during deliver", e);
throw new CourierTransportException(e);
} finally {
try {
if (!transactional) {
connection.close();
}
} catch (SQLException e) {
_logger.error("Exception while closing DataSource connection.", e);
}
}
}
public Message pickup(long millis) throws CourierException, CourierTimeoutException
{
Message result = null;
long limit = System.currentTimeMillis()
+ ((millis < 100) ? 100 : millis);
do
{
boolean transactional = isTransactional();
MessagePickupProspect pickupProspect;
try {
pickupProspect = getPickupProspect(transactional);
} catch (Exception e) {
_logger.warn("Exception while attempting to lookup message pickup prospect.", e);
return null;
}
if(pickupProspect != null) {
Connection connection = jdbcFactory.createConnection(transactional);
try {
result = tryToPickup(pickupProspect, connection);
// We've successfully picked up a message, so we can commit on a
// non-transacted connection...
if (!transactional) {
connection.commit();
}
if (result != null) {
if(transactional && monitoringRetryCount && pickupProspect.sendToDQL) {
// Not going to deliver this message to the action pipeline
// because it has already failed (been retried) a number of
// time. Sending to the DLQ...
deliverToDLQ(result);
return null;
} else {
return result;
}
}
} catch (FaultMessageException e) {
// The picked up message was a fault, generating this exception
// in Factory.createExceptionFromFault. Just rethrow...
throw e;
} catch (Exception e) {
_logger.warn("Exception during pickup", e);
if (!transactional) {
try {
connection.rollback();
} catch (SQLException e1) {
_logger.warn("SQL Exception during rollback", e);
}
}
throw new CourierTransportException(e);
} finally {
try {
connection.close();
} catch (SQLException e) {
_logger.warn("Error closing DataSource Connection.", e);
}
}
try {
long lSleep = limit - System.currentTimeMillis();
if (_pollLatency < lSleep)
lSleep = _pollLatency;
if (lSleep > 0)
Thread.sleep(lSleep);
}
catch (InterruptedException e) {
return null;
}
}
} while (System.currentTimeMillis() <= limit);
return null;
}
private MessagePickupProspect getPickupProspect(boolean transactional) throws TransactionStrategyException, CourierServiceBindException, SQLException, CourierTransportException {
MessagePickupProspect prospect = null;
// Attempt to read a prospect's details from the DB...
Connection connection = jdbcFactory.createConnection(transactional);
try {
PreparedStatement listStatement = jdbcFactory.createListStatement(connection);
try {
ResultSet resultSet = listStatement.executeQuery();
try {
if (resultSet.next()) {
prospect = new MessagePickupProspect();
prospect.messageId = resultSet.getString(1);
prospect.timestamp = resultSet.getLong(2);
prospect.status = resultSet.getString(3);
if (transactional && monitoringRetryCount) {
prospect.retryCount = resultSet.getInt(4);
}
}
} finally {
resultSet.close();
}
} finally {
listStatement.close();
}
} finally {
connection.close();
}
// If we read a prospect...
if(prospect != null && transactional && monitoringRetryCount) {
if(prospect.retryCount >= retryLimit) {
prospect.sendToDQL = true;
}
prospect = updateRetryCount(prospect);
}
return prospect;
}
private Message tryToPickup(MessagePickupProspect prospect, Connection connection) throws CourierException, SQLException
{
PreparedStatement selectPicukupMessageStatement = jdbcFactory.createSelectPickupMessage(connection);
try {
selectPicukupMessageStatement.setString(1, prospect.messageId);
selectPicukupMessageStatement.setString(2, prospect.status);
selectPicukupMessageStatement.setLong(3, prospect.timestamp);
ResultSet resultSet = selectPicukupMessageStatement.executeQuery();
try
{
if (resultSet.next())
{
Message result = null;
try
{
final Serializable value ;
final int type = getMessageType(resultSet) ;
switch (type)
{
case Types.BLOB:
final Blob blob = resultSet.getBlob(1) ;
final byte[] blobData = ((blob != null) ? StreamUtils.readStream(blob.getBinaryStream()) : null);
if (blobData != null)
value = new String(blobData) ;
else
value = "";
break ;
case Types.BINARY:
case Types.VARBINARY:
case Types.LONGVARBINARY:
final byte[] binaryData = StreamUtils.readStream(resultSet.getBinaryStream(1)) ;
value = new String(binaryData) ;
break ;
case Types.CLOB:
final Clob clob = resultSet.getClob(1) ;
if (clob != null)
value = StreamUtils.readReader(clob.getCharacterStream());
else
value = "";
break ;
case Types.CHAR:
case Types.VARCHAR:
case Types.LONGVARCHAR:
value = resultSet.getString(1) ;
break ;
default:
value = (Serializable) resultSet.getObject(1);
break ;
}
result = Util.deserialize(value);
}
catch (IOException e) {} // ignore
catch (SAXException e) {} // ignore
catch (ParserConfigurationException e) {} // ignore
finally {
if (result == null && deleteOnError) {
deleteMsg(prospect.messageId, connection);
} else if (result != null && deleteOnSuccess) {
deleteMsg(prospect.messageId, connection);
} else if(result == null) {
changeStatus(prospect.messageId, State.Error, connection);
} else {
changeStatus(prospect.messageId, State.Done, connection);
}
}
return result;
}
}
finally
{
try
{
resultSet.close();
} catch (final Exception ex) {
_logger.warn("Could not close ResultSet.", ex);
}
}
} finally {
selectPicukupMessageStatement.close();
}
return null;
}
private MessagePickupProspect updateRetryCount(MessagePickupProspect prospect) throws SQLException, CourierServiceBindException, CourierTransportException, TransactionStrategyException {
TransactionStrategy txStrategy = TransactionStrategy.getTransactionStrategy(true);
Object suspendedTX;
suspendedTX = txStrategy.suspend();
try {
txStrategy.begin();
try {
Connection connection = jdbcFactory.createConnection(true);
boolean updated = false;
try {
PreparedStatement updateStatement = jdbcFactory.createUpdateRetryCountStatement(connection);
try {
long readTimestamp = prospect.timestamp;
long updateTimeout = System.currentTimeMillis();
if(prospect.sendToDQL) {
// Mark status as error...
prospect.status = SqlTableCourier.State.Error.getColumnValue();
}
updateStatement.setInt(1, prospect.retryCount + 1);
updateStatement.setString(2, prospect.status);
updateStatement.setLong(3, updateTimeout);
updateStatement.setString(4, prospect.messageId);
updateStatement.setLong(5, readTimestamp);
// Update the timestamp as the pickup will need this...
prospect.timestamp = updateTimeout;
updated = (updateStatement.executeUpdate() == 1);
} finally {
updateStatement.close();
}
} finally {
connection.close();
}
txStrategy.terminate();
// Only return the prospec if the retry count was successfully updated
// and commited...
if(updated) {
return prospect;
}
} catch (Exception e){
txStrategy.rollbackOnly();
txStrategy.terminate();
_logger.debug("Error updating message retry count: " + e.getMessage());
}
} finally {
txStrategy.resume(suspendedTX);
}
return null;
}
private void deleteMsg(String messageId, Connection connection) throws SQLException
{
PreparedStatement statement = jdbcFactory.createDeleteStatement(connection);
try {
statement.setString(1, messageId);
statement.executeUpdate();
} finally {
statement.close();
}
}
private void changeStatus(String messageId, State to, Connection connection) throws SQLException
{
PreparedStatement statement = jdbcFactory.createUpdateStatusStatement(connection);
try {
statement.setString(1, to.getColumnValue());
statement.setString(2, messageId);
statement.executeUpdate();
} finally {
statement.close();
}
}
public static enum State
{
Pending, WorkInProgress, Done, Error;
public String getColumnValue()
{
return toString().substring(0, 1);
}
}
public void setPollLatency(Long millis)
{
if (millis <= 200)
_logger.warn("Poll latency must be >= 200 milliseconds - Keeping old value of "+_pollLatency);
else
_pollLatency = millis;
}
private boolean isTransactional() throws CourierException {
boolean transactional;
try
{
TransactionStrategy txStrategy = TransactionStrategy.getTransactionStrategy(true);
Object txHandle = ((txStrategy == null) ? null : txStrategy.getTransaction());
boolean isActive = ((txStrategy == null) ? false : txStrategy.isActive());
transactional = (txHandle != null);
/*
* Make sure the current transaction is still active! If we
* have previously slept, then the timeout may be longer than that
* associated with the transaction.
*/
if (transactional && !isActive)
{
throw new CourierException("Associated transaction is no longer active!");
}
}
catch (TransactionStrategyException ex)
{
throw new CourierException(ex);
}
return transactional;
}
private synchronized int getMessageType(final Connection connection)
throws SQLException
{
if (messageType != Types.OTHER)
{
return messageType ;
}
final PreparedStatement ps = jdbcFactory.createSelect4UpdateStatement(connection) ;
try
{
ps.setString(1, "");
ps.setString(2, State.Pending.getColumnValue());
final ResultSet resultSet = ps.executeQuery();
try
{
return getMessageType(resultSet) ;
}
finally
{
try
{
resultSet.close() ;
}
catch (final Throwable th) {} // ignore
}
}
finally
{
try
{
ps.close() ;
}
catch (final Throwable th) {} // ignore
}
}
private synchronized int getMessageType(final ResultSet resultSet)
throws SQLException
{
if (messageType == Types.OTHER)
{
final ResultSetMetaData metaData = resultSet.getMetaData() ;
messageType = metaData.getColumnType(1) ;
}
return messageType ;
}
/**
* Deliver a message to the Dead Letter Channel Service.
*
* @throws org.jboss.soa.esb.listeners.message.MessageDeliverException Message delivery failure.
*/
protected void deliverToDLQ(Message message) throws MessageDeliverException {
if (!"true".equalsIgnoreCase(Configuration.getRedeliveryDlsOn())) {
_logger.debug("org.jboss.soa.esb.dls.redeliver is turned off");
} else {
synchronized (ServiceInvoker.dlqService) {
if (dlQueueInvoker == null) {
dlQueueInvoker = new ServiceInvoker(ServiceInvoker.dlqService);
}
}
dlQueueInvoker.deliverAsync(message);
}
}
private static class MessagePickupProspect {
private String messageId;
private String status;
private long timestamp;
public int retryCount;
private boolean sendToDQL;
}
}