/* * 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.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 ResourceRegistry { final Logger logger = LoggerFactory.getLogger(DefaultResourceRegistry.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(); 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 = false; synchronized (boundResources) { synchronized (entityResources) { synchronized (sessionResources) { SessionContext sessionContext = getSessionContext(resourceId); // 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 = 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 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; } }