blob: 5d57c0a7c7660dcffb6c55c199757f8a309e5fa8 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package org.apache.vysper.xmpp.state.resourcebinding;
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.server.InternalSessionContext;
import org.apache.vysper.xmpp.uuid.JVMBuiltinUUIDGenerator;
import org.apache.vysper.xmpp.uuid.UUIDGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* assigns and holds resource ids and their related session
*
* @author The Apache MINA Project (dev@mina.apache.org)
*/
public class DefaultResourceRegistry implements InternalResourceRegistry {
final Logger logger = LoggerFactory.getLogger(DefaultResourceRegistry.class);
private static class SessionData {
private final InternalSessionContext context;
private ResourceState state;
private Integer priority;
SessionData(InternalSessionContext 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(InternalSessionContext 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();
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);
}
}
}
return 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;
synchronized (boundResources) {
synchronized (entityResources) {
synchronized (sessionResources) {
SessionContext sessionContext = getSessionContext(resourceId);
// remove from entity's list of resources
List<String> resourceListForEntity = getResourceList(sessionContext.getInitiatingEntity());
if (resourceListForEntity != null) {
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 = getResourcesForSessionInternal(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 = getResourcesForSessionInternal(sessionContext);
if (list != null && list.size() == 1)
return list.get(0);
return null;
}
public List<String> getResourcesForSession(SessionContext sessionContext) {
return Collections.unmodifiableList(getResourcesForSessionInternal(sessionContext));
}
/*package*/List<String> getResourcesForSessionInternal(SessionContext sessionContext) {
if (sessionContext == null)
return null;
List<String> resourceList = sessionResources.get(sessionContext);
if (resourceList == null)
resourceList = Collections.emptyList();
return resourceList;
}
public InternalSessionContext 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<InternalSessionContext> getSessions(Entity entity) {
List<InternalSessionContext> sessionContexts = new ArrayList<>();
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<InternalSessionContext> getSessions(Entity entity, Integer prioThreshold) {
if (prioThreshold == null)
prioThreshold = 0;
List<InternalSessionContext> results = new ArrayList<>();
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<InternalSessionContext> getHighestPrioSessions(Entity entity, Integer prioThreshold) {
Integer currentPrio = prioThreshold == null ? Integer.MIN_VALUE : prioThreshold;
List<InternalSessionContext> results = new ArrayList<>();
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>();
if (resources == null) return result;
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>();
if (resources == null) return result;
for (String resource : resources) {
ResourceState resourceState = getResourceState(resource);
if (resourceState == AVAILABLE || resourceState == AVAILABLE_INTERESTED) {
result.add(resource);
}
}
return result;
}
}