package nl.topicus.konijn.xmpp.util;
import static org.apache.vysper.xmpp.state.resourcebinding.ResourceState.AVAILABLE;
import static org.apache.vysper.xmpp.state.resourcebinding.ResourceState.AVAILABLE_INTERESTED;
import static org.apache.vysper.xmpp.state.resourcebinding.ResourceState.CONNECTED;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.vysper.xmpp.addressing.Entity;
import org.apache.vysper.xmpp.server.SessionContext;
import org.apache.vysper.xmpp.state.resourcebinding.ResourceRegistry;
import org.apache.vysper.xmpp.state.resourcebinding.ResourceState;
import org.apache.vysper.xmpp.uuid.JVMBuiltinUUIDGenerator;
import org.apache.vysper.xmpp.uuid.UUIDGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Custom resourceregistry, derived from the original resourceregistry.
*
* @author Joost Limburg
*
*/
public class CustomResourceRegistry implements ResourceRegistry {
final Logger logger = LoggerFactory.getLogger(ResourceRegistry.class);
private static class SessionData {
private final SessionContext context;
private ResourceState state;
private Integer priority;
SessionData(SessionContext context, ResourceState status,
Integer priority) {
this.context = context;
this.state = status;
this.priority = priority == null ? 0 : priority;
}
}
private UUIDGenerator resourceIdGenerator = new JVMBuiltinUUIDGenerator();
/**
* maps resource id to session. note: two resources may point to the same
* session, but often this is a 1:1 relationship
*/
protected final Map<String, SessionData> boundResources = new HashMap<String, SessionData>();
/**
* an entity's list of resources maps bare JID to all its bound resources.
* the list of resource ids might not be emtpy, and if there is more than
* one id, the list usually spans more than 1 session
*/
protected final Map<Entity, List<String>> entityResources = new HashMap<Entity, List<String>>();
/**
* a session's list of resources maps a session to all the resource ids
* bound to it.
*/
protected final Map<SessionContext, List<String>> sessionResources = new HashMap<SessionContext, List<String>>();
/**
* allocates new resource ID for the given session and binds it to the
* session
*
* @param sessionContext
* @return newly allocated resource id
*/
public String bindSession(SessionContext sessionContext) {
if (sessionContext == null) {
throw new IllegalArgumentException("session context cannot be NULL");
}
if (sessionContext.getInitiatingEntity() == null) {
throw new IllegalStateException(
"session context must have a initiating entity set");
}
String resourceId = resourceIdGenerator.create();
bindSession(resourceId, sessionContext);
return resourceId;
}
public void bindSession(String resourceId, SessionContext sessionContext) {
synchronized (boundResources) {
synchronized (entityResources) {
synchronized (sessionResources) {
// record session for the resource id
boundResources.put(resourceId, new SessionData(
sessionContext, CONNECTED, 0));
Entity initiatingEntity = sessionContext
.getInitiatingEntity();
List<String> resourceForEntityList = getResourceList(initiatingEntity);
if (resourceForEntityList == null) {
resourceForEntityList = new ArrayList<String>(1);
entityResources.put(getBareEntity(initiatingEntity),
resourceForEntityList);
}
resourceForEntityList.add(resourceId);
logger.info(
"added resource no. "
+ resourceForEntityList.size()
+ " to entity {} <- {}",
initiatingEntity.getFullQualifiedName(), resourceId);
List<String> resourcesForSessionList = sessionResources
.get(sessionContext);
if (resourcesForSessionList == null) {
resourcesForSessionList = new ArrayList<String>(1);
sessionResources.put(sessionContext,
resourcesForSessionList);
}
resourcesForSessionList.add(resourceId);
logger.info(
"added resource no. "
+ resourcesForSessionList.size()
+ " to session {} <- {}",
sessionContext.getSessionId(), resourceId);
}
}
}
}
/**
* not as commonly used as #unbindSession, this method unbinds only one of
* multiple resource ids for the _same_ session. In XMPP, this is done by
* sending a stanza like <iq id='unbind_1' type='set'><unbind
* xmlns='urn:ietf:params:xml:ns:xmpp-bind'> <resource>resourceId</resource>
* </unbind></iq>
*
* @param resourceId
*/
public boolean unbindResource(String resourceId) {
boolean noResourceRemainsForSession = false;
synchronized (boundResources) {
synchronized (entityResources) {
synchronized (sessionResources) {
SessionContext sessionContext = getSessionContext(resourceId);
BunniePresenceCache cache = (BunniePresenceCache)sessionContext.getServerRuntimeContext().getPresenceCache();
cache.removeAll(sessionContext.getInitiatingEntity());
// remove from entity's list of resources
List<String> resourceListForEntity = getResourceList(sessionContext
.getInitiatingEntity());
resourceListForEntity.remove(resourceId);
if (resourceListForEntity.isEmpty())
entityResources.remove(sessionContext
.getInitiatingEntity());
// remove from session's list of resources
List<String> resourceListForSession = sessionResources
.get(sessionContext);
resourceListForSession.remove(resourceId);
noResourceRemainsForSession = resourceListForSession
.isEmpty();
if (noResourceRemainsForSession)
sessionResources.remove(sessionContext);
// remove from overall list of bound resource
boundResources.remove(resourceId);
}
}
}
return noResourceRemainsForSession;
}
/**
* unbinds a complete session, together with all its bound resources. this
* is typically done when a XMPP session end because the client sends a
* </stream:stream> or the connection is cut.
*
* @param unbindingSessionContext
* sessionContext to be unbound
*/
public void unbindSession(SessionContext unbindingSessionContext) {
if (unbindingSessionContext == null)
return;
synchronized (boundResources) {
synchronized (entityResources) {
synchronized (sessionResources) {
// collect all remove candidates
List<String> removeResourceIds = getPrivResourcesForSessionInternal(unbindingSessionContext);
// actually remove from bound resources
for (String removeResourceId : removeResourceIds) {
boundResources.remove(removeResourceId);
}
// actually remove from entity map
List<String> resourceList = getResourceList(unbindingSessionContext
.getInitiatingEntity());
if (resourceList != null) {
resourceList.removeAll(removeResourceIds);
}
// actually remove from session map
sessionResources.remove(unbindingSessionContext);
}
}
}
}
/**
* retrieves the one and only bound resource for a given session.
*
* @param sessionContext
* @return null, if a unique resource cannot be determined (there is more or
* less than 1), the resource id otherwise
*/
public String getUniqueResourceForSession(SessionContext sessionContext) {
List<String> list = getPrivResourcesForSessionInternal(sessionContext);
if (list != null && list.size() == 1)
return list.get(0);
return null;
}
public List<String> getResourcesForSession(SessionContext sessionContext) {
return Collections
.unmodifiableList(getPrivResourcesForSessionInternal(sessionContext));
}
/* package */private List<String> getPrivResourcesForSessionInternal(
SessionContext sessionContext) {
if (sessionContext == null)
return null;
List<String> resourceList = sessionResources.get(sessionContext);
if (resourceList == null)
resourceList = Collections.emptyList();
return resourceList;
}
public SessionContext getSessionContext(String resourceId) {
SessionData data = boundResources.get(resourceId);
if (data == null)
return null;
return data.context;
}
private Entity getBareEntity(Entity entity) {
return entity == null ? null : entity.getBareJID();
}
/**
* @param entity
* @return all resources bound to this entity modulo the entity's resource
* (if given)
*/
private List<String> getResourceList(Entity entity) {
return entityResources.get(getBareEntity(entity));
}
/**
* retrieve IDs of all bound resources for this entity
*/
public List<String> getBoundResources(Entity entity) {
return getBoundResources(entity, true);
}
/**
* retrieve IDs of all bound resources for this entity
*/
public List<String> getBoundResources(Entity entity, boolean considerBareID) {
// all resources for the entity
List<String> resourceList = getResourceList(entity);
if (resourceList == null)
return Collections.emptyList();
// if resource should not be considered, return all resources
if (considerBareID || entity.getResource() == null)
return Collections.unmodifiableList(resourceList);
// resource not contained, result is empty
if (!resourceList.contains(entity.getResource())) {
return Collections.emptyList();
}
// do we have a bound entity and want only their resource returned?
return Collections.singletonList(entity.getResource());
}
/**
* retrieves all sessions handling this entity. note: if given entity is not
* a bare JID, it will return only the session for the JID's resource part.
* if it's a bare JID, it will return all session for the JID.
*
* @param entity
*/
public List<SessionContext> getSessions(Entity entity) {
List<SessionContext> sessionContexts = new ArrayList<SessionContext>();
List<String> boundResources = getBoundResources(entity, false);
for (String resourceId : boundResources) {
sessionContexts.add(getSessionContext(resourceId));
}
return sessionContexts;
}
/**
* retrieves sessions with same or above threshold
*
* @param entity
* all session for the bare jid will be considered.
* @param prioThreshold
* only resources will be returned having same or higher
* priority. a common value for the threshold is 0 (zero), which
* is also the default when param is NULL.
* @return returns the sessions matching the given JID (bare) with same or
* higher priority
*/
public List<SessionContext> getSessions(Entity entity, Integer prioThreshold) {
if (prioThreshold == null)
prioThreshold = 0;
List<SessionContext> results = new ArrayList<SessionContext>();
List<String> boundResourceIds = getBoundResources(entity, true);
for (String resourceId : boundResourceIds) {
SessionData sessionData = boundResources.get(resourceId);
if (sessionData == null)
continue;
if (sessionData.priority >= prioThreshold) {
results.add(sessionData.context);
}
}
return results;
}
/**
* number of active bare ids (# of users, regardless whether they have one
* or more connected sessions)
*
* @return
*/
public long getSessionCount() {
return entityResources.size();
}
/**
* retrieves the highest prioritized session(s) for this entity.
*
* @param entity
* if this is not a bare JID, only the session for the JID's
* resource part will be returned, without looking at other
* sessions for the resource's bare JID. otherwise, in case of a
* full JID, it will return the highest prioritized sessions.
* @param prioThreshold
* if not NULL, only resources will be returned having same or
* higher priority. a common value for the threshold is 0 (zero).
* @return for a bare JID, it will return the highest prioritized sessions.
* for a full JID, it will return the related session.
*/
public List<SessionContext> getHighestPrioSessions(Entity entity,
Integer prioThreshold) {
Integer currentPrio = prioThreshold == null ? Integer.MIN_VALUE
: prioThreshold;
List<SessionContext> results = new ArrayList<SessionContext>();
boolean isResourceSet = entity.isResourceSet();
List<String> boundResourceIds = getBoundResources(entity, false);
for (String resourceId : boundResourceIds) {
SessionData sessionData = boundResources.get(resourceId);
if (sessionData == null)
continue;
if (isResourceSet) {
// if resource id matches, there can only be one result
// this overrides even parameter prio threshold
results.clear();
results.add(sessionData.context);
return results;
}
if (sessionData.priority > currentPrio) {
results.clear(); // discard all accumulated lower prio sessions
currentPrio = sessionData.priority;
results.add(sessionData.context);
} else if (sessionData.priority.intValue() == currentPrio
.intValue()) {
results.add(sessionData.context);
}
}
return results;
}
/**
* Sets the {@link ResourceState} for the given resource.
*
* @param resourceId
* the resource identifier
* @param state
* the {@link ResourceState} to set
* @return true iff the state has effectively changed
*/
public boolean setResourceState(String resourceId, ResourceState state) {
SessionData data = boundResources.get(resourceId);
if (data == null) {
throw new IllegalArgumentException("resource not registered: "
+ resourceId);
}
synchronized (data) {
boolean result = data.state != state;
data.state = state;
return result;
}
}
/**
* Gets the {@link ResourceState} of the given resource.
*
* @param resourceId
* the resource identifier
* @return the {@link ResourceState}
*/
public ResourceState getResourceState(String resourceId) {
if (resourceId == null)
return null;
SessionData data = boundResources.get(resourceId);
if (data == null)
return null;
return data.state;
}
public void setResourcePriority(String resourceId, int priority) {
if (resourceId == null)
return;
SessionData data = boundResources.get(resourceId);
if (data == null)
return;
data.priority = priority;
}
public List<String> getInterestedResources(Entity entity) {
List<String> resources = getResourceList(entity);
List<String> result = new ArrayList<String>();
for (String resource : resources) {
ResourceState resourceState = getResourceState(resource);
if (ResourceState.isInterested(resourceState))
result.add(resource);
}
return result;
}
/**
* resources which are available or even interested - an higher form of
* available.
*
* @see org.apache.vysper.xmpp.state.resourcebinding.ResourceState
*/
public List<String> getAvailableResources(Entity entity) {
List<String> resources = getResourceList(entity);
List<String> result = new ArrayList<String>();
for (String resource : resources) {
ResourceState resourceState = getResourceState(resource);
if (resourceState == AVAILABLE
|| resourceState == AVAILABLE_INTERESTED) {
result.add(resource);
}
}
return result;
}
}