/*
* 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.core.im.handler;
import static org.apache.vysper.compliance.SpecCompliant.ComplianceStatus.IN_PROGRESS;
import static org.apache.vysper.compliance.SpecCompliant.ComplianceStatus.NOT_STARTED;
import static org.apache.vysper.xmpp.modules.roster.AskSubscriptionType.ASK_SUBSCRIBE;
import static org.apache.vysper.xmpp.modules.roster.AskSubscriptionType.ASK_SUBSCRIBED;
import static org.apache.vysper.xmpp.modules.roster.RosterSubscriptionMutator.Result.ALREADY_SET;
import static org.apache.vysper.xmpp.modules.roster.RosterSubscriptionMutator.Result.OK;
import static org.apache.vysper.xmpp.modules.roster.SubscriptionType.FROM;
import static org.apache.vysper.xmpp.modules.roster.SubscriptionType.NONE;
import static org.apache.vysper.xmpp.modules.roster.SubscriptionType.TO;
import static org.apache.vysper.xmpp.stanza.PresenceStanzaType.SUBSCRIBED;
import static org.apache.vysper.xmpp.stanza.PresenceStanzaType.isSubscriptionType;
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.StanzaRelay;
import org.apache.vysper.xmpp.delivery.failure.DeliveryException;
import org.apache.vysper.xmpp.delivery.failure.IgnoreFailureStrategy;
import org.apache.vysper.xmpp.modules.roster.RosterException;
import org.apache.vysper.xmpp.modules.roster.RosterItem;
import org.apache.vysper.xmpp.modules.roster.RosterStanzaUtils;
import org.apache.vysper.xmpp.modules.roster.RosterSubscriptionMutator;
import org.apache.vysper.xmpp.modules.roster.persistence.RosterManager;
import org.apache.vysper.xmpp.server.ServerRuntimeContext;
import org.apache.vysper.xmpp.server.SessionContext;
import org.apache.vysper.xmpp.stanza.PresenceStanza;
import org.apache.vysper.xmpp.stanza.PresenceStanzaType;
import org.apache.vysper.xmpp.stanza.Stanza;
import org.apache.vysper.xmpp.stanza.XMPPCoreStanzaVerifier;
import org.apache.vysper.xmpp.state.resourcebinding.ResourceRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* handling presence stanzas of type subscription
*
* TODO: review all the printStackTraces and throws and turn them into logs or stanza errors
*
* @author The Apache MINA Project (dev@mina.apache.org)
*/
public class PresenceSubscriptionHandler extends AbstractPresenceSpecializedHandler {
final Logger logger = LoggerFactory.getLogger(PresenceSubscriptionHandler.class);
@Override
/*package*/Stanza executeCorePresence(ServerRuntimeContext serverRuntimeContext, boolean isOutboundStanza,
SessionContext sessionContext, PresenceStanza presenceStanza, RosterManager rosterManager) {
if (!isSubscriptionType(presenceStanza.getPresenceType())) {
throw new RuntimeException("case not handled in availability handler"
+ presenceStanza.getPresenceType().value());
}
// TODO: either use the resource associated with the session
// (initiatingEntity)
// or in case of multiple resources, use the from attribute or return an
// error if the from attribute is not present.
Entity initiatingEntity = sessionContext == null ? null : sessionContext.getInitiatingEntity();
XMPPCoreStanzaVerifier verifier = presenceStanza.getCoreVerifier();
ResourceRegistry registry = serverRuntimeContext.getResourceRegistry();
PresenceStanzaType type = presenceStanza.getPresenceType();
if (isOutboundStanza) {
// this is an outbound subscription
// request/approval/cancellation/unsubscription
// stamp it with the bare JID of the user
Entity user = initiatingEntity;
PresenceStanza stampedStanza = buildPresenceStanza(user.getBareJID(), presenceStanza.getTo().getBareJID(),
presenceStanza.getPresenceType(), null);
switch (type) {
case SUBSCRIBE:
// RFC3921bis-04#3.1.2
// user requests subsription to contact
handleOutboundSubscriptionRequest(stampedStanza, serverRuntimeContext, sessionContext, registry,
rosterManager);
break;
case SUBSCRIBED:
// RFC3921bis-04#3.1.5
// user approves subscription to requesting contact
handleOutboundSubscriptionApproval(stampedStanza, serverRuntimeContext, sessionContext, registry,
rosterManager);
break;
case UNSUBSCRIBE:
// RFC3921bis-04#3.3.2
// user removes subscription from contact
handleOutboundUnsubscription(stampedStanza, serverRuntimeContext, sessionContext, registry,
rosterManager);
break;
case UNSUBSCRIBED:
// RFC3921bis-04#3.2.2
// user approves unsubscription of contact
handleOutboundSubscriptionCancellation(stampedStanza, serverRuntimeContext, sessionContext, registry,
rosterManager);
break;
default:
throw new RuntimeException("unhandled case " + type.value());
}
} else /* inbound */{
switch (type) {
case SUBSCRIBE:
// RFC3921bis-04#3.1.3
// contact requests subscription to user
return handleInboundSubscriptionRequest(presenceStanza, serverRuntimeContext, sessionContext, registry,
rosterManager);
case SUBSCRIBED:
// RFC3921bis-04#3.1.6
// contact approves user's subsription request
return handleInboundSubscriptionApproval(presenceStanza, serverRuntimeContext, sessionContext,
registry, rosterManager);
case UNSUBSCRIBE:
// RFC3921bis-04#3.3.3
// contact unsubscribes
handleInboundUnsubscription(presenceStanza, serverRuntimeContext, sessionContext, registry,
rosterManager);
return null;
case UNSUBSCRIBED:
// RFC3921bis-04#3.2.3
// contact denies subsription
handleInboundSubscriptionCancellation(presenceStanza, serverRuntimeContext, sessionContext, registry,
rosterManager);
return null;
default:
throw new RuntimeException("unhandled case " + type.value());
}
}
return null;
}
@SpecCompliance(compliant = {
@SpecCompliant(spec = "RFC3921bis-05", section = "3.3.3", status = IN_PROGRESS, comment = "current impl based hereupon"),
@SpecCompliant(spec = "RFC3921bis-08", section = "3.3.3", status = NOT_STARTED, comment = "substantial additions from bis-05 not yet taken into account") })
protected void handleInboundUnsubscription(PresenceStanza stanza, ServerRuntimeContext serverRuntimeContext,
SessionContext sessionContext, ResourceRegistry registry, RosterManager rosterManager) {
Entity contact = stanza.getFrom();
Entity user = stanza.getTo();
Entity userBareJid = user.getBareJID();
Entity contactBareJid = contact.getBareJID();
RosterItem rosterItem;
try {
rosterItem = rosterManager.getContact(userBareJid, contactBareJid);
} catch (RosterException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
if (rosterItem == null)
return;
RosterSubscriptionMutator.Result result = RosterSubscriptionMutator.getInstance().remove(rosterItem, FROM);
if (result != OK) {
// TODO
return;
}
// send roster push to all interested resources
// TODO do this only once, since inbound is multiplexed on DeliveringInboundStanzaRelay level already
List<String> resources = registry.getInterestedResources(user);
for (String resource : resources) {
Entity userResource = new EntityImpl(user, resource);
Stanza push = RosterStanzaUtils.createRosterItemPushIQ(userResource, sessionContext.nextSequenceValue(),
rosterItem);
LocalDeliveryUtils.relayToResourceDirectly(registry, resource, push);
}
}
@SpecCompliance(compliant = {
@SpecCompliant(spec = "RFC3921bis-05", section = "3.3.2", status = IN_PROGRESS, comment = "current impl based hereupon"),
@SpecCompliant(spec = "RFC3921bis-08", section = "3.3.2", status = NOT_STARTED, comment = "rephrasing from bis-05 not yet taken into account") })
protected void handleOutboundUnsubscription(PresenceStanza stanza, ServerRuntimeContext serverRuntimeContext,
SessionContext sessionContext, ResourceRegistry registry, RosterManager rosterManager) {
Entity user = stanza.getFrom();
Entity contact = stanza.getTo();
Entity userBareJid = user.getBareJID();
Entity contactBareJid = contact.getBareJID();
relayStanza(contact, stanza, sessionContext);
RosterItem rosterItem = null;
try {
rosterItem = rosterManager.getContact(userBareJid, contactBareJid);
} catch (RosterException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
if (rosterItem == null)
return;
RosterSubscriptionMutator.Result result = RosterSubscriptionMutator.getInstance().remove(rosterItem, TO);
if (result != OK) {
// TODO
return;
}
relayStanza(contact, stanza, sessionContext);
sendRosterUpdate(sessionContext, registry, user, rosterItem);
}
/**
* send roster push to all of the user's interested resources
*/
protected void sendRosterUpdate(SessionContext sessionContext, ResourceRegistry registry, Entity user,
RosterItem rosterItem) {
List<String> resources = registry.getInterestedResources(user);
for (String resource : resources) {
Entity userResource = new EntityImpl(user, resource);
Stanza push = RosterStanzaUtils.createRosterItemPushIQ(userResource, sessionContext.nextSequenceValue(),
rosterItem);
LocalDeliveryUtils.relayToResourceDirectly(registry, resource, push);
}
}
@SpecCompliance(compliant = {
@SpecCompliant(spec = "RFC3921bis-05", section = "3.2.3", status = IN_PROGRESS, comment = "current impl based hereupon"),
@SpecCompliant(spec = "RFC3921bis-08", section = "3.2.3", status = NOT_STARTED, comment = "additions from bis-05 not yet taken into account") })
protected void handleInboundSubscriptionCancellation(PresenceStanza stanza,
ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext, ResourceRegistry registry,
RosterManager rosterManager) {
Entity contact = stanza.getFrom();
Entity user = stanza.getTo();
Entity userBareJid = user.getBareJID();
Entity contactBareJid = contact.getBareJID();
RosterItem rosterItem;
try {
rosterItem = rosterManager.getContact(userBareJid, contactBareJid);
} catch (RosterException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
if (rosterItem == null)
return;
RosterSubscriptionMutator.Result result = RosterSubscriptionMutator.getInstance().remove(rosterItem, TO);
if (result != OK) {
// TODO
return;
}
// send roster push to all interested resources
// TODO do this only once, since inbound is multiplexed on DeliveringInboundStanzaRelay level already
List<String> resources = registry.getInterestedResources(user);
for (String resource : resources) {
Entity userResource = new EntityImpl(user, resource);
Stanza push = RosterStanzaUtils.createRosterItemPushIQ(userResource, sessionContext.nextSequenceValue(),
rosterItem);
LocalDeliveryUtils.relayToResourceDirectly(registry, resource, push);
}
}
@SpecCompliance(compliant = {
@SpecCompliant(spec = "RFC3921bis-05", section = "3.2.2", status = IN_PROGRESS, comment = "current impl based hereupon"),
@SpecCompliant(spec = "RFC3921bis-08", section = "3.2.2", status = NOT_STARTED, comment = "rephrasing from bis-05 not yet taken into account") })
protected void handleOutboundSubscriptionCancellation(PresenceStanza stanza,
ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext, ResourceRegistry registry,
RosterManager rosterManager) {
Entity user = stanza.getFrom();
Entity contact = stanza.getTo();
Entity userBareJid = user.getBareJID();
Entity contactBareJid = contact.getBareJID();
RosterItem rosterItem = null;
try {
rosterItem = rosterManager.getContact(userBareJid, contactBareJid);
} catch (RosterException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
if (rosterItem == null)
return;
RosterSubscriptionMutator.Result result = RosterSubscriptionMutator.getInstance().remove(rosterItem, FROM);
if (result != OK) {
// TODO
return;
}
relayStanza(contact, stanza, sessionContext);
// send roster push to all of the user's interested resources
sendRosterUpdate(sessionContext, registry, user, rosterItem);
}
@SpecCompliance(compliant = {
@SpecCompliant(spec = "RFC3921bis-05", section = "3.1.5", status = IN_PROGRESS, comment = "current impl based hereupon"),
@SpecCompliant(spec = "RFC3921bis-08", section = "3.1.5", status = NOT_STARTED, comment = "slight changes from bis-05 not yet taken into account") })
protected void handleOutboundSubscriptionApproval(PresenceStanza stanza, ServerRuntimeContext serverRuntimeContext,
SessionContext sessionContext, ResourceRegistry registry, RosterManager rosterManager) {
Entity user = stanza.getFrom();
Entity contact = stanza.getTo();
Entity userBareJid = user.getBareJID();
Entity contactBareJid = contact.getBareJID();
RosterItem rosterItem = null;
try {
rosterItem = getExistingOrNewRosterItem(rosterManager, userBareJid, contactBareJid);
RosterSubscriptionMutator.Result result = RosterSubscriptionMutator.getInstance().add(rosterItem, FROM);
if (result != OK) {
// TODO
return;
}
rosterManager.addContact(userBareJid, rosterItem);
} catch (RosterException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
relayStanza(contact, stanza, sessionContext);
// send roster push to all of the user's interested resources
sendRosterUpdate(sessionContext, registry, user, rosterItem);
// send presence information from user's available resource to the
// contact
List<String> resources = registry.getAvailableResources(user);
for (String resource : resources) {
Entity userResource = new EntityImpl(user, resource);
PresenceStanza cachedPresenceStanza = sessionContext.getServerRuntimeContext().getPresenceCache().get(
userResource);
if (cachedPresenceStanza == null)
continue;
PresenceStanza sendoutPresence = buildPresenceStanza(userResource, contactBareJid, null,
cachedPresenceStanza.getInnerElements());
relayStanza(contact, sendoutPresence, sessionContext);
}
}
private RosterItem getExistingOrNewRosterItem(RosterManager rosterManager, Entity userJid, Entity contactJid)
throws RosterException {
RosterItem rosterItem = rosterManager.getContact(userJid, contactJid);
if (rosterItem == null) {
rosterItem = new RosterItem(contactJid, NONE);
}
return rosterItem;
}
/**
* TODO this handling method should be optimized to be processed only once for every session
* DeliveringInboundStanzaRelay call this for every resource separately
*/
@SpecCompliance(compliant = {
@SpecCompliant(spec = "RFC3921bis-05", section = "3.1.6", status = IN_PROGRESS, comment = "current impl based hereupon"),
@SpecCompliant(spec = "RFC3921bis-08", section = "3.1.6", status = NOT_STARTED, comment = "minor rephrasing from bis-05 not yet taken into account") })
protected Stanza handleInboundSubscriptionApproval(PresenceStanza stanza,
ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext, ResourceRegistry registry,
RosterManager rosterManager) {
Entity contact = stanza.getFrom();
Entity user = stanza.getTo();
Entity userBareJid = user.getBareJID();
RosterItem rosterItem;
RosterSubscriptionMutator.Result result;
try {
rosterItem = getExistingOrNewRosterItem(rosterManager, userBareJid, contact);
result = RosterSubscriptionMutator.getInstance().add(rosterItem, TO);
rosterManager.addContact(userBareJid, rosterItem);
} catch (RosterException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
if (result == OK || result == ALREADY_SET) {
// send roster push to all interested resources
// TODO do this only once, since inbound is multiplexed on DeliveringInboundStanzaRelay level already
List<String> resources = registry.getInterestedResources(user);
for (String resource : resources) {
Entity userResource = new EntityImpl(user, resource);
Stanza push = RosterStanzaUtils.createRosterItemPushIQ(userResource,
sessionContext.nextSequenceValue(), rosterItem);
LocalDeliveryUtils.relayToResourceDirectly(registry, resource, push);
}
} else {
// silently drop the stanza
}
return null;
}
@SpecCompliance(compliant = {
@SpecCompliant(spec = "RFC3921bis-05", section = "3.1.3", status = IN_PROGRESS, comment = "current impl based hereupon"),
@SpecCompliant(spec = "RFC3921bis-08", section = "3.1.3", status = NOT_STARTED, comment = "major rephrasing from bis-05 not yet taken into account") })
protected Stanza handleInboundSubscriptionRequest(PresenceStanza stanza, ServerRuntimeContext serverRuntimeContext,
SessionContext sessionContext, ResourceRegistry registry, RosterManager rosterManager) {
Entity contact = stanza.getFrom();
Entity user = stanza.getTo();
Entity userBareJid = user.getBareJID();
RosterItem rosterItem;
RosterSubscriptionMutator.Result result;
try {
rosterItem = getExistingOrNewRosterItem(rosterManager, userBareJid, contact);
result = RosterSubscriptionMutator.getInstance().add(rosterItem, ASK_SUBSCRIBED);
rosterManager.addContact(userBareJid, rosterItem);
} catch (RosterException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
// check whether user already has a subscription to contact
if (result == ALREADY_SET) {
Entity receiver = contact.getBareJID();
PresenceStanza alreadySubscribedResponse = buildPresenceStanza(userBareJid, receiver, SUBSCRIBED, null);
relayStanza(receiver, alreadySubscribedResponse, sessionContext);
return null;
}
// user exists and doesn't have a subscription, so...
// TODO check if user has blocked contact
// write inbound subscription request to the user
sessionContext.getResponseWriter().write(stanza);
return null;
}
@SpecCompliance(compliant = {
@SpecCompliant(spec = "RFC3921bis-05", section = "3.1.2", status = IN_PROGRESS, comment = "current impl based hereupon"),
@SpecCompliant(spec = "RFC3921bis-08", section = "3.1.2", status = NOT_STARTED, comment = "major rephrasing from bis-05 not yet taken into account") })
protected void handleOutboundSubscriptionRequest(PresenceStanza stanza, ServerRuntimeContext serverRuntimeContext,
SessionContext sessionContext, ResourceRegistry registry, RosterManager rosterManager) {
StanzaRelay stanzaRelay = serverRuntimeContext.getStanzaRelay();
Entity user = stanza.getFrom();
Entity contact = stanza.getTo().getBareJID();
// TODO schedule a observer which can re-send the request
RosterItem rosterItem = null;
try {
rosterItem = getExistingOrNewRosterItem(rosterManager, user.getBareJID(), contact);
RosterSubscriptionMutator.Result result = RosterSubscriptionMutator.getInstance().add(rosterItem,
ASK_SUBSCRIBE);
if (result != OK) {
return;
}
rosterManager.addContact(user.getBareJID(), rosterItem);
} catch (RosterException e) {
throw new RuntimeException(e);
}
// relay the stanza to the contact (via the contact's server)
try {
stanzaRelay.relay(stanza.getTo(), stanza, new IgnoreFailureStrategy());
} catch (DeliveryException e) {
e.printStackTrace();
}
// send roster push to all of the user's interested resources
List<String> resources = registry.getInterestedResources(user);
for (String resource : resources) {
Entity userResource = new EntityImpl(user, resource);
Stanza push = RosterStanzaUtils.createRosterItemPushIQ(userResource, sessionContext.nextSequenceValue(),
rosterItem);
LocalDeliveryUtils.relayToResourceDirectly(registry, resource, push);
}
}
}