/* * 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.modules.roster.handler; import static org.apache.vysper.compliance.SpecCompliant.ComplianceCoverage.COMPLETE; import static org.apache.vysper.compliance.SpecCompliant.ComplianceCoverage.PARTIAL; import static org.apache.vysper.compliance.SpecCompliant.ComplianceStatus.FINISHED; import static org.apache.vysper.compliance.SpecCompliant.ComplianceStatus.IN_PROGRESS; import static org.apache.vysper.xmpp.modules.roster.SubscriptionType.NONE; import static org.apache.vysper.xmpp.modules.roster.SubscriptionType.REMOVE; import java.util.List; import org.apache.vysper.compliance.SpecCompliance; import org.apache.vysper.compliance.SpecCompliant; import org.apache.vysper.xmpp.addressing.Entity; import org.apache.vysper.xmpp.addressing.EntityImpl; import org.apache.vysper.xmpp.delivery.LocalDeliveryUtils; import org.apache.vysper.xmpp.delivery.failure.DeliveryException; import org.apache.vysper.xmpp.delivery.failure.IgnoreFailureStrategy; import org.apache.vysper.xmpp.modules.core.base.handler.DefaultIQHandler; import org.apache.vysper.xmpp.modules.roster.Roster; import org.apache.vysper.xmpp.modules.roster.RosterBadRequestException; import org.apache.vysper.xmpp.modules.roster.RosterException; import org.apache.vysper.xmpp.modules.roster.RosterItem; import org.apache.vysper.xmpp.modules.roster.RosterNotAcceptableException; import org.apache.vysper.xmpp.modules.roster.RosterStanzaUtils; import org.apache.vysper.xmpp.modules.roster.RosterUtils; import org.apache.vysper.xmpp.modules.roster.persistence.RosterManager; import org.apache.vysper.xmpp.protocol.NamespaceURIs; import org.apache.vysper.xmpp.server.ServerRuntimeContext; import org.apache.vysper.xmpp.server.SessionContext; import org.apache.vysper.xmpp.server.response.ServerErrorResponses; import org.apache.vysper.xmpp.stanza.IQStanza; import org.apache.vysper.xmpp.stanza.IQStanzaType; import org.apache.vysper.xmpp.stanza.PresenceStanzaType; import org.apache.vysper.xmpp.stanza.Stanza; import org.apache.vysper.xmpp.stanza.StanzaBuilder; import org.apache.vysper.xmpp.stanza.StanzaErrorCondition; import org.apache.vysper.xmpp.stanza.StanzaErrorType; import org.apache.vysper.xmpp.state.resourcebinding.ResourceRegistry; import org.apache.vysper.xmpp.state.resourcebinding.ResourceState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * handles roster get, set, push & result requests * * @author The Apache MINA Project (dev@mina.apache.org) */ @SpecCompliant(spec = "rfc3921bis-08", section = "2", status = IN_PROGRESS, coverage = COMPLETE) public class RosterIQHandler extends DefaultIQHandler { final Logger logger = LoggerFactory.getLogger(RosterIQHandler.class); @Override protected boolean verifyNamespace(Stanza stanza) { return verifyInnerNamespace(stanza, NamespaceURIs.JABBER_IQ_ROSTER); } @Override protected boolean verifyInnerElement(Stanza stanza) { return verifyInnerElementWorker(stanza, "query"); } @SpecCompliance(compliant = { @SpecCompliant(spec = "rfc3921bis-08", section = "2.1.2", status = FINISHED, coverage = COMPLETE), @SpecCompliant(spec = "rfc3921bis-08", section = "2.1.5", status = FINISHED, coverage = PARTIAL), @SpecCompliant(spec = "rfc3921bis-08", section = "2.2", status = FINISHED, coverage = COMPLETE) }) @Override protected Stanza handleGet(IQStanza stanza, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext) { ResourceRegistry registry = serverRuntimeContext.getResourceRegistry(); RosterManager rosterManager = (RosterManager) serverRuntimeContext.getStorageProvider(RosterManager.class); if (rosterManager == null) { return handleCannotRetrieveRoster(stanza, sessionContext); } Entity from = extractUniqueSenderJID(stanza, sessionContext); if (from == null || !from.isResourceSet()) { return ServerErrorResponses.getStanzaError(StanzaErrorCondition.UNKNOWN_SENDER, stanza, StanzaErrorType.MODIFY, "sender info insufficient: " + ((from == null) ? "no from" : from.getFullQualifiedName()), null, null); } String resourceId = from.getResource(); ResourceState currentState = registry.getResourceState(resourceId); if (currentState != null) { registry.setResourceState(resourceId, ResourceState.makeInterested(currentState)); } Roster roster = null; try { roster = rosterManager.retrieve(from.getBareJID()); if (roster == null) return handleCannotRetrieveRoster(stanza, sessionContext); } catch (RosterException e) { return handleCannotRetrieveRoster(stanza, sessionContext); } // from becomes to StanzaBuilder stanzaBuilder = RosterStanzaUtils.createRosterItemsIQ(from, stanza.getID(), IQStanzaType.RESULT, roster); return stanzaBuilder.build(); } @SpecCompliance(compliant = { @SpecCompliant(spec = "rfc3921bis-08", section = "2.1.3", status = FINISHED, coverage = COMPLETE), @SpecCompliant(spec = "rfc3921bis-08", section = "2.1.5", status = FINISHED, coverage = PARTIAL), @SpecCompliant(spec = "rfc3921bis-08", section = "2.1.6", status = FINISHED, coverage = PARTIAL, comment = "only set-related content applies"), @SpecCompliant(spec = "rfc3921bis-08", section = "2.5", status = FINISHED, coverage = COMPLETE, comment = "only calling from here") }) @Override protected Stanza handleSet(IQStanza stanza, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext) { RosterManager rosterManager = (RosterManager) serverRuntimeContext.getStorageProvider(RosterManager.class); if (rosterManager == null) { return handleCannotRetrieveRoster(stanza, sessionContext); } Entity user = extractUniqueSenderJID(stanza, sessionContext); if (user == null || !user.isResourceSet()) { return ServerErrorResponses.getStanzaError(StanzaErrorCondition.UNKNOWN_SENDER, stanza, StanzaErrorType.MODIFY, "sender info insufficient: " + ((user == null) ? "no from" : user.getFullQualifiedName()), null, null); } RosterItem setRosterItem; try { setRosterItem = RosterUtils.parseRosterItem(stanza); } catch (RosterBadRequestException e) { return ServerErrorResponses.getStanzaError(StanzaErrorCondition.BAD_REQUEST, stanza, StanzaErrorType.MODIFY, e.getMessage(), null, null); } catch (RosterNotAcceptableException e) { return ServerErrorResponses.getStanzaError(StanzaErrorCondition.NOT_ACCEPTABLE, stanza, StanzaErrorType.MODIFY, e.getMessage(), null, null); } Entity contactJid = setRosterItem.getJid().getBareJID(); RosterItem existingItem; try { existingItem = rosterManager.getContact(user.getBareJID(), contactJid); } catch (RosterException e) { existingItem = null; } if (setRosterItem.getSubscriptionType() == REMOVE) { // remove is handled in separate method, return afterwards return rosterItemRemove(stanza, sessionContext, rosterManager, user, contactJid, existingItem); } /* else: all other subscription types are ignored in a roster set and have been filtered out by RosterUtils.parseRosterItem() */ // proper set (update, not a remove) if (existingItem == null) { existingItem = new RosterItem(contactJid, NONE); } if (setRosterItem.getName() != null) { existingItem.setName(setRosterItem.getName()); logger.debug(user.getBareJID() + " roster: set roster item name to " + setRosterItem.getName()); } existingItem.setGroups(setRosterItem.getGroups()); logger.debug(user.getBareJID() + " roster: roster item groups set to " + setRosterItem.getGroups()); try { // update contact persistently rosterManager.addContact(user.getBareJID(), existingItem); } catch (RosterException e) { return ServerErrorResponses.getStanzaError(StanzaErrorCondition.BAD_REQUEST, stanza, StanzaErrorType.CANCEL, "roster item contact not (yet) in roster: " + contactJid, null, null); } pushRosterItemToInterestedResources(sessionContext, user, existingItem); return RosterStanzaUtils.createRosterItemIQ(user, stanza.getID(), IQStanzaType.RESULT, existingItem); } @SpecCompliant(spec = "rfc3921bis-08", section = "2.5", status = IN_PROGRESS, coverage = COMPLETE, comment = "actual implementation") private Stanza rosterItemRemove(IQStanza stanza, SessionContext sessionContext, RosterManager rosterManager, Entity user, Entity contactJid, RosterItem existingItem) { // rfc3921bis-08/2.5 Stanza unsubscribedStanza = null; Stanza unsubscribeStanza = null; if (existingItem != null) { if (existingItem.hasFrom()) { // send unsubbed unsubscribedStanza = StanzaBuilder.createPresenceStanza(user.getBareJID(), contactJid, null, PresenceStanzaType.UNSUBSCRIBED, null, null).build(); } if (existingItem.hasTo()) { // send unsub unsubscribeStanza = StanzaBuilder.createPresenceStanza(user.getBareJID(), contactJid, null, PresenceStanzaType.UNSUBSCRIBE, null, null).build(); } } try { rosterManager.removeContact(user.getBareJID(), contactJid); } catch (RosterException e) { return ServerErrorResponses.getStanzaError(StanzaErrorCondition.ITEM_NOT_FOUND, stanza, StanzaErrorType.CANCEL, "roster item contact not in roster: " + contactJid, null, null); } if (unsubscribedStanza != null) { try { sessionContext.getServerRuntimeContext().getStanzaRelay().relay(contactJid, unsubscribedStanza, new IgnoreFailureStrategy()); } catch (DeliveryException e) { logger.warn("failure sending unsubscribed on roster remove", e); } } if (unsubscribeStanza != null) { try { sessionContext.getServerRuntimeContext().getStanzaRelay().relay(contactJid, unsubscribeStanza, new IgnoreFailureStrategy()); } catch (DeliveryException e) { logger.warn("failure sending unsubscribe on roster remove", e); } } // send roster item push to all interested resources pushRosterItemToInterestedResources(sessionContext, user, new RosterItem(contactJid, REMOVE)); // return success return StanzaBuilder.createIQStanza(null, user, IQStanzaType.RESULT, stanza.getID()).build(); } private void pushRosterItemToInterestedResources(SessionContext sessionContext, Entity user, RosterItem rosterItem) { ResourceRegistry registry = sessionContext.getServerRuntimeContext().getResourceRegistry(); List<String> resources = registry.getInterestedResources(user.getBareJID()); for (String resource : resources) { Entity userResource = new EntityImpl(user, resource); Stanza push = RosterStanzaUtils.createRosterItemPushIQ(userResource, sessionContext.nextSequenceValue(), rosterItem); LocalDeliveryUtils.relayToResourceDirectly(registry, resource, push); } } protected Stanza handleCannotRetrieveRoster(IQStanza stanza, SessionContext sessionContext) { throw new RuntimeException("gracefully handling roster management problem not implemented"); } }