/*
* 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.transport;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.log4j.Logger;
import org.jboss.internal.soa.esb.couriers.tx.InVMXAResource;
import org.jboss.internal.soa.esb.message.format.MessageSerializer;
import org.jboss.soa.esb.addressing.EPR;
import org.jboss.soa.esb.addressing.eprs.InVMEpr;
import org.jboss.soa.esb.common.TransactionStrategy;
import org.jboss.soa.esb.common.TransactionStrategyException;
import org.jboss.soa.esb.message.ByReferenceMessage;
import org.jboss.soa.esb.message.Message;
import org.jboss.soa.esb.services.registry.ServiceNotFoundException;
/**
* Provide support for the InVM transports and EPRs.
*
* The InVM transport is divided into two distinct types, permanent transports which
* represent ESB aware services and temporary transports which represent default reply
* queues.
*
* This class provides the interface and support for registry queries/permanent transports
* while support for the temporary queues are through the InVMTemporaryTransport class.
*
* @author kevin
*/
public class InVMTransport
{
/**
* The logger for this instance.
*/
private static final Logger LOGGER = Logger.getLogger(InVMTransport.class) ;
/**
* Mapping of service ids to entries.
*/
private final Map<String, InVMEntry> serviceIdToEntry = new HashMap<String, InVMEntry>() ;
/**
* Mapping of category names to service names and entries.
*/
private final Map<String, Map<String, InVMEntry>> categoryToNameToEntry = new HashMap<String, Map<String,InVMEntry>>() ;
/**
* The lock guarding access and modification to the structures.
*/
private ReadWriteLock lock = new ReentrantReadWriteLock() ;
/**
* Factory singleton instance.
*/
private static InVMTransport instance = new InVMTransport();
/**
* Get the InVM Transport.
* @return The InVM Transport instance.
*/
public static InVMTransport getInstance()
{
return instance;
}
/**
* Register the specific EPR against the service category/name.
*
* @param category The service category.
* @param name The service name.
* @param epr The associated InVM EPR.
*
* @throws InVMException for InVM transport specific errors.
*/
public void registerEPR(final String category, final String name, final InVMEpr epr)
throws InVMException
{
if (epr.isTemporaryEPR())
{
throw new InVMException("Attempt to register temporary EPR in permanent registry") ;
}
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Registering EPR " + epr + " for category " + category + ", name " + name) ;
}
final String serviceId = epr.getServiceId() ;
acquireWriteLock() ;
try
{
final InVMEntry existingEntry = serviceIdToEntry.get(serviceId) ;
Map<String, InVMEntry> nameToEntry = categoryToNameToEntry.get(category) ;
if (nameToEntry == null)
{
if (existingEntry != null)
{
throw new InVMException("Service " + serviceId + " registered under a multiple categories") ;
}
nameToEntry = new HashMap<String, InVMEntry>() ;
categoryToNameToEntry.put(category, nameToEntry) ;
}
InVMEntry entry = nameToEntry.get(name) ;
if (entry == null)
{
if (existingEntry != null)
{
throw new InVMException("Service " + serviceId + " registered under a multiple names") ;
}
entry = new InVMEntry(serviceId) ;
nameToEntry.put(name, entry) ;
}
else if ((existingEntry != null) && (existingEntry != entry))
{
throw new InVMException("Service " + serviceId + " registered under a multiple names") ;
}
if (entry.addEPR(epr))
{
serviceIdToEntry.put(serviceId, entry) ;
}
}
finally
{
releaseWriteLock() ;
}
}
/**
* Unregister the service category/name.
*
* @param category The service category.
* @param name The service name.
* @return true if the service was removed, false otherwise
*/
public boolean unRegisterService(final String category, final String name)
{
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Unregistering service category " + category + ", name " + name) ;
}
acquireWriteLock() ;
try
{
Map<String, InVMEntry> nameToEntry = categoryToNameToEntry.get(category) ;
if (nameToEntry == null)
{
return false ;
}
InVMEntry entry = nameToEntry.remove(name) ;
if (entry == null)
{
return false ;
}
entry.shutdown() ;
if (nameToEntry.isEmpty())
{
categoryToNameToEntry.remove(category) ;
}
serviceIdToEntry.remove(entry.getServiceId()) ;
return true ;
}
finally
{
releaseWriteLock() ;
}
}
/**
* Unregister the specific EPR from the service category/name.
*
* @param category The service category.
* @param name The service name.
* @param epr The associated InVM EPR.
*
* @throws ServiceNotFoundException if the service/EPR combination is not found.
*/
public void unRegisterEPR(final String category, final String name, final InVMEpr epr)
throws ServiceNotFoundException
{
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Unregistering EPR " + epr + " for category " + category + ", name " + name) ;
}
acquireWriteLock() ;
try
{
Map<String, InVMEntry> nameToEntry = categoryToNameToEntry.get(category) ;
if (nameToEntry == null)
{
throw new ServiceNotFoundException("Could not locate any services for category " + category) ;
}
InVMEntry entry = nameToEntry.get(name) ;
if (entry == null)
{
throw new ServiceNotFoundException("Could not locate service " + category + ", " + name) ;
}
if (entry.removeEPR(epr))
{
nameToEntry.remove(name) ;
entry.shutdown() ;
if (nameToEntry.isEmpty())
{
categoryToNameToEntry.remove(category) ;
}
serviceIdToEntry.remove(entry.getServiceId()) ;
}
}
finally
{
releaseWriteLock() ;
}
}
/**
* Retrieve a list of all known services.
* @return The known services.
*/
public List<String> findAllServices()
{
final List<String> result = new ArrayList<String>() ;
acquireReadLock() ;
try
{
for(Map<String, InVMEntry> nameToEntry: categoryToNameToEntry.values())
{
result.addAll(nameToEntry.keySet()) ;
}
}
finally
{
releaseReadLock() ;
}
return result ;
}
/**
* Retrieve a list of all known services in a specified category.
* @param category The category to query.
* @return The known services.
*/
public List<String> findServices(final String category)
{
final List<String> result = new ArrayList<String>() ;
acquireReadLock() ;
try
{
final Map<String, InVMEntry> nameToEntry = categoryToNameToEntry.get(category) ;
if (nameToEntry != null)
{
result.addAll(nameToEntry.keySet()) ;
}
}
finally
{
releaseReadLock() ;
}
return result ;
}
/**
* Retrieve a list of EPRs for the specified category and name.
* @param category The service category to query.
* @param name The service name to query.
* @return The known EPRs.
*/
public List<EPR> findEPRs(final String category, final String name)
{
final List<EPR> result = new ArrayList<EPR>() ;
acquireReadLock() ;
try
{
final Map<String, InVMEntry> nameToEntry = categoryToNameToEntry.get(category) ;
if (nameToEntry != null)
{
final InVMEntry entry = nameToEntry.get(name) ;
if (entry != null)
{
result.addAll(entry.getEPRs()) ;
}
}
}
finally
{
releaseReadLock() ;
}
return result ;
}
/**
* Retrieve an EPR for the specified category and name.
* @param category The service category to query.
* @param name The service name to query.
* @return An EPR for the specified category and name or null if not registered.
*/
public EPR findEPR(final String category, final String name)
{
acquireReadLock() ;
try
{
final Map<String, InVMEntry> nameToEntry = categoryToNameToEntry.get(category) ;
if (nameToEntry != null)
{
final InVMEntry entry = nameToEntry.get(name) ;
if (entry != null)
{
return entry.getEPRs().get(0) ;
}
}
}
finally
{
releaseReadLock() ;
}
return null;
}
/**
* Deliver a message to the specified EPR.
* @param inVMEpr The EPR to receive the message.
* @param message The message to deliver.
* @throws InVMException for InVM transport specific errors.
*/
public void deliver(final InVMEpr inVMEpr, final Message message)
throws InVMException
{
final String serviceId = inVMEpr.getServiceId() ;
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Delivering message to " + serviceId) ;
}
final InVMEntry entry ;
final boolean passByValue ;
final long lockstep ;
acquireReadLock() ;
try
{
entry = serviceIdToEntry.get(serviceId) ;
if (entry == null)
{
throw new InVMException("Could not locate service entry for epr " + inVMEpr) ;
}
passByValue = entry.isPassByValue() ;
lockstep = entry.getLockstep() ;
}
finally
{
releaseReadLock() ;
}
final Object addedObject = toDeliveryObject(message, passByValue);
if (isTransactional())
{
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Delivering transactional message to " + serviceId) ;
}
/*
* Can't do lockstep wait here because otherwise the transaction may not terminate if this
* is the transaction controller thread!
*/
final TransactionStrategy txStrategy = TransactionStrategy.getTransactionStrategy(true) ;
try
{
txStrategy.enlistResource(new InVMXAResource(inVMEpr, addedObject, InVMXAResource.Operation.INSERT));
}
catch (final TransactionStrategyException tse)
{
throw new InVMException("Unexpected error enlisting transaction resource", tse) ;
}
}
else
{
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Delivering message direct to " + serviceId + " queue") ;
}
entry.deliver(addedObject, lockstep) ;
}
}
/**
* Pickup a message for the specified EPR.
* @param inVMEpr The EPR to receive the message.
* @param millis The number of milliseconds to wait for a message.
* @return The message or null if nothing present within the timeout.
* @throws InVMException for InVM transport specific errors.
*/
public Message pickup(final InVMEpr inVMEpr, final long millis)
throws InVMException
{
final String serviceId = inVMEpr.getServiceId() ;
final InVMEntry entry ;
acquireReadLock() ;
try
{
entry = serviceIdToEntry.get(serviceId) ;
}
finally
{
releaseReadLock() ;
}
if (entry == null)
{
throw new InVMException("Could not locate service entry for epr " + inVMEpr) ;
}
final Object msgObject = entry.pickup(millis) ;
if (msgObject != null)
{
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Pickup of message from " + serviceId) ;
}
final Message message = fromDeliveryObject(msgObject, inVMEpr.getPassByValue());
if (isTransactional())
{
/*
* Return the message, but don't remove it from the queue until the transaction
* commits. If the transaction rolls back then the message may not go back into the
* queue at the exact place it was originally: other messages may have been removed
* successfully by other threads. Plus, we would have to maintain a before and after
* image of the queue. This is more a compensation transaction.
*/
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Pickup enlisting transactional resource for service " + serviceId) ;
}
final TransactionStrategy txStrategy = TransactionStrategy.getTransactionStrategy(true) ;
try
{
txStrategy.enlistResource(new InVMXAResource(inVMEpr, msgObject, InVMXAResource.Operation.REMOVE)) ;
}
catch (final TransactionStrategyException tse)
{
throw new InVMException("Unexpected error enlisting transaction resource", tse) ;
}
}
return message ;
}
return null ;
}
/**
* Deliver an object as a consequence of a transaction. This will either be a
* rollback, placing the object back on the source queue, or a commit delivering
* to a target queue.
*
* @param inVMEpr The EPR to receive the message.
* @param msgObject The object to deliver.
* @throws InVMException for InVM transport specific errors.
*/
public void deliverTx(final InVMEpr inVMEpr, final Object msgObject)
throws InVMException
{
final String serviceId = inVMEpr.getServiceId() ;
final InVMEntry entry ;
acquireReadLock() ;
try
{
entry = serviceIdToEntry.get(serviceId) ;
}
finally
{
releaseReadLock() ;
}
if (entry == null)
{
throw new InVMException("Could not locate service entry for epr " + inVMEpr) ;
}
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Transactional redelivery of message to " + serviceId) ;
}
entry.deliver(msgObject, 0) ;
}
/**
* Encode a message to an Object ready for delivery.
* @param message The message to be encoded.
* @param passByValue If the message will be delivered by value (as opposed to be reference).
* @return The delivery Object.
* @throws InVMException Error encoding message.
*/
public static Object toDeliveryObject(Message message, boolean passByValue) throws InVMException {
final Object object ;
if (passByValue)
{
try
{
object = MessageSerializer.serialize(message) ;
}
catch (final IOException ex)
{
throw new InVMException("Could not serialize message to pass by value.", ex) ;
}
}
else if (message instanceof ByReferenceMessage)
{
object = ((ByReferenceMessage)message).reference() ;
}
else
{
object = message ;
}
return object;
}
/**
* Decode a delivery Object instance back to an ESB {@link Message} object instance.
* @param msgObject The delivery Object to be decoded.
* @param passByValue If the message was delivered by value (as opposed to be reference).
* @return The ESB Message Object instance.
* @throws InVMException Error decoding message.
*/
public static Message fromDeliveryObject(Object msgObject, boolean passByValue) throws InVMException {
final Message message ;
try
{
if (msgObject instanceof byte[])
{
message = MessageSerializer.deserialize((byte[])msgObject) ;
}
else if (passByValue)
{
// pass by reference but now expecting value.
message = ((Message)msgObject).copy() ;
}
else
{
message = (Message)msgObject ;
}
}
catch (final IOException ioe)
{
throw new InVMException("Failed to deserialise incoming message", ioe) ;
}
return message;
}
/**
* Acquire a read lock for accessing the data.
*/
private void acquireReadLock()
{
lock.readLock().lock() ;
}
/**
* Release a read lock for accessing the data.
*/
private void releaseReadLock()
{
lock.readLock().unlock() ;
}
/**
* Acquire a write lock for accessing the data.
*/
private void acquireWriteLock()
{
lock.writeLock().lock() ;
}
/**
* Release a write lock for accessing the data.
*/
private void releaseWriteLock()
{
lock.writeLock().unlock() ;
}
/**
* TODO this is used in a number of classes so should be a separate
* util routine.
*/
static boolean isTransactional()
throws InVMException
{
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 InVMException("Associated transaction is no longer active!");
}
}
catch (final TransactionStrategyException ex)
{
throw new InVMException(ex);
}
return transactional;
}
/**
* Data representing an entry in the message queue.
* @author kevin
*/
private static class InVMQueueEntry
{
/**
* The value enqueued.
*/
private final Object value ;
/**
* Condition associated with lockstep.
*/
private final Condition condition ;
/**
* Construct the InVM queue entry.
* @param value The value being enqueued.
* @param condition The condition representing the lockstep or null if not required.
*/
InVMQueueEntry(final Object value, final Condition condition)
{
this.value = value ;
this.condition = condition ;
}
/**
* Get the enqueued value.
* @return The enqueued value.
*/
Object getValue()
{
return value ;
}
/**
* Get the lockstep condition.
* @return The lockstep condition or null if not necessary.
*/
Condition getCondition()
{
return condition ;
}
}
/**
* The InVM entries in the data structures.
* @author kevin
*/
private static class InVMEntry
{
/**
* The lock for the queue.
*/
private final Lock lock = new ReentrantLock() ;
/**
* The condition on which to await messages.
*/
private final Condition waitingCondition = lock.newCondition() ;
/**
* The number of waiters on the waiting condition.
*/
private int numWaiters ;
/**
* Flag indicating service has been shutdown.
*/
private boolean shutdown ;
/**
* Entries in the queue.
*/
private final Queue<InVMQueueEntry> entries = new LinkedList<InVMQueueEntry>() ;
/**
* Current EPRs.
*/
private final List<InVMEpr> eprs = new LinkedList<InVMEpr>() ;
/**
* The service id for this entry.
*/
private final String serviceId ;
/**
* The number of EPRs supporting pass by value.
*/
private int numPassByValue ;
/**
* The maximum lockstep value.
*/
private long lockstep ;
/**
* Create an entry with the specified service id.
* @param serviceId The service id.
*/
InVMEntry(final String serviceId)
{
this.serviceId = serviceId ;
}
/**
* Get the service id associated with this entry.
* @return The service id.
*/
String getServiceId()
{
return serviceId ;
}
/**
* Get the EPRs associated with this entry.
* Must hold external lock before accessing.
*
* @return the service EPRs.
*/
List<InVMEpr> getEPRs()
{
return eprs ;
}
/**
* Add an EPR into this entry.
* Must hold external write lock before accessing.
*
* @param epr the EPR to register with this entry.
* @return true if this is the first epr, false otherwise.
*/
boolean addEPR(final InVMEpr epr)
{
boolean result = eprs.isEmpty() ;
eprs.add(epr) ;
if (epr.getPassByValue())
{
numPassByValue ++ ;
}
if (epr.getLockstep())
{
final long eprLockstep = epr.getLockstepWaitTime() ;
if (eprLockstep > lockstep)
{
lockstep = eprLockstep ;
}
}
return result ;
}
/**
* Remove an EPR from this entry.
* Must hold external write lock before accessing.
*
* @param epr the EPR to remove from this entry.
* @return true if this is the last epr, false otherwise.
* @throws ServiceNotFoundException if the EPR cannot be located.
*/
boolean removeEPR(final InVMEpr epr)
throws ServiceNotFoundException
{
if (!eprs.remove(epr))
{
throw new ServiceNotFoundException("Could not locate the EPR in the current service") ;
}
if (epr.getPassByValue())
{
numPassByValue-- ;
}
if (epr.getLockstep())
{
if (epr.getLockstepWaitTime() == lockstep)
{
lockstep = 0 ;
for(InVMEpr inVMEpr: eprs)
{
if (inVMEpr.getLockstep())
{
final long eprLockstep = inVMEpr.getLockstepWaitTime() ;
if (eprLockstep > lockstep)
{
lockstep = eprLockstep ;
}
}
}
}
}
return eprs.isEmpty() ;
}
/**
* Shutdown this entry, releasing any waiters.
* Can hold external write lock before accessing.
*/
public void shutdown()
{
lock.lock() ;
try
{
shutdown = true ;
if (numWaiters > 0)
{
waitingCondition.signalAll() ;
}
}
finally
{
lock.unlock() ;
}
}
/**
* Should we pass the message by value?
* Must hold external lock before querying.
*
* @return true if should pass by value, false otherwise.
*/
public boolean isPassByValue()
{
return numPassByValue > 0 ;
}
/**
* Get the lockstep value.
* Must hold external lock before querying.
*
* @return the lockstep value or 0 if not active.
*/
public long getLockstep()
{
return lockstep ;
}
/**
* Deliver the specified value onto the queue.
*
* N.B. Must not hold external lock while calling this method.
*
* @param value The value being appended to the queue.
* @param lockstep The lockstep timeout or 0 if not required.
*
* @throws InVMException for InVM transport specific errors.
*/
public void deliver(final Object value, final long lockstep)
throws InVMException
{
lock.lock() ;
try
{
if (shutdown)
{
throw new InVMException("InVM Transport already shutdown") ;
}
final Condition condition = (lockstep > 0 ? lock.newCondition() : null) ;
final InVMQueueEntry queueEntry = new InVMQueueEntry(value, condition) ;
if (!entries.offer(queueEntry))
{
throw new InVMException("Failed to append message to InVM queue") ;
}
if (numWaiters > 0)
{
waitingCondition.signal() ;
}
if (condition != null)
{
try
{
condition.await(lockstep, TimeUnit.MILLISECONDS) ;
}
catch (final InterruptedException ie)
{
LOGGER.warn("Waiting delivery thread interupted while waiting on message pickup on InVM queue '" + serviceId + "'. Exiting pickup wait state.") ;
}
}
}
finally
{
lock.unlock() ;
}
}
/**
* Pickup an entry from the queue.
*
* N.B. Must not hold external lock while calling this method.
*
* @param millis The number of milliseconds to wait for a message.
* @return The message or null if nothing present within the timeout.
*
* @throws InVMException for InVM transport specific errors.
*/
public Object pickup(final long millis)
throws InVMException
{
final long end = System.currentTimeMillis() + millis ;
lock.lock() ;
try
{
if (shutdown)
{
throw new InVMException("InVM Transport already shutdown") ;
}
if (entries.isEmpty())
{
final long delay = end - System.currentTimeMillis() ;
if (delay > 0)
{
numWaiters++ ;
try
{
waitingCondition.await(delay, TimeUnit.MILLISECONDS) ;
}
catch (final InterruptedException ioe)
{
throw new InVMException("Interrupted during wait") ;
}
}
}
final InVMQueueEntry entry = entries.poll() ;
if (entry != null)
{
final Object result = entry.getValue() ;
final Condition condition = entry.getCondition() ;
if (condition != null)
{
condition.signal() ;
}
return result ;
}
return null ;
}
finally
{
lock.unlock() ;
}
}
}
}