/*------------------------------------------------------------------------------
Name: SubjectInfo.java
Project: xmlBlaster.org
Copyright: xmlBlaster.org, see xmlBlaster-LICENSE file
Comment: Handling the Client data
Author: xmlBlaster@marcelruff.info
------------------------------------------------------------------------------*/
package org.xmlBlaster.authentication;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.management.AttributeChangeNotification;
import javax.management.MBeanNotificationInfo;
import javax.management.NotificationBroadcasterSupport;
import org.xmlBlaster.authentication.plugins.I_Subject;
import org.xmlBlaster.engine.MsgErrorHandler;
import org.xmlBlaster.engine.ServerScope;
import org.xmlBlaster.engine.admin.I_AdminSession;
import org.xmlBlaster.engine.cluster.ClusterNode;
import org.xmlBlaster.engine.qos.ConnectQosServer;
import org.xmlBlaster.engine.query.plugins.QueueQueryPlugin;
import org.xmlBlaster.engine.queuemsg.MsgQueueUpdateEntry;
import org.xmlBlaster.util.MsgUnit;
import org.xmlBlaster.util.ReentrantLock;
import org.xmlBlaster.util.SessionName;
import org.xmlBlaster.util.XmlBlasterException;
import org.xmlBlaster.util.admin.extern.JmxMBeanHandle;
import org.xmlBlaster.util.cluster.NodeId;
import org.xmlBlaster.util.context.ContextNode;
import org.xmlBlaster.util.def.Constants;
import org.xmlBlaster.util.def.ErrorCode;
import org.xmlBlaster.util.dispatch.DispatchStatistic;
import org.xmlBlaster.util.error.I_MsgErrorHandler;
import org.xmlBlaster.util.error.MsgErrorInfo;
import org.xmlBlaster.util.qos.SessionQos;
import org.xmlBlaster.util.qos.address.AddressBase;
import org.xmlBlaster.util.qos.address.CallbackAddress;
import org.xmlBlaster.util.qos.storage.CbQueueProperty;
import org.xmlBlaster.util.queue.I_Entry;
import org.xmlBlaster.util.queue.I_Queue;
import org.xmlBlaster.util.queue.StorageId;
import org.xmlBlaster.util.queuemsg.MsgQueueEntry;
/**
* The SubjectInfo stores all known data about a client.
* <p>
* It also contains a subject queue, where messages are stored
* until they are delivered at the next login of this client.
* </p>
* <p>
* There are three states for SubjectInfo namely UNDEF, ALIVE, DEAD.
* A transition from UNDEF directly to DEAD is not supported.
* Transitions from ALIVE or DEAD to UNDEF are not possible.
* </p>
* @author <a href="mailto:xmlBlaster@marcelruff.info">Marcel Ruff</a>
*/
public final class SubjectInfo extends NotificationBroadcasterSupport /* implements I_AdminSubject, SubjectInfoMBean -> is delegated to SubjectInfoProtector */
{
private String ME = "SubjectInfo";
private final ServerScope glob;
private static Logger log = Logger.getLogger(SubjectInfo.class.getName());
private final ContextNode contextNode;
private final Authenticate authenticate;
/** The cluster wide unique identifier of the subject e.g. "/node/heron/client/joe" */
private SessionName subjectName;
/** The partner class from the security framework */
private I_Subject securityCtx = null;
private boolean blockClientLogin;
/**
* All sessions of this subject are stored in this map.
* The absoluteSessionName == sessionInfo.getId() is the key,
* the SessionInfo object the value
*/
private Map sessionMap = new HashMap();
private volatile SessionInfo[] sessionArrCache;
public CallbackAddress[] callbackAddressCache = null;
private MsgErrorHandler msgErrorHandler;
private final DispatchStatistic dispatchStatistic;
private final SubjectInfoProtector subjectInfoProtector;
private NodeId nodeId = null;
private boolean determineNodeId = true;
// Enforced by I_AdminSubject
/** Incarnation time of this object instance in millis */
private long startupTime;
private int maxSessions;
/** State during and after construction */
public final int UNDEF = -1;
/** State after calling toAlive() */
public final int ALIVE = 0;
/** State after calling shutdown() */
public final int DEAD = 1;
private int state = UNDEF;
private ReentrantLock lock = new ReentrantLock();
/**
* All MsgUnit which can't be delivered to the client (if he is not logged in)
* are queued here and are delivered when the client comes on line.
* <p>
* Node objects = MsgQueueEntry
*/
private I_Queue subjectQueue;
/** this is used for administrative gets (queries on callback queue) */
private volatile QueueQueryPlugin queueQueryPlugin;
/** Statistics */
private static long instanceCounter = 0L;
private long instanceId = 0L;
/** My JMX registration */
private JmxMBeanHandle mbeanHandle;
/**
* <p />
* @param subjectName The unique loginName
* @param securityCtx The security context of this subject
* @param prop The property from the subject queue, usually from connectQos.getSubjectQueueProperty()
*/
public SubjectInfo(ServerScope glob, Authenticate authenticate, SessionName subjectName) //, I_Subject securityCtx, CbQueueProperty prop)
throws XmlBlasterException {
synchronized (SubjectInfo.class) {
this.instanceId = instanceCounter;
instanceCounter++;
}
this.glob = glob;
this.authenticate = authenticate;
this.subjectInfoProtector = new SubjectInfoProtector(this);
this.subjectName = subjectName; //new SessionName(glob, glob.getNodeId(), loginName);
if (this.subjectName.isSession()) {
log.severe(ME+": Didn't expect a session name for a subject: " + this.subjectName.toXml());
Thread.dumpStack();
}
String instanceName = this.glob.validateJmxValue(this.subjectName.getLoginName());
this.contextNode = new ContextNode(ContextNode.SUBJECT_MARKER_TAG, instanceName,
this.glob.getContextNode());
this.ME = this.instanceId + "-" + this.subjectName.getAbsoluteName();
this.dispatchStatistic = new DispatchStatistic();
// JMX register "client/joe"
this.mbeanHandle = this.glob.registerMBean(this.contextNode, this.subjectInfoProtector);
if (log.isLoggable(Level.FINE)) log.fine(ME+": Created new SubjectInfo");
}
/**
* The unique name of this subject instance.
* @return Never null, for example "/xmlBlaster/node/heron/client/joe"
*/
public final ContextNode getContextNode() {
return this.contextNode;
}
/**
* if state==UNDEF we block until we are ALIVE (or DEAD)
* @exception If we are DEAD or on one minute timeout, subjectInfo is never locked in such a case
*/
public void waitUntilAlive(boolean returnLocked) throws XmlBlasterException {
if (this.state == ALIVE) {
if (returnLocked) this.lock.lock();
return;
}
if (this.state == DEAD)
throw new XmlBlasterException(glob, ErrorCode.INTERNAL_UNKNOWN, ME+".waitUntilAlive()", "Did not expect state DEAD, please try again.");
if (log.isLoggable(Level.FINE)) log.fine(ME+": is going to wait max. one minute");
long msecs = 1000 * 60;
while (true) {
synchronized (this) {
try {
this.wait(msecs);
break;
}
catch (InterruptedException e) {
log.severe(ME+": Ignoring unexpected exception: " + e.toString());
}
}
}
if (returnLocked) this.lock.lock();
if (this.state != ALIVE) {
if (returnLocked) this.lock.release();
throw new XmlBlasterException(glob, ErrorCode.INTERNAL_UNKNOWN, ME+".waitUntilAlive()", "ALIVE not reached, state=" + this.state);
}
return;
}
/**
* Access the synchronization object of this SubjectInfo instance.
*/
public ReentrantLock getLock() {
return this.lock;
}
SubjectInfoProtector getSubjectInfoProtector() {
return this.subjectInfoProtector;
}
/**
* Initialize SubjectInfo
* @param securityCtx Can be null for PtP message with implicit SubjectInfo creation
* @param prop The property to configure the PtP message queue
*/
public void toAlive(I_Subject securityCtx, CbQueueProperty prop) throws XmlBlasterException {
if (isAlive()) {
return;
}
this.lock.lock();
try {
if (securityCtx != null) {
this.securityCtx = securityCtx;
}
this.startupTime = System.currentTimeMillis();
this.maxSessions = glob.getProperty().get("session.maxSessions", SessionQos.DEFAULT_maxSessions);
if (glob.getId() != null)
this.maxSessions = glob.getProperty().get("session.maxSessions["+glob.getId()+"]", this.maxSessions);
this.subjectQueue = createSubjectQueue(prop);
this.state = ALIVE;
synchronized (this) {
this.notifyAll(); // notify waitUntilAlive()
}
// done externally to avoid dead locks
//this.authenticate.addLoginName(this); // register myself -> enters synchronized(this.loginNameSubjectInfoMap)
try { // Keep this code in the sync block, in case a disconnect() arrives immediately
glob.getRequestBroker().updateInternalUserList();
}
catch (XmlBlasterException e) {
log.severe(ME+": Publishing internal user list failed: " + e.getMessage());
}
if (log.isLoggable(Level.FINE)) log.fine(ME+": Transition from UNDEF to ALIVE done");
}
finally {
this.lock.release();
}
}
private I_Queue createSubjectQueue(CbQueueProperty prop) throws XmlBlasterException {
if (prop == null) prop = new CbQueueProperty(glob, Constants.RELATING_SUBJECT, glob.getId());
String type = prop.getType();
String version = prop.getVersion();
StorageId storageId = new StorageId(glob, this.glob.getDatabaseNodeStr(), Constants.RELATING_SUBJECT,
this.subjectName);
// old xb_entries:
// StorageId storageId = new StorageId(glob, Constants.RELATING_SUBJECT,
// this.subjectName.getAbsoluteName());
I_Queue queue = glob.getQueuePluginManager().getPlugin(type, version, storageId, prop);
queue.setNotifiedAboutAddOrRemove(true); // Entries are notified to support reference counting
return queue;
}
/**
* The shutdown is synchronized and checks if there is no need for this subject anymore.
* <p>
* clearQueue==false&&forceIfEntries==true: We shutdown and preserve existing PtP messages
* </p>
* @param clearQueue Shall the message queue of the client be destroyed as well on last session logout?
* @param forceIfEntries Shutdown even if there are messages in the queue
*/
public void shutdown(boolean clearQueue, boolean forceIfEntries) {
if (log.isLoggable(Level.FINER)) log.finer(ME+": clearQueue=" + clearQueue + ", forceIfEntries=" + forceIfEntries);
this.lock.lock();
try {
if (!this.isAlive()) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": Ignoring shutdown request as we are in state " + getStateStr());
return;
}
if (isLoggedIn()) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": Ignoring shutdown request as there are still login sessions");
return;
}
if (!forceIfEntries && !clearQueue && getSubjectQueue().getNumOfEntries() > 0) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": Ignoring shutdown request as there are still messages in the subject queue");
return;
}
this.glob.unregisterMBean(this.mbeanHandle);
if (getSubjectQueue().getNumOfEntries() < 1)
log.info(ME+": Destroying SubjectInfo. Nobody is logged in and no queue entries available");
else {
if (clearQueue)
log.warning(ME+": Destroying SubjectInfo. Lost " + getSubjectQueue().getNumOfEntries() + " messages in the subject queue as clearQueue is set to true");
else
log.warning(ME+": Destroying SubjectInfo. The subject queue still contains " + getSubjectQueue().getNumOfEntries() + " messages, " +
getSubjectQueue().getNumOfPersistentEntries() + " persistent messages remain on disk, the transients are lost");
}
this.authenticate.removeLoginName(this); // deregister
this.state = DEAD;
if (clearQueue)
this.subjectQueue.clear();
if (this.subjectQueue != null) {
this.subjectQueue.shutdown();
}
if (getSessions().length > 0) {
log.warning(ME+": shutdown of subject " + getLoginName() + " has still " + getSessions().length + " sessions - memory leak?");
}
synchronized (this.sessionMap) {
this.sessionArrCache = null;
this.sessionMap.clear();
this.callbackAddressCache = null;
}
if (this.msgErrorHandler != null)
this.msgErrorHandler.shutdown();
synchronized (this) {
this.notifyAll(); // notify waitUntilAlive()
}
// Not possible to allow toAlive()
//this.securityCtx = null;
}
finally {
this.lock.release();
}
}
/**
* Shutdown my queue
*/
public void finalize() {
try {
if (log.isLoggable(Level.FINE)) log.fine(ME+": finalize - garbage collected");
//boolean force = true;
//this.subjectQueue.shutdown();
}
catch (Throwable e) {
e.printStackTrace();
}
try {
super.finalize();
}
catch (Throwable e) {
e.printStackTrace();
}
}
/**
* Blocks for existing SessionInfo until it is initialized.
* For new created SessionInfo you need to call sessionInfo.init()
* @param sessionName
* @param connectQos
* @return
* @throws XmlBlasterException
*/
SessionInfo getOrCreateSessionInfo(SessionName sessionName, ConnectQosServer connectQos) throws XmlBlasterException {
synchronized (this.sessionMap) {
SessionInfo sessionInfo = getSessionInfo(sessionName);
if (sessionInfo == null) {
checkNumberOfSessions(connectQos);
sessionInfo = new SessionInfo(glob, sessionName);
this.sessionMap.put(sessionInfo.getId(), sessionInfo);
this.sessionArrCache = null;
this.callbackAddressCache = null;
}
else {
final int MAX = 10000;
int i=0;
for (; i<MAX; i++) {
if (sessionInfo.isInitialized())
break;
try {
Thread.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (i >= MAX) {
Thread.dumpStack();
XmlBlasterException ex = new XmlBlasterException(glob, ErrorCode.RESOURCE_TEMPORARY_UNAVAILABLE, ME,
"Connection for " + sessionName.getAbsoluteName() + " failed, timeout while waiting for concurrently created same session name, please retry.");
// The client shall retry (behave like a communication exception)
throw new XmlBlasterException(glob, ErrorCode.COMMUNICATION_RESOURCE_TEMPORARY_UNAVAILABLE, ME, "", ex);
}
}
return sessionInfo;
}
}
/**
* Find a session by its pubSessionId or return null if not found
*/
public SessionInfo getSessionInfo(SessionName sessionName) {
SessionInfo[] sessions = getSessions();
for (int ii=0; ii<sessions.length; ii++) {
if (sessions[ii].getSessionName().equalsRelative(sessionName)) {
return sessions[ii];
}
}
return null;
}
/**
* @return not null if client is a cluster node, else null
*/
public final NodeId getNodeId() throws XmlBlasterException {
if (determineNodeId) {
determineNodeId = false;
if (this.subjectName.getLoginName().startsWith(org.xmlBlaster.engine.RequestBroker.internalLoginNamePrefix))
return null; // don't check for internal logins
if (glob.isClusterManagerReady()) {
// Is the client a well known, configured cluster node?
ClusterNode clusterNode = glob.getClusterManager().getClusterNode(this.subjectName.getLoginName()); // is null if not found
if (clusterNode != null) {
nodeId = clusterNode.getNodeId();
}
else {
// Does the client send a tag which marks it as a cluster node?
SessionInfo ses = getFirstSession();
if (ses != null) {
if (ses.getConnectQos().isClusterNode())
nodeId = new NodeId(this.subjectName.getLoginName());
}
}
}
}
return nodeId;
}
/**
* @return true if this client is an xmlBlaster cluster node
*/
public boolean isCluster() throws XmlBlasterException {
return getNodeId() != null;
}
/**
* Allows to overwrite queue property.
* <p>
* It will be only written if prop!= null.
* </p>
* @param prop CbQueueProperty transports subject queue property as well
* TODO: we should have a clear named SubjectQueueProperty
*/
public final void setSubjectQueueProperty(CbQueueProperty prop) throws XmlBlasterException {
CbQueueProperty origProp = (CbQueueProperty)this.subjectQueue.getProperties();
if (origProp == null) {
log.severe(ME+": Existing subject queue properties are null");
return;
}
if (prop == null) prop = new CbQueueProperty(glob, Constants.RELATING_SUBJECT, glob.getId());
this.lock.lock();
try {
if (prop.getTypeVersion().equals(origProp.getTypeVersion())) {
this.subjectQueue.setProperties(prop);
return;
}
// TODO: Extend CACHE queue to handle reconfigurations hidden so we don't need to do anything here
if (!this.subjectQueue.isTransient()) {
I_Queue newQueue = createSubjectQueue(prop);
if (newQueue.isTransient()) {
log.info(ME+": Reconfiguring subject queue: Copying " + this.subjectQueue.getNumOfEntries() + " entries from old " + origProp.getType() + " queue to " + prop.getTypeVersion() + " queue");
List<I_Entry> list = null;
int lastSize = -99;
while (this.subjectQueue.getNumOfEntries() > 0) {
try {
list = this.subjectQueue.peek(-1, -1);
if (this.subjectQueue.getNumOfEntries() == lastSize) {
log.severe(ME+": PANIC: " + this.subjectQueue.getNumOfEntries() + " entries from old queue " + this.subjectQueue.getStorageId() + " can't be copied, giving up!");
break;
}
lastSize = (int)this.subjectQueue.getNumOfEntries();
}
catch (XmlBlasterException e) {
log.severe(ME+": PANIC: Can't copy from subject queue '" + this.subjectQueue.getStorageId() + "' with " + this.subjectQueue.getNumOfEntries() + " entries: " + e.getMessage());
e.printStackTrace();
continue;
}
MsgQueueEntry[] queueEntries = (MsgQueueEntry[])list.toArray(new MsgQueueEntry[list.size()]);
// On error we send them as dead letters, as we don't know what to do with them in our holdback queue
try {
newQueue.put(queueEntries, false);
}
catch (XmlBlasterException e) {
log.warning(ME+": flushHoldbackQueue() failed: " + e.getMessage());
// errorCode == "ONOVERFLOW"
getMsgErrorHandler().handleError(new MsgErrorInfo(glob, queueEntries, null, e));
}
try {
long num = this.subjectQueue.removeNum(list.size());
if (num != list.size()) {
log.severe(ME+": PANIC: Expected to remove from subject queue '" + this.subjectQueue.getStorageId() + "' with " + this.subjectQueue.getNumOfEntries() + " entries " + list.size() + " entries, but only " + num + " where removed");
}
}
catch (XmlBlasterException e) {
log.severe(ME+": PANIC: Expected to remove from subject queue '" + this.subjectQueue.getStorageId() + "' with " + this.subjectQueue.getNumOfEntries() + " entries " + list.size() + " entries: " + e.getMessage());
}
}
this.subjectQueue.clear();
this.subjectQueue.shutdown();
this.subjectQueue = newQueue;
return;
}
}
} // synchronized
finally {
this.lock.release();
}
log.severe(ME+": Can't reconfigure subject queue type '" + origProp.getTypeVersion() + "' to '" + prop.getTypeVersion() + "'");
return;
//throw new XmlBlasterException(glob, ErrorCode.USER_CONFIGURATION, ME+".setSubjectQueueProperty()", "Can't reconfigure subject queue type '" + origProps.getTypeVersion() + "' to '" + props.getTypeVersion() + "'");
}
/**
* This queue holds all messages which where addressed to destination loginName
* @return never null
*/
public I_Queue getSubjectQueue() {
return this.subjectQueue;
}
/**
* Subject specific informations from the security framework
* @return null if created without login (for example with a PtP message)
*/
public I_Subject getSecurityCtx() {
return this.securityCtx;
}
public void setSecurityCtx(I_Subject securityCtx) {
this.securityCtx = securityCtx;
}
/*
* Check if this subject is permitted to do something
* <p/>
* @param String The action the user tries to perfrom
* @param String whereon the user tries to perform the action
*
* EXAMPLE:
* isAuthorized("PUBLISH", "thisIsAMessageKey");
*
* The above line checks if this subject is permitted to >>publish<<
* a message under the key >>thisIsAMessageKey<<
*
* Known action keys:
* PUBLISH, SUBSCRIBE, GET, ERASE,
public boolean isAuthorized(MethodName actionKey, String key) {
if (this.securityCtx == null) {
log.warning("No authorization for '" + actionKey + "' and msg=" + key);
return false;
}
return this.securityCtx.isAuthorized(actionKey, key);
}
*/
/**
* PtP mode: If the qos is set to forceQueuing the message is queued.
* @param msgUnit The message. Only called in sync mode on publish (TopicHandler)
* @param destination The Destination object of the receiver
*/
public final void queueMessage(MsgQueueEntry entry) throws XmlBlasterException {
if (log.isLoggable(Level.FINER)) log.finer(ME+": Queuing message for destination " + entry.getReceiver());
if (log.isLoggable(Level.FINEST)) log.finest(ME+": Putting PtP message to queue: " + entry.toXml(""));
this.subjectQueue.put(entry, I_Queue.USE_PUT_INTERCEPTOR);
//forwardToSessionQueue();
// returns here with no waiting ...
this.glob.getSubjectInfoShuffler().shuffle(this);
}
/**
* Forward entries in subject queue to all session queues,
* if no entries are available
* we return 0 without doing anything.
* @return number of messages taken from queue and forwarded
*/
public final long forwardToSessionQueue() {
if (getSessions().length < 1 || this.subjectQueue.getNumOfEntries() < 1) return 0;
long numMsgs = 0;
MsgQueueUpdateEntry entry = null;
if (log.isLoggable(Level.FINE)) log.fine(ME+": Trying to forward " + this.subjectQueue.getNumOfEntries() + " messages in subject queue to session queue ...");
while (true) {
try {
try {
entry = (MsgQueueUpdateEntry)this.subjectQueue.peek(); // non-blocking
}
catch (Throwable ex) {
log.severe(ME+": Can't get entry from subject queue when trying to forward it to session queue " + ex.getMessage());
// TODO toDead from the subject may be necessary to avoid looping
break;
}
if (entry == null)
break;
if (entry.isDestroyed()) {
log.info(ME+": Message " + entry.getLogId() + " is destroyed, ignoring it");
this.subjectQueue.removeRandom(entry); // Remove the destroyed entry
}
else {
int countForwarded = forwardToSessionQueue(entry);
if (countForwarded > 0) {
this.subjectQueue.removeRandom(entry); // Remove the forwarded entry (blocking)
numMsgs++;
}
else if (countForwarded == -1) { // There are sessions but they don't want PtP
break;
}
}
}
catch(Throwable e) {
MsgQueueEntry[] msgQueueEntries = new MsgQueueEntry[] { entry };
MsgErrorInfo msgErrorInfo = new MsgErrorInfo(glob, msgQueueEntries, null, e); // this.subjectQueue
getMsgErrorHandler().handleError(msgErrorInfo);
try {
this.subjectQueue.removeRandom(entry); // Remove the entry
}
catch (XmlBlasterException ex) {
log.severe(ME+": Can't empty queue when removing '" + entry.getLogId() + "' " + ex.getMessage());
// TODO toDead from the subject may be necessary to avoid looping
break;
}
}
}
if (log.isLoggable(Level.FINE)) log.fine(ME+": Forwarded " + numMsgs + " messages from subject queue to session queue");
if (!isLoggedIn()) { // Check if we can shutdown now
shutdown(false, false);
}
return numMsgs;
}
/**
* Forward the given message to session queue.
* @return Number of session queues this message is forwarded to.
* -1 if not delivered because the available sessions don't want PtP
* @throws XmlBlasterException if not delivered at all.
*/
private final int forwardToSessionQueue(MsgQueueEntry entry) throws XmlBlasterException {
if (getSessions().length < 1) return -1;
int countForwarded = 0;
SessionName destination = entry.getReceiver();
if (destination.isSession()) {
// send to a specific session, it should never happen to have such messages in the subject queue ...
String tmp = "Can't forward msg " + entry.getLogId() + " from " +
this.subjectQueue.getStorageId() + " size=" +
this.subjectQueue.getNumOfEntries() + " to unknown session '" +
entry.getReceiver().getAbsoluteName() + "'";
log.warning(ME+": "+tmp);
throw new XmlBlasterException(glob, ErrorCode.INTERNAL_UNKNOWN, ME, tmp);
}
// ... or send to ALL sessions
SessionInfo[] sessions = getSessions();
for (int i=0; i<sessions.length; i++) {
SessionInfo sessionInfo = sessions[i];
I_Queue sessionQueue = sessionInfo.getSessionQueue();
if (sessionInfo.getConnectQos().isPtpAllowed() && sessionInfo.hasCallback() && sessionQueue != null) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": Forwarding msg " + entry.getLogId() + " from " +
this.subjectQueue.getStorageId() + " size=" + this.subjectQueue.getNumOfEntries() +
" to session queue " + sessionQueue.getStorageId() +
" size=" + sessionQueue.getNumOfEntries() + " ...");
try {
MsgQueueUpdateEntry entryCb = new MsgQueueUpdateEntry((MsgQueueUpdateEntry)entry, sessionQueue.getStorageId());
sessionInfo.queueMessage(entryCb);
countForwarded++;
}
catch (XmlBlasterException e) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": Can't forward message from subject queue '" + this.subjectQueue.getStorageId() + "' to session '" + sessionInfo.getId() + "', we keep it in the subject queue: " + e.getMessage());
}
catch (Throwable e) {
e.printStackTrace();
log.warning(ME+": Can't forward message from subject queue '" + this.subjectQueue.getStorageId() + "' to session '" + sessionInfo.getId() + "', we keep it in the subject queue: " + e.toString());
}
}
}
if (countForwarded > 0) {
return countForwarded;
}
return -1;
}
public final I_MsgErrorHandler getMsgErrorHandler() {
if (this.msgErrorHandler == null) {
this.lock.lock();
try {
if (this.msgErrorHandler == null) {
log.severe(ME+": INTERNAL: Support for MsgErrorHandler is not implemented");
this.msgErrorHandler = new MsgErrorHandler(glob, null);
}
}
finally {
this.lock.release();
}
}
return this.msgErrorHandler;
}
/**
* Is the client currently logged in?
* @return true yes
* false client is not on line
*/
public final boolean isLoggedIn() {
synchronized (this.sessionMap) {
return this.sessionMap.size() > 0;
}
//return getSessions().length > 0;
}
/**
* Access the collection containing all SessionInfo objects of this user.
*/
public final SessionInfo[] getSessions() {
if (this.sessionArrCache == null) {
synchronized (this.sessionMap) {
if (this.sessionArrCache == null) {
this.sessionArrCache = (SessionInfo[])this.sessionMap.values().toArray(new SessionInfo[this.sessionMap.size()]);
}
}
}
return this.sessionArrCache;
}
/** @return true it publicSessionId is given by xmlBlaster server (if < 0) */
public final int getCountSessionsInternal() {
int count = 0;
SessionInfo[] arr = getSessions();
for (int i=0; i<arr.length; i++) {
if (arr[i].getSessionName().isPubSessionIdInternal())
count++;
}
return count;
}
/** @return true it publicSessionId is given by user/client (if > 0) */
public final int getCountSessionsUser() {
int count = 0;
SessionInfo[] arr = getSessions();
for (int i=0; i<arr.length; i++) {
if (arr[i].getSessionName().isPubSessionIdUser())
count++;
}
return count;
}
/**
* Find a session by its absolute name.
* @param absoluteName e.g. "/node/heron/client/joe/2"
* @return SessionInfo or null if not found
*/
public final SessionInfo getSessionByAbsoluteName(String absoluteName) {
synchronized (this.sessionMap) {
return (SessionInfo)this.sessionMap.get(absoluteName);
}
}
/**
* Find a session by its public session ID.
* @param sessionName
* @return SessionInfo or null if not found
*/
public final SessionInfo getSession(SessionName sessionName) {
synchronized (this.sessionMap) {
return (SessionInfo)this.sessionMap.get(sessionName.getAbsoluteName());
}
}
public final SessionInfo getFirstSession() {
SessionInfo[] sessions = getSessions();
return (sessions.length > 0) ? sessions[0] : null;
}
/**
* Get the callback addresses for this subjectQueue, every session
* callback may have decided to receive subject messages
*/
public final CallbackAddress[] getCallbackAddresses() {
if (this.callbackAddressCache == null) {
SessionInfo[] sessions = getSessions();
Set set = new HashSet();
for (int i=0; i<sessions.length; i++) {
SessionInfo ses = sessions[i];
if (ses.hasCallback()) {
CallbackAddress[] arr = ((CbQueueProperty)ses.getSessionQueue().getProperties()).getCallbackAddresses();
for (int ii=0; arr!=null && ii<arr.length; ii++) {
if (arr[ii].useForSubjectQueue() == true)
set.add(arr[ii]);
}
}
}
this.callbackAddressCache = (CallbackAddress[])set.toArray(new CallbackAddress[set.size()]);
}
if (log.isLoggable(Level.FINE)) log.fine(ME+": Accessing " + this.callbackAddressCache.length + " callback addresses from " + getSessions().length + " sessions for '" + getLoginName() + "' queue");
return this.callbackAddressCache;
}
/**
* If you have a callback address and want to know to which session it belongs.
* @param addr The address object
* @return the sessionInfo or null
*/
public final SessionInfo findSessionInfo(AddressBase addr) {
SessionInfo[] sessions = getSessions();
for (int i=0; i<sessions.length; i++) {
SessionInfo ses = sessions[i];
if (ses.hasAddress(addr))
return ses;
}
return null;
}
/**
* @exception Throws XmlBlasterException if max. sessions is exhausted
*/
public final void checkNumberOfSessions(ConnectQosServer qos) throws XmlBlasterException {
if (qos.isFromPersistenceRecovery())
return;
if (SessionQos.DEFAULT_maxSessions != qos.getSessionQos().getMaxSessions())
this.maxSessions = qos.getSessionQos().getMaxSessions();
int count = getSessions().length;
if (qos.isSessionLimitsPubSessionIdSpecific()) {
count = qos.getSessionName().isPubSessionIdInternal() ? getCountSessionsInternal() : getCountSessionsUser();
}
if (count >= this.maxSessions) {
log.warning(ME+": Max sessions = " + this.maxSessions + " for user " + getLoginName() + "@" + qos.getSecurityQos().getClientIp() + " exhausted, login denied.");
throw new XmlBlasterException(glob, ErrorCode.USER_CONFIGURATION_MAXSESSION, ME, "Max sessions = " + this.maxSessions + " exhausted, login denied.");
}
}
/**
* Check if client does a re-login and wants to destroy old sessions.
* @return never null
*/
public SessionInfo[] getSessionsToClear(ConnectQosServer q) {
if (q.clearSessions() == true && getNumSessions() > 0) {
SessionInfo[] arr = getSessions();
if (q.isSessionLimitsPubSessionIdSpecific()) {
// Special case: only destroy pubSessionId<0 if we are also <0 and vice versa
boolean isInternal = q.getSessionQos().getSessionName().isPubSessionIdInternal();
ArrayList list = new ArrayList(arr.length);
for (int i=0; i<arr.length; i++) {
if (isInternal == arr[i].getSessionName().isPubSessionIdInternal())
list.add(arr[i]);
}
log.warning("clearSessions for " + list.size() + " isInternal=" + isInternal + " sessions, max=" + getNumSessions() + " reached");
return (SessionInfo[])list.toArray(new SessionInfo[list.size()]);
}
else {
log.warning("clearSessions for " + arr.length + " sessions, max=" + getNumSessions() + " reached");
return arr;
}
}
return new SessionInfo[0];
}
/**
* Get notification that the client did a login.
* <p />
* This instance may exist before a login was done, for example
* when some messages where directly addressed to this client.<br />
* This notifies about a client login.
*/
public final void notifyAboutLogin(SessionInfo sessionInfo) throws XmlBlasterException {
if (!isAlive()) { // disconnect() and connect() are not synchronized, so this can happen
throw new XmlBlasterException(glob, ErrorCode.INTERNAL_UNKNOWN, ME, "SubjectInfo is shutdown, try to login again");
}
if (log.isLoggable(Level.FINER)) log.finer(ME+": notifyAboutLogin(" + sessionInfo.getSecretSessionId() + ")");
synchronized (this.sessionMap) {
this.sessionMap.put(sessionInfo.getId(), sessionInfo);
this.sessionArrCache = null;
this.callbackAddressCache = null;
}
if (log.isLoggable(Level.FINEST)) log.finest(ME+": "+this.subjectQueue.toXml(""));
if (this.subjectQueue.getNumOfEntries() > 0) {
if (log.isLoggable(Level.FINE)) log.fine(ME+": Flushing " + this.subjectQueue.getNumOfEntries() + " messages");
this.glob.getSubjectInfoShuffler().shuffle(this); // Background thread
}
}
/**
* Get notification that the client did a logout.
* <br />
* Note that the loginName is not reset.
* @param absoluteSessionName == sessionInfo.getId()
* @param clearQueue Shall the message queue of the client be cleared&destroyed as well (e.g. disconnectQos.deleteSubjectQueue())?
* @param forceShutdownEvenIfEntriesExist on last session
*/
public final void notifyAboutLogout(String absoluteSessionName, boolean clearQueue, boolean forceShutdownEvenIfEntriesExist) throws XmlBlasterException {
if (!isAlive()) { // disconnect() and connect() are not synchronized, so this can happen
throw new XmlBlasterException(glob, ErrorCode.INTERNAL_UNKNOWN, ME, "SubjectInfo is shutdown, no logout");
}
if (log.isLoggable(Level.FINER)) log.finer(ME+": Entering notifyAboutLogout(" + absoluteSessionName + ", " + clearQueue + ")");
SessionInfo sessionInfo = null;
synchronized (this.sessionMap) {
sessionInfo = (SessionInfo)sessionMap.remove(absoluteSessionName);
this.sessionArrCache = null;
this.callbackAddressCache = null;
}
if (sessionInfo != null) {
this.dispatchStatistic.incrNumUpdate(sessionInfo.getNumUpdate());
}
else {
log.warning(ME+": Lookup of session with absoluteSessionName=" + absoluteSessionName + " failed");
}
if (log.isLoggable(Level.FINEST)) log.finest(this.subjectQueue.toXml(null));
//if (!isLoggedIn()) {
// if (clearQueue || getSubjectQueue().getNumOfEntries() < 1) {
shutdown(clearQueue, forceShutdownEvenIfEntriesExist); // Does shutdown only on last session
// }
//}
}
/**
* Access the unique login name of a client.
* <br />
* If not known, its unique key (subjectId) is delivered
* @return The SessionName object specific for a subject (pubSessionId is null)
*/
public final SessionName getSubjectName() {
return this.subjectName;
}
/**
* Cluster wide unique identifier "/node/heron/client/<loginName>" e.g. for logging
* <p />
* @return e.g. "client/joe
*/
public final String getId() {
return this.subjectName.getAbsoluteName();
}
/**
* @see #getId
*/
public final String toString() {
return this.subjectName.getAbsoluteName();
}
/**
* Access the unique login name of a client.
* <br />
* If not known, its unique key (subjectId) is delivered
* @return loginName
*/
public final String getLoginName() {
return this.subjectName.getLoginName();
}
/**
* Dump state of this object into a XML ASCII string.
* <br>
* @return internal state of SubjectInfo as a XML ASCII string
*/
public final String toXml() {
return toXml((String)null);
}
/**
* Dump state of this object into a XML ASCII string.
* <br>
* @param extraOffset indenting of tags for nice output
* @return internal state of SubjectInfo as a XML ASCII string
*/
public final String toXml(String extraOffset) {
StringBuffer sb = new StringBuffer(256);
if (extraOffset == null) extraOffset = "";
String offset = Constants.OFFSET + extraOffset;
sb.append(offset).append("<SubjectInfo id='").append(this.subjectName.getAbsoluteName()).append("'>");
sb.append(offset).append(" <state>").append(getStateStr()).append("</state>");
if (isAlive()) {
sb.append(offset).append(" <subjectId>").append(getLoginName()).append("</subjectId>");
sb.append(subjectQueue.toXml(extraOffset+Constants.INDENT));
SessionInfo[] sessions = getSessions();
for (int i=0; i<sessions.length; i++) {
SessionInfo sessionInfo = sessions[i];
sb.append(sessionInfo.toXml(extraOffset+Constants.INDENT, (Properties)null));
}
}
sb.append(offset).append("</SubjectInfo>");
return sb.toString();
}
/**
* Get the SessionInfo with its public session identifier e.g. "5"
* @return null if not found
*/
public final SessionInfo getSessionByPublicId(long publicSessionId) {
if (publicSessionId == 0L) {
return null;
}
SessionName sessionName = new SessionName(glob, subjectName, publicSessionId);
synchronized (this.sessionMap) {
return (SessionInfo)this.sessionMap.get(sessionName.getAbsoluteName());
}
}
public final boolean isUndef() {
return this.state == UNDEF;
}
public final boolean isAlive() {
return this.state == ALIVE;
}
public final boolean isDead() {
return this.state == DEAD;
}
public final String getStateStr() {
if (isAlive()) {
return "ALIVE";
}
else if (isDead()) {
return "DEAD";
}
else if (isUndef()) {
return "UNDEF";
}
else {
return "INTERNAL_ERROR";
}
}
/**
* Query the subject queue, can be peeking or consuming.
* @param querySpec Can be configured to be consuming
* @see <a href="http://www.xmlBlaster.org/xmlBlaster/doc/requirements/engine.qos.queryspec.QueueQuery.html">The engine.qos.queryspec.QueueQuery requirement</a>
*/
public MsgUnit[] getSubjectQueueEntries(String querySpec) throws XmlBlasterException {
if (this.queueQueryPlugin == null) {
synchronized (this) {
if (this.queueQueryPlugin == null) {
this.queueQueryPlugin = new QueueQueryPlugin(this.glob);
}
}
}
return this.queueQueryPlugin.query(this.subjectQueue, querySpec);
}
//=========== Enforced by I_AdminSubject and SubjectInfoProtector.java ================
/**
* @return startupTime in seconds
*/
long getUptime() {
return (System.currentTimeMillis() - this.startupTime)/1000L;
}
public final String getCreationDate() {
long ll = this.startupTime;
java.sql.Timestamp tt = new java.sql.Timestamp(ll);
return tt.toString();
}
/**
* How many update where sent for this client, the sum of all session and
* subject queues of this clients.
*/
long getNumUpdate() {
long numUpdates = this.dispatchStatistic.getNumUpdate(); // The sessions which disappeared already are remembered here
SessionInfo[] sessions = getSessions();
for (int i=0; i<sessions.length; i++) {
SessionInfo sessionInfo = sessions[i];
numUpdates += sessionInfo.getNumUpdate();
}
return numUpdates;
}
long getSubjectQueueNumMsgs() {
return subjectQueue.getNumOfEntries();
}
long getSubjectQueueMaxMsgs() {
return subjectQueue.getMaxNumOfEntries();
}
public boolean isBlockClientLogin() {
return blockClientLogin;
}
public String setBlockClientLogin(boolean blockClient) {
if (this.blockClientLogin == blockClient)
return "Client is alread in state blocking=" + blockClient;
this.blockClientLogin = blockClient;
String text = blockClient ? "" + getNumAliveSessions() + " ALIVE clients remain logged in, new ones are blocked"
: "Blocking of " + getId() + " is switched off";
log.info(text);
if (blockClient == false) {
SessionInfo[] sessionInfos = getSessions();
for (int i = 0; i < sessionInfos.length; i++) {
sessionInfos[i].setBlockClientSessionLogin(false);
}
}
return text;
}
public String blockClientAndResetConnections() {
setBlockClientLogin(true);
StringBuffer buf = new StringBuffer(512);
SessionInfo[] sessionInfos = getSessions();
for (int i = 0; i < sessionInfos.length; i++) {
if (i > 0)
buf.append("\n");
String ret = sessionInfos[i].disconnectClientKeepSession();
buf.append(ret);
}
if (buf.length() < 1)
buf.append("No client connections to shutdown");
log.info(buf.toString());
return buf.toString();
}
/**
* Access the number of sessions of this user.
* @return The number of sessions of this user
*/
int getNumSessions() {
return getSessions().length;
}
public int getNumAliveSessions() {
int countAlive = 0;
SessionInfo[] sessionInfos = getSessions();
for (int i = 0; i < sessionInfos.length; i++) {
if (sessionInfos[i].isAlive())
countAlive++;
}
return countAlive;
}
/**
* @return The max allowed simultaneous logins of this user
*/
int getMaxSessions() {
return this.maxSessions;
}
/**
* JMX access.
* @param Change the max allowed simultaneous logins of this user
*/
void setMaxSessions(int max) {
this.maxSessions = max;
}
/**
* Access a list of public session identifier e.g. "1,5,7,12"
* @return An empty string if no sessions available
*/
String getSessionList() {
int numSessions = getNumSessions();
if (numSessions < 1)
return "";
StringBuffer sb = new StringBuffer(numSessions * 30);
SessionInfo[] sessions = getSessions();
for (int i=0; i<sessions.length; i++) {
if (sb.length() > 0)
sb.append(",");
sb.append(sessions[i].getPublicSessionId());
}
return sb.toString();
}
/**
* Find a session by its public session ID.
* @param pubSessionId e.g. "-2"
* @return I_AdminSession or null if not found
*/
I_AdminSession getSessionByPubSessionId(long pubSessionId) {
SessionInfo sessionInfo = getSessionByPublicId(pubSessionId);
return (sessionInfo == null) ? null : sessionInfo.getSessionInfoProtector();
}
/**
* Kills all sessions of this client
* @return The list of killed sessions (public session IDs), in a human readable string
*/
String killClient() throws XmlBlasterException {
int numSessions = getNumSessions();
long num = getSubjectQueueNumMsgs();
if (numSessions < 1) {
shutdown(true, true);
return getId() + " killed. No sessions where available, but destroyed " + num + " subject queue messages";
}
String sessionList = getSessionList();
while (true) {
SessionInfo sessionInfo = null;
synchronized (sessionMap) {
Iterator iterator = sessionMap.values().iterator();
if (!iterator.hasNext())
break;
sessionInfo = (SessionInfo)iterator.next();
}
sessionInfo.killSession();
}
/* The upper form is probably better
SessionInfo[] sessions = getSessions();
for (int ii=0; ii<sessions.length; ii++) {
sessions[ii].killSession();
}
*/
String post = (num == 0) ? "" : " Destroyed " + num + " subject queue messages";
return getId() + " sessions " + sessionList + " killed." + post;
}
public String[] peekSubjectMessages(int numOfEntries) throws XmlBlasterException {
return this.glob.peekMessages(this.subjectQueue, numOfEntries, "subject");
}
public String[] peekSubjectMessagesToFile(int numOfEntries, String path) throws Exception {
try {
return this.glob.peekQueueMessagesToFile(this.subjectQueue, numOfEntries, path, "subject");
}
catch (XmlBlasterException e) {
throw new Exception(e.toString());
}
}
/** JMX */
public java.lang.String usage() {
return ServerScope.getJmxUsageLinkInfo(this.getClass().getName(), null);
}
/** JMX */
public java.lang.String getUsageUrl() {
return ServerScope.getJavadocUrl(this.getClass().getName(), null);
}
/* JMX dummy to have a copy/paste functionality in jconsole */
public void setUsageUrl(java.lang.String url) {}
/**
* JMX: Enforced by interface NotificationBroadcasterSupport
*/
public MBeanNotificationInfo[] getNotificationInfo() {
String[] types = new String[] {
AttributeChangeNotification.ATTRIBUTE_CHANGE
};
String name = AttributeChangeNotification.class.getName();
String description = "TODO: An attribute of this MBean has changed";
MBeanNotificationInfo info =
new MBeanNotificationInfo(types, name, description);
return new MBeanNotificationInfo[] {info};
}
}