Commit 39c56232 authored by Mike McCarthy's avatar Mike McCarthy Committed by michaelcmccarthy

OF-418 - Introduce a pluggable RosterItemProvider. Committing both code and...

OF-418 - Introduce a pluggable RosterItemProvider. Committing both code and documentation patch around pluggable roster item providers

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@13336 b35dd754-fafc-0310-a699-88a17e54d16e
parent 25edb24c
......@@ -50,6 +50,10 @@ messaging (IM) services using the XMPP protocol.
<a href="db-integration-guide.html">Custom Database Integration Guide</a> -
A guide to integrating Openfire authentication, user, and group data with a custom database.
</li>
<li>
<a href="pluggable-roster-support-guide.html">Pluggable Roster Support Guide</a> -
A guide to integrating Openfire rosters with an alternate store.
</li>
</ul>
<p><b>Developer Documentation:</b></p>
......
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Openfire: Pluggable Roster Support Guide</title>
<link href="style.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id="pageContainer">
<a name="top"></a>
<div id="pageHeader">
<div id="logo"></div>
<h1>Pluggable Roster Support Guide</h1>
</div>
<div class="navigation">
<a href="index.html">&laquo; Back to documentation index</a>
</div>
<div id="pageBody">
<h2>Introduction</h2>
<p>
This document provides instructions on how to configure Openfire's roster support to work
with alternate sources of roster data rather than the provided database tables.
</p>
<p>
Consider the scenario where Openfire is integrated as the chat server solution for an existing
'social' application. In this application a user is linked to other users via a relationship
of sorts, and the system of record for the relationship data is NOT the Openfire system.
Using an XMPP server such as Openfire the applications benefit from the real-time nature of
XMPP rosters and other subscription based features such as PEP, but with the caveat that
the existing relationship information has to be duplicated in Openfire and could potentially
become out of sync.
</p>
<p>
With the introduction of pluggable roster providers, Openfire can be instructed to retrieve and
modify roster data that lives in alternative locations to the standard database tables. The options
are limitless as to what this could be, e.g. an alternative database table(s), a web service, a NoSQL
database etc.
</p>
<h2>Background</h2>
<p>
This integration requires some Java knowledge in order to implement a custom roster provider
for Openfire. The skill needed will vary depending on what you are trying to achieve.
The extension approach is similar to the custom AuthProvider approach on which it is based.
</p>
<h2>The RosterItemProvider extension point</h2>
<p>
As of Openfire 3.7.2, the class RosterItemProvider has changed from being a concrete class to an interface.
The default implementation of this provider is the DefaultRosterItemProvider, which as the name suggests is the
version of this provider Openfire will use if not overriden. The DefaultRosterItemProvider uses the Openfire
database to retrieve roster information.
</p>
<p>
The steps to get Openfire using a custom RosterItemProvider are described below.
<ol>
<li>Write a class that implements RosterItemProvider, providing your own business logic.</li>
<li>Make the class available in a jar and make this available to Openfire by placing it in the lib directory.
There are numerous ways to package a jar with this class inside it, popular build systems such as Gradle and Maven
can make your life easier.</li>
<li>Set the property 'provider.roster.className' to be the full name of your class, e.g. 'com.foo.bar.MyRosterItemProvider'.
</li>
<li>Restart Openfire. Your custom class should now be handling all the roster based operations.</li>
</ol>
</p>
<h2>Frequently Asked Questions</h2>
<p>
<ol>
<li><b>Do I have to compile my custom class into the Openfire jar?</b> No, the class only needs to be visible on the Openfire classpath.</li>
<li><b>How do I ensure my custom class is visible on the Openfire classpath?</b> Just place your new custom library in the Openfire lib directory,
this will ensure it is automatically available at startup.</li>
<li><b>Will I have a degradation in performance using a custom RosterItemProvider?</b> It completely depends on your implementation. As with any
Openfire customisation or plugin, badly written code has the potential to cause Openfire to perform slower. Use performance testing tools such
as Tsung to ensure issues haven't been introduced.</li>
<li><b>How can I have my custom class connect to another DB/Web service/NoSQL store etc?</b>
This is outside of the scope of this documentation and is your choice as a developer. If you are looking to externalize properties like
connection details, the Openfire properties mechanism and the JiveGlobals class are good places to start investigating.</li>
<li><b>Can I have multiple RosterItemProviders, in a similar vein to the HybridAuthProvider?</b> No, this feature is currently not implemented.
Get involved if you feel a need to have it!</li>
</ol>
</p>
</div>
</div>
</body>
</html>
/**
* $RCSfile$
* $Revision: 1751 $
* $Date: 2005-08-07 20:08:47 -0300 (Sun, 07 Aug 2005) $
*
* Copyright (C) 2005-2008 Jive Software. All rights reserved.
*
* Licensed 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.jivesoftware.openfire.roster;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.database.SequenceManager;
import org.jivesoftware.openfire.user.UserAlreadyExistsException;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.LocaleUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
/**
* Defines the provider methods required for creating, reading, updating and deleting roster
* items.<p>
*
* Rosters are another user resource accessed via the user or chatbot's long ID. A user/chatbot
* may have zero or more roster items and each roster item may have zero or more groups. Each
* roster item is additionaly keyed on a XMPP jid. In most cases, the entire roster will be read
* in from memory and manipulated or sent to the user. However some operations will need to retrive
* specific roster items rather than the entire roster.
*
* @author Iain Shigeoka
*/
public class DefaultRosterItemProvider implements RosterItemProvider {
private static final Logger Log = LoggerFactory.getLogger(DefaultRosterItemProvider.class);
private static final String CREATE_ROSTER_ITEM =
"INSERT INTO ofRoster (username, rosterID, jid, sub, ask, recv, nick) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)";
private static final String UPDATE_ROSTER_ITEM =
"UPDATE ofRoster SET sub=?, ask=?, recv=?, nick=? WHERE rosterID=?";
private static final String DELETE_ROSTER_ITEM_GROUPS =
"DELETE FROM ofRosterGroups WHERE rosterID=?";
private static final String CREATE_ROSTER_ITEM_GROUPS =
"INSERT INTO ofRosterGroups (rosterID, rank, groupName) VALUES (?, ?, ?)";
private static final String DELETE_ROSTER_ITEM =
"DELETE FROM ofRoster WHERE rosterID=?";
private static final String LOAD_USERNAMES =
"SELECT DISTINCT username from ofRoster WHERE jid=?";
private static final String COUNT_ROSTER_ITEMS =
"SELECT COUNT(rosterID) FROM ofRoster WHERE username=?";
private static final String LOAD_ROSTER =
"SELECT jid, rosterID, sub, ask, recv, nick FROM ofRoster WHERE username=?";
private static final String LOAD_ROSTER_ITEM_GROUPS =
"SELECT rosterID,groupName FROM ofRosterGroups";
/* (non-Javadoc)
* @see org.jivesoftware.openfire.roster.RosterItemProvider#createItem(java.lang.String, org.jivesoftware.openfire.roster.RosterItem)
*/
@Override
public RosterItem createItem(String username, RosterItem item)
throws UserAlreadyExistsException
{
Connection con = null;
PreparedStatement pstmt = null;
try {
long rosterID = SequenceManager.nextID(JiveConstants.ROSTER);
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(CREATE_ROSTER_ITEM);
pstmt.setString(1, username);
pstmt.setLong(2, rosterID);
pstmt.setString(3, item.getJid().toBareJID());
pstmt.setInt(4, item.getSubStatus().getValue());
pstmt.setInt(5, item.getAskStatus().getValue());
pstmt.setInt(6, item.getRecvStatus().getValue());
pstmt.setString(7, item.getNickname());
pstmt.executeUpdate();
item.setID(rosterID);
insertGroups(rosterID, item.getGroups().iterator(), con);
}
catch (SQLException e) {
Log.warn("Error trying to insert a new row in ofRoster", e);
throw new UserAlreadyExistsException(item.getJid().toBareJID());
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
return item;
}
/* (non-Javadoc)
* @see org.jivesoftware.openfire.roster.RosterItemProvider#updateItem(java.lang.String, org.jivesoftware.openfire.roster.RosterItem)
*/
@Override
public void updateItem(String username, RosterItem item) throws UserNotFoundException {
Connection con = null;
PreparedStatement pstmt = null;
long rosterID = item.getID();
try {
con = DbConnectionManager.getConnection();
// Update existing roster item
pstmt = con.prepareStatement(UPDATE_ROSTER_ITEM);
pstmt.setInt(1, item.getSubStatus().getValue());
pstmt.setInt(2, item.getAskStatus().getValue());
pstmt.setInt(3, item.getRecvStatus().getValue());
pstmt.setString(4, item.getNickname());
pstmt.setLong(5, rosterID);
pstmt.executeUpdate();
// Close now the statement (do not wait to be GC'ed)
DbConnectionManager.fastcloseStmt(pstmt);
// Delete old group list
pstmt = con.prepareStatement(DELETE_ROSTER_ITEM_GROUPS);
pstmt.setLong(1, rosterID);
pstmt.executeUpdate();
insertGroups(rosterID, item.getGroups().iterator(), con);
}
catch (SQLException e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}
/* (non-Javadoc)
* @see org.jivesoftware.openfire.roster.RosterItemProvider#deleteItem(java.lang.String, long)
*/
@Override
public void deleteItem(String username, long rosterItemID) {
// Only try to remove the user if they exist in the roster already:
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
// Remove roster groups
pstmt = con.prepareStatement(DELETE_ROSTER_ITEM_GROUPS);
pstmt.setLong(1, rosterItemID);
pstmt.executeUpdate();
// Close now the statement (do not wait to be GC'ed)
DbConnectionManager.fastcloseStmt(pstmt);
// Remove roster
pstmt = con.prepareStatement(DELETE_ROSTER_ITEM);
pstmt.setLong(1, rosterItemID);
pstmt.executeUpdate();
}
catch (SQLException e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}
/* (non-Javadoc)
* @see org.jivesoftware.openfire.roster.RosterItemProvider#getUsernames(java.lang.String)
*/
@Override
public Iterator<String> getUsernames(String jid) {
List<String> answer = new ArrayList<String>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_USERNAMES);
pstmt.setString(1, jid);
rs = pstmt.executeQuery();
while (rs.next()) {
answer.add(rs.getString(1));
}
}
catch (SQLException e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return answer.iterator();
}
/* (non-Javadoc)
* @see org.jivesoftware.openfire.roster.RosterItemProvider#getItemCount(java.lang.String)
*/
@Override
public int getItemCount(String username) {
int count = 0;
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(COUNT_ROSTER_ITEMS);
pstmt.setString(1, username);
rs = pstmt.executeQuery();
if (rs.next()) {
count = rs.getInt(1);
}
}
catch (SQLException e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return count;
}
/* (non-Javadoc)
* @see org.jivesoftware.openfire.roster.RosterItemProvider#getItems(java.lang.String)
*/
@Override
public Iterator<RosterItem> getItems(String username) {
LinkedList<RosterItem> itemList = new LinkedList<RosterItem>();
Map<Long, RosterItem> itemsByID = new HashMap<Long, RosterItem>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// Load all the contacts in the roster
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_ROSTER);
pstmt.setString(1, username);
rs = pstmt.executeQuery();
while (rs.next()) {
// Create a new RosterItem (ie. user contact) from the stored information
RosterItem item = new RosterItem(rs.getLong(2),
new JID(rs.getString(1)),
RosterItem.SubType.getTypeFromInt(rs.getInt(3)),
RosterItem.AskType.getTypeFromInt(rs.getInt(4)),
RosterItem.RecvType.getTypeFromInt(rs.getInt(5)),
rs.getString(6),
null);
// Add the loaded RosterItem (ie. user contact) to the result
itemList.add(item);
itemsByID.put(item.getID(), item);
}
// Close the statement and result set
DbConnectionManager.fastcloseStmt(rs, pstmt);
// Set null to pstmt to be sure that it's not closed twice. It seems that
// Sybase driver is raising an error when trying to close an already closed statement.
// it2000 comment: TODO interesting, that's the only place with the sybase fix
// it2000 comment: one should move this in closeStatement()
pstmt = null;
// Load the groups for the loaded contact
if (!itemList.isEmpty()) {
StringBuilder sb = new StringBuilder(100);
sb.append(LOAD_ROSTER_ITEM_GROUPS).append(" WHERE rosterID IN (");
for (RosterItem item : itemList) {
sb.append(item.getID()).append(",");
}
sb.setLength(sb.length()-1);
sb.append(") ORDER BY rosterID, rank");
pstmt = con.prepareStatement(sb.toString());
rs = pstmt.executeQuery();
while (rs.next()) {
itemsByID.get(rs.getLong(1)).getGroups().add(rs.getString(2));
}
}
}
catch (SQLException e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return itemList.iterator();
}
/**
* Insert the groups into the given roster item.
*
* @param rosterID the roster ID of the item the groups belong to
* @param iter an iterator over the group names to insert
* @param con the database connection to use for the operation.
* @throws SQLException if an SQL exception occurs.
*/
private void insertGroups(long rosterID, Iterator<String> iter, Connection con) throws SQLException
{
PreparedStatement pstmt = null;
try {
pstmt = con.prepareStatement(CREATE_ROSTER_ITEM_GROUPS);
pstmt.setLong(1, rosterID);
for (int i = 0; iter.hasNext(); i++) {
pstmt.setInt(2, i);
String groupName = iter.next();
pstmt.setString(3, groupName);
try {
pstmt.executeUpdate();
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
}
}
finally {
DbConnectionManager.closeStatement(pstmt);
}
}
}
......@@ -129,7 +129,7 @@ public class Roster implements Cacheable, Externalizable {
//Collection<Group> userGroups = GroupManager.getInstance().getGroups(getUserJID());
// Add RosterItems that belong to the personal roster
rosterItemProvider = RosterItemProvider.getInstance();
rosterItemProvider = RosterManager.getRosterItemProvider();
Iterator<RosterItem> items = rosterItemProvider.getItems(username);
while (items.hasNext()) {
RosterItem item = items.next();
......@@ -1152,7 +1152,7 @@ public class Roster implements Cacheable, Externalizable {
presenceManager = XMPPServer.getInstance().getPresenceManager();
rosterManager = XMPPServer.getInstance().getRosterManager();
sessionManager = SessionManager.getInstance();
rosterItemProvider = RosterItemProvider.getInstance();
rosterItemProvider = RosterManager.getRosterItemProvider();
routingTable = XMPPServer.getInstance().getRoutingTable();
username = ExternalizableUtil.getInstance().readSafeUTF(in);
......
/**
* $RCSfile$
* $Revision: 1751 $
* $Date: 2005-08-07 20:08:47 -0300 (Sun, 07 Aug 2005) $
*
* Copyright (C) 2005-2008 Jive Software. All rights reserved.
*
* Licensed 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.jivesoftware.openfire.roster;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.database.SequenceManager;
import org.jivesoftware.openfire.user.UserAlreadyExistsException;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.LocaleUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
/**
* Defines the provider methods required for creating, reading, updating and deleting roster
* items.<p>
*
* Rosters are another user resource accessed via the user or chatbot's long ID. A user/chatbot
* may have zero or more roster items and each roster item may have zero or more groups. Each
* roster item is additionaly keyed on a XMPP jid. In most cases, the entire roster will be read
* in from memory and manipulated or sent to the user. However some operations will need to retrive
* specific roster items rather than the entire roster.
*
* @author Iain Shigeoka
*/
public class RosterItemProvider {
private static final Logger Log = LoggerFactory.getLogger(RosterItemProvider.class);
private static final String CREATE_ROSTER_ITEM =
"INSERT INTO ofRoster (username, rosterID, jid, sub, ask, recv, nick) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)";
private static final String UPDATE_ROSTER_ITEM =
"UPDATE ofRoster SET sub=?, ask=?, recv=?, nick=? WHERE rosterID=?";
private static final String DELETE_ROSTER_ITEM_GROUPS =
"DELETE FROM ofRosterGroups WHERE rosterID=?";
private static final String CREATE_ROSTER_ITEM_GROUPS =
"INSERT INTO ofRosterGroups (rosterID, rank, groupName) VALUES (?, ?, ?)";
private static final String DELETE_ROSTER_ITEM =
"DELETE FROM ofRoster WHERE rosterID=?";
private static final String LOAD_USERNAMES =
"SELECT DISTINCT username from ofRoster WHERE jid=?";
private static final String COUNT_ROSTER_ITEMS =
"SELECT COUNT(rosterID) FROM ofRoster WHERE username=?";
private static final String LOAD_ROSTER =
"SELECT jid, rosterID, sub, ask, recv, nick FROM ofRoster WHERE username=?";
private static final String LOAD_ROSTER_ITEM_GROUPS =
"SELECT rosterID,groupName FROM ofRosterGroups";
private static RosterItemProvider instance = new RosterItemProvider();
public static RosterItemProvider getInstance() {
return instance;
}
/**
* Creates a new roster item for the given user (optional operation).<p>
*
* <b>Important!</b> The item passed as a parameter to this method is strictly a convenience
* for passing all of the data needed for a new roster item. The roster item returned from the
* method will be cached by Openfire. In some cases, the roster item passed in will be passed
* back out. However, if an implementation may return RosterItems as a separate class
* (for example, a RosterItem that directly accesses the backend storage, or one that is an
* object in an object database).<p>
*
* @param username the username of the user/chatbot that owns the roster item.
* @param item the settings for the roster item to create.
* @return the new roster item.
* @throws UserAlreadyExistsException if a roster item with the username already exists.
*/
public RosterItem createItem(String username, RosterItem item)
throws UserAlreadyExistsException
{
Connection con = null;
PreparedStatement pstmt = null;
try {
long rosterID = SequenceManager.nextID(JiveConstants.ROSTER);
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(CREATE_ROSTER_ITEM);
pstmt.setString(1, username);
pstmt.setLong(2, rosterID);
pstmt.setString(3, item.getJid().toBareJID());
pstmt.setInt(4, item.getSubStatus().getValue());
pstmt.setInt(5, item.getAskStatus().getValue());
pstmt.setInt(6, item.getRecvStatus().getValue());
pstmt.setString(7, item.getNickname());
pstmt.executeUpdate();
item.setID(rosterID);
insertGroups(rosterID, item.getGroups().iterator(), con);
}
catch (SQLException e) {
Log.warn("Error trying to insert a new row in ofRoster", e);
throw new UserAlreadyExistsException(item.getJid().toBareJID());
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
return item;
}
/**
* Update the roster item in storage with the information contained in the given item
* (optional operation).<p>
*
* If you don't want roster items edited through openfire, throw UnsupportedOperationException.
*
* @param username the username of the user/chatbot that owns the roster item
* @param item The roster item to update
* @throws UserNotFoundException If no entry could be found to update
*/
public void updateItem(String username, RosterItem item) throws UserNotFoundException {
Connection con = null;
PreparedStatement pstmt = null;
long rosterID = item.getID();
try {
con = DbConnectionManager.getConnection();
// Update existing roster item
pstmt = con.prepareStatement(UPDATE_ROSTER_ITEM);
pstmt.setInt(1, item.getSubStatus().getValue());
pstmt.setInt(2, item.getAskStatus().getValue());
pstmt.setInt(3, item.getRecvStatus().getValue());
pstmt.setString(4, item.getNickname());
pstmt.setLong(5, rosterID);
pstmt.executeUpdate();
// Close now the statement (do not wait to be GC'ed)
DbConnectionManager.fastcloseStmt(pstmt);
// Delete old group list
pstmt = con.prepareStatement(DELETE_ROSTER_ITEM_GROUPS);
pstmt.setLong(1, rosterID);
pstmt.executeUpdate();
insertGroups(rosterID, item.getGroups().iterator(), con);
}
catch (SQLException e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}
/**
* Delete the roster item with the given itemJID for the user (optional operation).<p>
*
* If you don't want roster items deleted through openfire, throw
* UnsupportedOperationException.
*
* @param username the long ID of the user/chatbot that owns the roster item
* @param rosterItemID The roster item to delete
*/
public void deleteItem(String username, long rosterItemID) {
// Only try to remove the user if they exist in the roster already:
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
// Remove roster groups
pstmt = con.prepareStatement(DELETE_ROSTER_ITEM_GROUPS);
pstmt.setLong(1, rosterItemID);
pstmt.executeUpdate();
// Close now the statement (do not wait to be GC'ed)
DbConnectionManager.fastcloseStmt(pstmt);
// Remove roster
pstmt = con.prepareStatement(DELETE_ROSTER_ITEM);
pstmt.setLong(1, rosterItemID);
pstmt.executeUpdate();
}
catch (SQLException e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}
/**
* Returns an iterator on the usernames whose roster includes the specified JID.
*
* @param jid the jid that the rosters should include.
* @return an iterator on the usernames whose roster includes the specified JID.
*/
public Iterator<String> getUsernames(String jid) {
List<String> answer = new ArrayList<String>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_USERNAMES);
pstmt.setString(1, jid);
rs = pstmt.executeQuery();
while (rs.next()) {
answer.add(rs.getString(1));
}
}
catch (SQLException e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return answer.iterator();
}
/**
* Obtain a count of the number of roster items available for the given user.
*
* @param username the username of the user/chatbot that owns the roster items
* @return The number of roster items available for the user
*/
public int getItemCount(String username) {
int count = 0;
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(COUNT_ROSTER_ITEMS);
pstmt.setString(1, username);
rs = pstmt.executeQuery();
if (rs.next()) {
count = rs.getInt(1);
}
}
catch (SQLException e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return count;
}
/**
* Retrieve an iterator of RosterItems for the given user.<p>
*
* This method will commonly be called when a user logs in. The data will be cached
* in memory when possible. However, some rosters may be very large so items may need
* to be retrieved from the provider more frequently than usual for provider data.
*
* @param username the username of the user/chatbot that owns the roster items
* @return An iterator of all RosterItems owned by the user
*/
public Iterator<RosterItem> getItems(String username) {
LinkedList<RosterItem> itemList = new LinkedList<RosterItem>();
Map<Long, RosterItem> itemsByID = new HashMap<Long, RosterItem>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// Load all the contacts in the roster
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_ROSTER);
pstmt.setString(1, username);
rs = pstmt.executeQuery();
while (rs.next()) {
// Create a new RosterItem (ie. user contact) from the stored information
RosterItem item = new RosterItem(rs.getLong(2),
new JID(rs.getString(1)),
RosterItem.SubType.getTypeFromInt(rs.getInt(3)),
RosterItem.AskType.getTypeFromInt(rs.getInt(4)),
RosterItem.RecvType.getTypeFromInt(rs.getInt(5)),
rs.getString(6),
null);
// Add the loaded RosterItem (ie. user contact) to the result
itemList.add(item);
itemsByID.put(item.getID(), item);
}
// Close the statement and result set
DbConnectionManager.fastcloseStmt(rs, pstmt);
// Set null to pstmt to be sure that it's not closed twice. It seems that
// Sybase driver is raising an error when trying to close an already closed statement.
// it2000 comment: TODO interesting, that's the only place with the sybase fix
// it2000 comment: one should move this in closeStatement()
pstmt = null;
// Load the groups for the loaded contact
if (!itemList.isEmpty()) {
StringBuilder sb = new StringBuilder(100);
sb.append(LOAD_ROSTER_ITEM_GROUPS).append(" WHERE rosterID IN (");
for (RosterItem item : itemList) {
sb.append(item.getID()).append(",");
}
sb.setLength(sb.length()-1);
sb.append(") ORDER BY rosterID, rank");
pstmt = con.prepareStatement(sb.toString());
rs = pstmt.executeQuery();
while (rs.next()) {
itemsByID.get(rs.getLong(1)).getGroups().add(rs.getString(2));
}
}
}
catch (SQLException e) {
Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return itemList.iterator();
}
/**
* Insert the groups into the given roster item.
*
* @param rosterID the roster ID of the item the groups belong to
* @param iter an iterator over the group names to insert
* @param con the database connection to use for the operation.
* @throws SQLException if an SQL exception occurs.
*/
private void insertGroups(long rosterID, Iterator<String> iter, Connection con) throws SQLException
{
PreparedStatement pstmt = null;
try {
pstmt = con.prepareStatement(CREATE_ROSTER_ITEM_GROUPS);
pstmt.setLong(1, rosterID);
for (int i = 0; iter.hasNext(); i++) {
pstmt.setInt(2, i);
String groupName = iter.next();
pstmt.setString(3, groupName);
try {
pstmt.executeUpdate();
}
catch (SQLException e) {
Log.error("Unable to insert group with name '" + groupName
+ "' (rank: '" + i + "')into roster with id '"
+ rosterID + "'.", e);
}
}
}
finally {
DbConnectionManager.closeStatement(pstmt);
}
}
}
package org.jivesoftware.openfire.roster;
import java.util.Iterator;
import org.jivesoftware.openfire.user.UserAlreadyExistsException;
import org.jivesoftware.openfire.user.UserNotFoundException;
public interface RosterItemProvider {
/**
* Creates a new roster item for the given user (optional operation).<p>
*
* <b>Important!</b> The item passed as a parameter to this method is strictly a convenience
* for passing all of the data needed for a new roster item. The roster item returned from the
* method will be cached by Openfire. In some cases, the roster item passed in will be passed
* back out. However, if an implementation may return RosterItems as a separate class
* (for example, a RosterItem that directly accesses the backend storage, or one that is an
* object in an object database).<p>
*
* @param username the username of the user/chatbot that owns the roster item.
* @param item the settings for the roster item to create.
* @return the new roster item.
* @throws UserAlreadyExistsException if a roster item with the username already exists.
*/
public abstract RosterItem createItem(String username, RosterItem item)
throws UserAlreadyExistsException;
/**
* Update the roster item in storage with the information contained in the given item
* (optional operation).<p>
*
* If you don't want roster items edited through openfire, throw UnsupportedOperationException.
*
* @param username the username of the user/chatbot that owns the roster item
* @param item The roster item to update
* @throws UserNotFoundException If no entry could be found to update
*/
public abstract void updateItem(String username, RosterItem item)
throws UserNotFoundException;
/**
* Delete the roster item with the given itemJID for the user (optional operation).<p>
*
* If you don't want roster items deleted through openfire, throw
* UnsupportedOperationException.
*
* @param username the long ID of the user/chatbot that owns the roster item
* @param rosterItemID The roster item to delete
*/
public abstract void deleteItem(String username, long rosterItemID);
/**
* Returns an iterator on the usernames whose roster includes the specified JID.
*
* @param jid the jid that the rosters should include.
* @return an iterator on the usernames whose roster includes the specified JID.
*/
public abstract Iterator<String> getUsernames(String jid);
/**
* Obtain a count of the number of roster items available for the given user.
*
* @param username the username of the user/chatbot that owns the roster items
* @return The number of roster items available for the user
*/
public abstract int getItemCount(String username);
/**
* Retrieve an iterator of RosterItems for the given user.<p>
*
* This method will commonly be called when a user logs in. The data will be cached
* in memory when possible. However, some rosters may be very large so items may need
* to be retrieved from the provider more frequently than usual for provider data.
*
* @param username the username of the user/chatbot that owns the roster items
* @return An iterator of all RosterItems owned by the user
*/
public abstract Iterator<RosterItem> getItems(String username);
}
\ No newline at end of file
/**
* $RCSfile: RosterManager.java,v $
* $Revision: 3138 $
* $Date: 2005-12-01 02:13:26 -0300 (Thu, 01 Dec 2005) $
*
* Copyright (C) 2004-2008 Jive Software. All rights reserved.
*
* Licensed 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.jivesoftware.openfire.roster;
import org.jivesoftware.openfire.RoutingTable;
import org.jivesoftware.openfire.SharedGroupException;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.event.GroupEventDispatcher;
import org.jivesoftware.openfire.event.GroupEventListener;
import org.jivesoftware.openfire.event.UserEventDispatcher;
import org.jivesoftware.openfire.event.UserEventListener;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.group.GroupManager;
import org.jivesoftware.openfire.group.GroupNotFoundException;
import org.jivesoftware.openfire.user.User;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.cache.Cache;
import org.jivesoftware.util.cache.CacheFactory;
import org.xmpp.packet.JID;
import org.xmpp.packet.Presence;
import java.util.*;
/**
* A simple service that allows components to retrieve a roster based solely on the ID
* of the owner. Users have convenience methods for obtaining a roster associated with
* the owner. However there are many components that need to retrieve the roster
* based solely on the generic ID owner key. This interface defines a service that can
* do that. This allows classes that generically manage resource for resource owners
* (such as presence updates) to generically offer their services without knowing or
* caring if the roster owner is a user, chatbot, etc.
*
* @author Iain Shigeoka
*/
public class RosterManager extends BasicModule implements GroupEventListener, UserEventListener {
private Cache<String, Roster> rosterCache = null;
private XMPPServer server;
private RoutingTable routingTable;
/**
* Returns true if the roster service is enabled. When disabled it is not possible to
* retrieve users rosters or broadcast presence packets to roster contacts.
*
* @return true if the roster service is enabled.
*/
public static boolean isRosterServiceEnabled() {
return JiveGlobals.getBooleanProperty("xmpp.client.roster.active", true);
}
public RosterManager() {
super("Roster Manager");
rosterCache = CacheFactory.createCache("Roster");
}
/**
* Returns the roster for the given username.
*
* @param username the username to search for.
* @return the roster associated with the ID.
* @throws org.jivesoftware.openfire.user.UserNotFoundException if the ID does not correspond
* to a known entity on the server.
*/
public Roster getRoster(String username) throws UserNotFoundException {
Roster roster = rosterCache.get(username);
if (roster == null) {
// Synchronize using a unique key so that other threads loading the User
// and not the Roster cannot produce a deadlock
synchronized ((username + " ro").intern()) {
roster = rosterCache.get(username);
if (roster == null) {
// Not in cache so load a new one:
roster = new Roster(username);
rosterCache.put(username, roster);
}
}
}
return roster;
}
/**
* Removes the entire roster of a given user. This is necessary when a user
* account is being deleted from the server.
*
* @param user the user.
*/
public void deleteRoster(JID user) {
if (!server.isLocal(user)) {
// Ignore request if user is not a local user
return;
}
try {
String username = user.getNode();
// Get the roster of the deleted user
Roster roster = getRoster(username);
// Remove each roster item from the user's roster
for (RosterItem item : roster.getRosterItems()) {
try {
roster.deleteRosterItem(item.getJid(), false);
}
catch (SharedGroupException e) {
// Do nothing. We shouldn't have this exception since we disabled the checkings
}
}
// Remove the cached roster from memory
rosterCache.remove(username);
// Get the rosters that have a reference to the deleted user
RosterItemProvider rosteItemProvider = RosterItemProvider.getInstance();
Iterator<String> usernames = rosteItemProvider.getUsernames(user.toBareJID());
while (usernames.hasNext()) {
username = usernames.next();
try {
// Get the roster that has a reference to the deleted user
roster = getRoster(username);
// Remove the deleted user reference from this roster
roster.deleteRosterItem(user, false);
}
catch (SharedGroupException e) {
// Do nothing. We shouldn't have this exception since we disabled the checkings
}
catch (UserNotFoundException e) {
// Do nothing.
}
}
}
catch (UnsupportedOperationException e) {
// Do nothing
}
catch (UserNotFoundException e) {
// Do nothing.
}
}
/**
* Returns a collection with all the groups that the user may include in his roster. The
* following criteria will be used to select the groups: 1) Groups that are configured so that
* everybody can include in his roster, 2) Groups that are configured so that its users may
* include the group in their rosters and the user is a group user of the group and 3) User
* belongs to a Group that may see a Group that whose members may include the Group in their
* rosters.
*
* @param username the username of the user to return his shared groups.
* @return a collection with all the groups that the user may include in his roster.
*/
public Collection<Group> getSharedGroups(String username) {
Collection<Group> answer = new HashSet<Group>();
Collection<Group> groups = GroupManager.getInstance().getSharedGroups();
for (Group group : groups) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("onlyGroup".equals(showInRoster)) {
if (group.isUser(username)) {
// The user belongs to the group so add the group to the answer
answer.add(group);
}
else {
// Check if the user belongs to a group that may see this group
Collection<Group> groupList = parseGroups(group.getProperties().get("sharedRoster.groupList"));
for (Group groupInList : groupList) {
if (groupInList.isUser(username)) {
answer.add(group);
}
}
}
}
else if ("everybody".equals(showInRoster)) {
// Anyone can see this group so add the group to the answer
answer.add(group);
}
}
return answer;
}
/**
* Returns the list of shared groups whose visibility is public.
*
* @return the list of shared groups whose visibility is public.
*/
public Collection<Group> getPublicSharedGroups() {
Collection<Group> answer = new HashSet<Group>();
Collection<Group> groups = GroupManager.getInstance().getSharedGroups();
for (Group group : groups) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("everybody".equals(showInRoster)) {
// Anyone can see this group so add the group to the answer
answer.add(group);
}
}
return answer;
}
/**
* Returns a collection of Groups obtained by parsing a comma delimited String with the name
* of groups.
*
* @param groupNames a comma delimited string with group names.
* @return a collection of Groups obtained by parsing a comma delimited String with the name
* of groups.
*/
private Collection<Group> parseGroups(String groupNames) {
Collection<Group> answer = new HashSet<Group>();
for (String groupName : parseGroupNames(groupNames)) {
try {
answer.add(GroupManager.getInstance().getGroup(groupName));
}
catch (GroupNotFoundException e) {
// Do nothing. Silently ignore the invalid reference to the group
}
}
return answer;
}
/**
* Returns a collection of Groups obtained by parsing a comma delimited String with the name
* of groups.
*
* @param groupNames a comma delimited string with group names.
* @return a collection of Groups obtained by parsing a comma delimited String with the name
* of groups.
*/
private static Collection<String> parseGroupNames(String groupNames) {
Collection<String> answer = new HashSet<String>();
if (groupNames != null) {
StringTokenizer tokenizer = new StringTokenizer(groupNames, ",");
while (tokenizer.hasMoreTokens()) {
answer.add(tokenizer.nextToken());
}
}
return answer;
}
public void groupCreated(Group group, Map params) {
//Do nothing
}
public void groupDeleting(Group group, Map params) {
// Get group members
Collection<JID> users = new HashSet<JID>(group.getMembers());
users.addAll(group.getAdmins());
// Get users whose roster will be updated
Collection<JID> affectedUsers = getAffectedUsers(group);
// Iterate on group members and update rosters of affected users
for (JID deletedUser : users) {
groupUserDeleted(group, affectedUsers, deletedUser);
}
}
public void groupModified(Group group, Map params) {
// Do nothing if no group property has been modified
if ("propertyDeleted".equals(params.get("type"))) {
return;
}
String keyChanged = (String) params.get("propertyKey");
String originalValue = (String) params.get("originalValue");
if ("sharedRoster.showInRoster".equals(keyChanged)) {
String currentValue = group.getProperties().get("sharedRoster.showInRoster");
// Nothing has changed so do nothing.
if (currentValue.equals(originalValue)) {
return;
}
// Get the users of the group
Collection<JID> users = new HashSet<JID>(group.getMembers());
users.addAll(group.getAdmins());
// Get the users whose roster will be affected
Collection<JID> affectedUsers = getAffectedUsers(group, originalValue,
group.getProperties().get("sharedRoster.groupList"));
// Remove the group members from the affected rosters
for (JID deletedUser : users) {
groupUserDeleted(group, affectedUsers, deletedUser);
}
// Simulate that the group users has been added to the group. This will cause to push
// roster items to the "affected" users for the group users
for (JID user : users) {
groupUserAdded(group, user);
}
}
else if ("sharedRoster.groupList".equals(keyChanged)) {
String currentValue = group.getProperties().get("sharedRoster.groupList");
// Nothing has changed so do nothing.
if (currentValue.equals(originalValue)) {
return;
}
// Get the users of the group
Collection<JID> users = new HashSet<JID>(group.getMembers());
users.addAll(group.getAdmins());
// Get the users whose roster will be affected
Collection<JID> affectedUsers = getAffectedUsers(group,
group.getProperties().get("sharedRoster.showInRoster"), originalValue);
// Remove the group members from the affected rosters
for (JID deletedUser : users) {
groupUserDeleted(group, affectedUsers, deletedUser);
}
// Simulate that the group users has been added to the group. This will cause to push
// roster items to the "affected" users for the group users
for (JID user : users) {
groupUserAdded(group, user);
}
}
else if ("sharedRoster.displayName".equals(keyChanged)) {
String currentValue = group.getProperties().get("sharedRoster.displayName");
// Nothing has changed so do nothing.
if (currentValue.equals(originalValue)) {
return;
}
// Do nothing if the group is not being shown in users' rosters
if (!isSharedGroup(group)) {
return;
}
// Get all the affected users
Collection<JID> users = getAffectedUsers(group);
// Iterate on all the affected users and update their rosters
for (JID updatedUser : users) {
// Get the roster to update.
Roster roster = null;
if (server.isLocal(updatedUser)) {
roster = rosterCache.get(updatedUser.getNode());
}
if (roster != null) {
// Update the roster with the new group display name
roster.shareGroupRenamed(users);
}
}
}
}
@Override
public void initialize(XMPPServer server) {
super.initialize(server);
this.server = server;
this.routingTable = server.getRoutingTable();
RosterEventDispatcher.addListener(new RosterEventListener() {
public void rosterLoaded(Roster roster) {
// Do nothing
}
public boolean addingContact(Roster roster, RosterItem item, boolean persistent) {
// Do nothing
return true;
}
public void contactAdded(Roster roster, RosterItem item) {
// Set object again in cache. This is done so that other cluster nodes
// get refreshed with latest version of the object
rosterCache.put(roster.getUsername(), roster);
}
public void contactUpdated(Roster roster, RosterItem item) {
// Set object again in cache. This is done so that other cluster nodes
// get refreshed with latest version of the object
rosterCache.put(roster.getUsername(), roster);
}
public void contactDeleted(Roster roster, RosterItem item) {
// Set object again in cache. This is done so that other cluster nodes
// get refreshed with latest version of the object
rosterCache.put(roster.getUsername(), roster);
}
});
}
/**
* Returns true if the specified Group may be included in a user roster. The decision is made
* based on the group properties that are configurable through the Admin Console.
*
* @param group the group to check if it may be considered a shared group.
* @return true if the specified Group may be included in a user roster.
*/
public static boolean isSharedGroup(Group group) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("onlyGroup".equals(showInRoster) || "everybody".equals(showInRoster)) {
return true;
}
return false;
}
/**
* Returns true if the specified Group may be seen by all users in the system. The decision
* is made based on the group properties that are configurable through the Admin Console.
*
* @param group the group to check if it may be seen by all users in the system.
* @return true if the specified Group may be seen by all users in the system.
*/
public static boolean isPublicSharedGroup(Group group) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("everybody".equals(showInRoster)) {
return true;
}
return false;
}
public void memberAdded(Group group, Map params) {
JID addedUser = new JID((String) params.get("member"));
// Do nothing if the user was an admin that became a member
if (group.getAdmins().contains(addedUser)) {
return;
}
if (!isSharedGroup(group)) {
for (Group visibleGroup : getVisibleGroups(group)) {
// Get the list of affected users
Collection<JID> users = new HashSet<JID>(visibleGroup.getMembers());
users.addAll(visibleGroup.getAdmins());
groupUserAdded(visibleGroup, users, addedUser);
}
}
else {
groupUserAdded(group, addedUser);
}
}
public void memberRemoved(Group group, Map params) {
String member = (String) params.get("member");
if (member == null) {
return;
}
JID deletedUser = new JID(member);
// Do nothing if the user is still an admin
if (group.getAdmins().contains(deletedUser)) {
return;
}
if (!isSharedGroup(group)) {
for (Group visibleGroup : getVisibleGroups(group)) {
// Get the list of affected users
Collection<JID> users = new HashSet<JID>(visibleGroup.getMembers());
users.addAll(visibleGroup.getAdmins());
groupUserDeleted(visibleGroup, users, deletedUser);
}
}
else {
groupUserDeleted(group, deletedUser);
}
}
public void adminAdded(Group group, Map params) {
JID addedUser = new JID((String) params.get("admin"));
// Do nothing if the user was a member that became an admin
if (group.getMembers().contains(addedUser)) {
return;
}
if (!isSharedGroup(group)) {
for (Group visibleGroup : getVisibleGroups(group)) {
// Get the list of affected users
Collection<JID> users = new HashSet<JID>(visibleGroup.getMembers());
users.addAll(visibleGroup.getAdmins());
groupUserAdded(visibleGroup, users, addedUser);
}
}
else {
groupUserAdded(group, addedUser);
}
}
public void adminRemoved(Group group, Map params) {
JID deletedUser = new JID((String) params.get("admin"));
// Do nothing if the user is still a member
if (group.getMembers().contains(deletedUser)) {
return;
}
// Do nothing if the group is not being shown in group members' rosters
if (!isSharedGroup(group)) {
for (Group visibleGroup : getVisibleGroups(group)) {
// Get the list of affected users
Collection<JID> users = new HashSet<JID>(visibleGroup.getMembers());
users.addAll(visibleGroup.getAdmins());
groupUserDeleted(visibleGroup, users, deletedUser);
}
}
else {
groupUserDeleted(group, deletedUser);
}
}
/**
* A new user has been created so members of public shared groups need to have
* their rosters updated. Members of public shared groups need to have a roster
* item with subscription FROM for the new user since the new user can see them.
*
* @param newUser the newly created user.
* @param params event parameters.
*/
public void userCreated(User newUser, Map<String,Object> params) {
JID newUserJID = server.createJID(newUser.getUsername(), null);
// Shared public groups that are public should have a presence subscription
// of type FROM for the new user
for (Group group : getPublicSharedGroups()) {
// Get group members of public group
Collection<JID> users = new HashSet<JID>(group.getMembers());
users.addAll(group.getAdmins());
// Update the roster of each group member to include a subscription of type FROM
for (JID userToUpdate : users) {
// Get the roster to update
Roster roster = null;
if (server.isLocal(userToUpdate)) {
// Check that the user exists, if not then continue with the next user
try {
UserManager.getInstance().getUser(userToUpdate.getNode());
}
catch (UserNotFoundException e) {
continue;
}
roster = rosterCache.get(userToUpdate.getNode());
}
// Only update rosters in memory
if (roster != null) {
roster.addSharedUser(group, newUserJID);
}
if (!server.isLocal(userToUpdate)) {
// Susbcribe to the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(newUserJID, userToUpdate, true);
}
}
}
}
public void userDeleting(User user, Map<String,Object> params) {
// Shared public groups that have a presence subscription of type FROM
// for the deleted user should no longer have a reference to the deleted user
JID userJID = server.createJID(user.getUsername(), null);
// Shared public groups that are public should have a presence subscription
// of type FROM for the new user
for (Group group : getPublicSharedGroups()) {
// Get group members of public group
Collection<JID> users = new HashSet<JID>(group.getMembers());
users.addAll(group.getAdmins());
// Update the roster of each group member to include a subscription of type FROM
for (JID userToUpdate : users) {
// Get the roster to update
Roster roster = null;
if (server.isLocal(userToUpdate)) {
// Check that the user exists, if not then continue with the next user
try {
UserManager.getInstance().getUser(userToUpdate.getNode());
}
catch (UserNotFoundException e) {
continue;
}
roster = rosterCache.get(userToUpdate.getNode());
}
// Only update rosters in memory
if (roster != null) {
roster.deleteSharedUser(group, userJID);
}
if (!server.isLocal(userToUpdate)) {
// Unsusbcribe from the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(userJID, userToUpdate, false);
}
}
}
deleteRoster(userJID);
}
public void userModified(User user, Map<String,Object> params) {
//Do nothing
}
/**
* Notification that a Group user has been added. Update the group users' roster accordingly.
*
* @param group the group where the user was added.
* @param addedUser the username of the user that has been added to the group.
*/
private void groupUserAdded(Group group, JID addedUser) {
groupUserAdded(group, getAffectedUsers(group), addedUser);
}
/**
* Notification that a Group user has been added. Update the group users' roster accordingly.
*
* @param group the group where the user was added.
* @param users the users to update their rosters
* @param addedUser the username of the user that has been added to the group.
*/
private void groupUserAdded(Group group, Collection<JID> users, JID addedUser) {
// Get the roster of the added user.
Roster addedUserRoster = null;
if (server.isLocal(addedUser)) {
addedUserRoster = rosterCache.get(addedUser.getNode());
}
// Iterate on all the affected users and update their rosters
for (JID userToUpdate : users) {
if (!addedUser.equals(userToUpdate)) {
// Get the roster to update
Roster roster = null;
if (server.isLocal(userToUpdate)) {
// Check that the user exists, if not then continue with the next user
try {
UserManager.getInstance().getUser(userToUpdate.getNode());
}
catch (UserNotFoundException e) {
continue;
}
roster = rosterCache.get(userToUpdate.getNode());
}
// Only update rosters in memory
if (roster != null) {
roster.addSharedUser(group, addedUser);
}
// Check if the roster is still not in memory
if (addedUserRoster == null && server.isLocal(addedUser)) {
addedUserRoster =
rosterCache.get(addedUser.getNode());
}
// Update the roster of the newly added group user.
if (addedUserRoster != null) {
Collection<Group> groups = GroupManager.getInstance().getGroups(userToUpdate);
addedUserRoster.addSharedUser(userToUpdate, groups, group);
}
if (!server.isLocal(addedUser)) {
// Susbcribe to the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(userToUpdate, addedUser, true);
}
if (!server.isLocal(userToUpdate)) {
// Susbcribe to the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(addedUser, userToUpdate, true);
}
}
}
}
/**
* Notification that a Group user has been deleted. Update the group users' roster accordingly.
*
* @param group the group from where the user was deleted.
* @param deletedUser the username of the user that has been deleted from the group.
*/
private void groupUserDeleted(Group group, JID deletedUser) {
groupUserDeleted(group, getAffectedUsers(group), deletedUser);
}
/**
* Notification that a Group user has been deleted. Update the group users' roster accordingly.
*
* @param group the group from where the user was deleted.
* @param users the users to update their rosters
* @param deletedUser the username of the user that has been deleted from the group.
*/
private void groupUserDeleted(Group group, Collection<JID> users, JID deletedUser) {
// Get the roster of the deleted user.
Roster deletedUserRoster = null;
if (server.isLocal(deletedUser)) {
deletedUserRoster = rosterCache.get(deletedUser.getNode());
}
// Iterate on all the affected users and update their rosters
for (JID userToUpdate : users) {
// Get the roster to update
Roster roster = null;
if (server.isLocal(userToUpdate)) {
// Check that the user exists, if not then continue with the next user
try {
UserManager.getInstance().getUser(userToUpdate.getNode());
}
catch (UserNotFoundException e) {
continue;
}
roster = rosterCache.get(userToUpdate.getNode());
}
// Only update rosters in memory
if (roster != null) {
roster.deleteSharedUser(group, deletedUser);
}
// Check if the roster is still not in memory
if (deletedUserRoster == null && server.isLocal(deletedUser)) {
deletedUserRoster =
rosterCache.get(deletedUser.getNode());
}
// Update the roster of the newly deleted group user.
if (deletedUserRoster != null) {
deletedUserRoster.deleteSharedUser(userToUpdate, group);
}
if (!server.isLocal(deletedUser)) {
// Unsusbcribe from the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(userToUpdate, deletedUser, false);
}
if (!server.isLocal(userToUpdate)) {
// Unsusbcribe from the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(deletedUser, userToUpdate, false);
}
}
}
private void sendSubscribeRequest(JID sender, JID recipient, boolean isSubscribe) {
Presence presence = new Presence();
presence.setFrom(sender);
presence.setTo(recipient);
if (isSubscribe) {
presence.setType(Presence.Type.subscribe);
}
else {
presence.setType(Presence.Type.unsubscribe);
}
routingTable.routePacket(recipient, presence, false);
}
private Collection<Group> getVisibleGroups(Group groupToCheck) {
Collection<Group> answer = new HashSet<Group>();
Collection<Group> groups = GroupManager.getInstance().getSharedGroups();
for (Group group : groups) {
if (group.equals(groupToCheck)) {
continue;
}
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("onlyGroup".equals(showInRoster)) {
// Check if the user belongs to a group that may see this group
Collection<String> groupList =
parseGroupNames(group.getProperties().get("sharedRoster.groupList"));
if (groupList.contains(groupToCheck.getName())) {
answer.add(group);
}
}
else if ("everybody".equals(showInRoster)) {
answer.add(group);
}
}
return answer;
}
/**
* Returns true if a given group is visible to a given user. That means, if the user can
* see the group in his roster.
*
* @param group the group to check if the user can see.
* @param user the JID of the user to check if he may see the group.
* @return true if a given group is visible to a given user.
*/
boolean isGroupVisible(Group group, JID user) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("everybody".equals(showInRoster)) {
return true;
}
else if ("onlyGroup".equals(showInRoster)) {
if (group.isUser(user)) {
return true;
}
// Check if the user belongs to a group that may see this group
Collection<Group> groupList = parseGroups(group.getProperties().get(
"sharedRoster.groupList"));
for (Group groupInList : groupList) {
if (groupInList.isUser(user)) {
return true;
}
}
}
return false;
}
/**
* Returns all the users that are related to a shared group. This is the logic that we are
* using: 1) If the group visiblity is configured as "Everybody" then all users in the system or
* all logged users in the system will be returned (configurable thorugh the "filterOffline"
* flag), 2) if the group visiblity is configured as "onlyGroup" then all the group users will
* be included in the answer and 3) if the group visiblity is configured as "onlyGroup" and
* the group allows other groups to include the group in the groups users' roster then all
* the users of the allowed groups will be included in the answer.
*/
private Collection<JID> getAffectedUsers(Group group) {
return getAffectedUsers(group, group.getProperties().get("sharedRoster.showInRoster"),
group.getProperties().get("sharedRoster.groupList"));
}
/**
* This method is similar to {@link #getAffectedUsers(Group)} except that it receives
* some group properties. The group properties are passed as parameters since the called of this
* method may want to obtain the related users of the group based in some properties values.
*
* This is useful when the group is being edited and some properties has changed and we need to
* obtain the related users of the group based on the previous group state.
*/
private Collection<JID> getAffectedUsers(Group group, String showInRoster, String groupNames) {
// Answer an empty collection if the group is not being shown in users' rosters
if (!"onlyGroup".equals(showInRoster) && !"everybody".equals(showInRoster)) {
return new ArrayList<JID>();
}
// Add the users of the group
Collection<JID> users = new HashSet<JID>(group.getMembers());
users.addAll(group.getAdmins());
// Check if anyone can see this shared group
if ("everybody".equals(showInRoster)) {
// Add all users in the system
for (String username : UserManager.getInstance().getUsernames()) {
users.add(server.createJID(username, null, true));
}
// Add all logged users. We don't need to add all users in the system since only the
// logged ones will be affected.
//users.addAll(SessionManager.getInstance().getSessionUsers());
}
else {
// Add the users that may see the group
Collection<Group> groupList = parseGroups(groupNames);
for (Group groupInList : groupList) {
users.addAll(groupInList.getMembers());
users.addAll(groupInList.getAdmins());
}
}
return users;
}
Collection<JID> getSharedUsersForRoster(Group group, Roster roster) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
String groupNames = group.getProperties().get("sharedRoster.groupList");
// Answer an empty collection if the group is not being shown in users' rosters
if (!"onlyGroup".equals(showInRoster) && !"everybody".equals(showInRoster)) {
return new ArrayList<JID>();
}
// Add the users of the group
Collection<JID> users = new HashSet<JID>(group.getMembers());
users.addAll(group.getAdmins());
// If the user of the roster belongs to the shared group then we should return
// users that need to be in the roster with subscription "from"
if (group.isUser(roster.getUsername())) {
// Check if anyone can see this shared group
if ("everybody".equals(showInRoster)) {
// Add all users in the system
for (String username : UserManager.getInstance().getUsernames()) {
users.add(server.createJID(username, null, true));
}
}
else {
// Add the users that may see the group
Collection<Group> groupList = parseGroups(groupNames);
for (Group groupInList : groupList) {
users.addAll(groupInList.getMembers());
users.addAll(groupInList.getAdmins());
}
}
}
return users;
}
/**
* Returns true if a group in the first collection may mutually see a group of the
* second collection. More precisely, return true if both collections contain a public
* group (i.e. anybody can see the group) or if both collection have a group that may see
* each other and the users are members of those groups or if one group is public and the
* other group allowed the public group to see it.
*
* @param user the name of the user associated to the first collection of groups. This is always a local user.
* @param groups a collection of groups to check against the other collection of groups.
* @param otherUser the JID of the user associated to the second collection of groups.
* @param otherGroups the other collection of groups to check against the first collection.
* @return true if a group in the first collection may mutually see a group of the
* second collection.
*/
boolean hasMutualVisibility(String user, Collection<Group> groups, JID otherUser,
Collection<Group> otherGroups) {
for (Group group : groups) {
for (Group otherGroup : otherGroups) {
// Skip this groups if the users are not group users of the groups
if (!group.isUser(user) || !otherGroup.isUser(otherUser)) {
continue;
}
if (group.equals(otherGroup)) {
return true;
}
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
String otherShowInRoster = otherGroup.getProperties().get("sharedRoster.showInRoster");
// Return true if both groups are public groups (i.e. anybody can see them)
if ("everybody".equals(showInRoster) && "everybody".equals(otherShowInRoster)) {
return true;
}
else if ("onlyGroup".equals(showInRoster) && "onlyGroup".equals(otherShowInRoster)) {
String groupNames = group.getProperties().get("sharedRoster.groupList");
String otherGroupNames = otherGroup.getProperties().get("sharedRoster.groupList");
// Return true if each group may see the other group
if (groupNames != null && otherGroupNames != null) {
if (groupNames.contains(otherGroup.getName()) &&
otherGroupNames.contains(group.getName())) {
return true;
}
// Check if each shared group can be seen by a group where each user belongs
Collection<Group> groupList = parseGroups(groupNames);
Collection<Group> otherGroupList = parseGroups(otherGroupNames);
for (Group groupName : groupList) {
if (groupName.isUser(otherUser)) {
for (Group otherGroupName : otherGroupList) {
if (otherGroupName.isUser(user)) {
return true;
}
}
}
}
}
}
else if ("everybody".equals(showInRoster) && "onlyGroup".equals(otherShowInRoster)) {
// Return true if one group is public and the other group allowed the public
// group to see him
String otherGroupNames = otherGroup.getProperties().get("sharedRoster.groupList");
if (otherGroupNames != null && otherGroupNames.contains(group.getName())) {
return true;
}
}
else if ("onlyGroup".equals(showInRoster) && "everybody".equals(otherShowInRoster)) {
// Return true if one group is public and the other group allowed the public
// group to see him
String groupNames = group.getProperties().get("sharedRoster.groupList");
// Return true if each group may see the other group
if (groupNames != null && groupNames.contains(otherGroup.getName())) {
return true;
}
}
}
}
return false;
}
@Override
public void start() throws IllegalStateException {
super.start();
// Add this module as a user event listener so we can update
// rosters when users are created or deleted
UserEventDispatcher.addListener(this);
// Add the new instance as a listener of group events
GroupEventDispatcher.addListener(this);
}
@Override
public void stop() {
super.stop();
// Remove this module as a user event listener
UserEventDispatcher.removeListener(this);
// Remove this module as a listener of group events
GroupEventDispatcher.removeListener(this);
}
}
/**
* $RCSfile: RosterManager.java,v $
* $Revision: 3138 $
* $Date: 2005-12-01 02:13:26 -0300 (Thu, 01 Dec 2005) $
*
* Copyright (C) 2004-2008 Jive Software. All rights reserved.
*
* Licensed 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.jivesoftware.openfire.roster;
import org.jivesoftware.openfire.RoutingTable;
import org.jivesoftware.openfire.SharedGroupException;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.event.GroupEventDispatcher;
import org.jivesoftware.openfire.event.GroupEventListener;
import org.jivesoftware.openfire.event.UserEventDispatcher;
import org.jivesoftware.openfire.event.UserEventListener;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.group.GroupManager;
import org.jivesoftware.openfire.group.GroupNotFoundException;
import org.jivesoftware.openfire.user.User;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.ClassUtils;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
import org.jivesoftware.util.cache.Cache;
import org.jivesoftware.util.cache.CacheFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
import org.xmpp.packet.Presence;
import java.util.*;
/**
* A simple service that allows components to retrieve a roster based solely on the ID
* of the owner. Users have convenience methods for obtaining a roster associated with
* the owner. However there are many components that need to retrieve the roster
* based solely on the generic ID owner key. This interface defines a service that can
* do that. This allows classes that generically manage resource for resource owners
* (such as presence updates) to generically offer their services without knowing or
* caring if the roster owner is a user, chatbot, etc.
*
* @author Iain Shigeoka
*/
public class RosterManager extends BasicModule implements GroupEventListener, UserEventListener {
private static final Logger Log = LoggerFactory.getLogger(RosterManager.class);
private Cache<String, Roster> rosterCache = null;
private XMPPServer server;
private RoutingTable routingTable;
private RosterItemProvider provider;
/**
* Returns true if the roster service is enabled. When disabled it is not possible to
* retrieve users rosters or broadcast presence packets to roster contacts.
*
* @return true if the roster service is enabled.
*/
public static boolean isRosterServiceEnabled() {
return JiveGlobals.getBooleanProperty("xmpp.client.roster.active", true);
}
public RosterManager() {
super("Roster Manager");
rosterCache = CacheFactory.createCache("Roster");
initProvider();
PropertyEventDispatcher.addListener(new PropertyEventListener() {
public void propertySet(String property, Map params) {
if (property.equals("provider.roster.className")) {
initProvider();
}
}
public void propertyDeleted(String property, Map params) {}
public void xmlPropertySet(String property, Map params) {}
public void xmlPropertyDeleted(String property, Map params) {}
});
}
/**
* Returns the roster for the given username.
*
* @param username the username to search for.
* @return the roster associated with the ID.
* @throws org.jivesoftware.openfire.user.UserNotFoundException if the ID does not correspond
* to a known entity on the server.
*/
public Roster getRoster(String username) throws UserNotFoundException {
Roster roster = rosterCache.get(username);
if (roster == null) {
// Synchronize using a unique key so that other threads loading the User
// and not the Roster cannot produce a deadlock
synchronized ((username + " ro").intern()) {
roster = rosterCache.get(username);
if (roster == null) {
// Not in cache so load a new one:
roster = new Roster(username);
rosterCache.put(username, roster);
}
}
}
return roster;
}
/**
* Removes the entire roster of a given user. This is necessary when a user
* account is being deleted from the server.
*
* @param user the user.
*/
public void deleteRoster(JID user) {
if (!server.isLocal(user)) {
// Ignore request if user is not a local user
return;
}
try {
String username = user.getNode();
// Get the roster of the deleted user
Roster roster = getRoster(username);
// Remove each roster item from the user's roster
for (RosterItem item : roster.getRosterItems()) {
try {
roster.deleteRosterItem(item.getJid(), false);
}
catch (SharedGroupException e) {
// Do nothing. We shouldn't have this exception since we disabled the checkings
}
}
// Remove the cached roster from memory
rosterCache.remove(username);
// Get the rosters that have a reference to the deleted user
Iterator<String> usernames = provider.getUsernames(user.toBareJID());
while (usernames.hasNext()) {
username = usernames.next();
try {
// Get the roster that has a reference to the deleted user
roster = getRoster(username);
// Remove the deleted user reference from this roster
roster.deleteRosterItem(user, false);
}
catch (SharedGroupException e) {
// Do nothing. We shouldn't have this exception since we disabled the checkings
}
catch (UserNotFoundException e) {
// Do nothing.
}
}
}
catch (UnsupportedOperationException e) {
// Do nothing
}
catch (UserNotFoundException e) {
// Do nothing.
}
}
/**
* Returns a collection with all the groups that the user may include in his roster. The
* following criteria will be used to select the groups: 1) Groups that are configured so that
* everybody can include in his roster, 2) Groups that are configured so that its users may
* include the group in their rosters and the user is a group user of the group and 3) User
* belongs to a Group that may see a Group that whose members may include the Group in their
* rosters.
*
* @param username the username of the user to return his shared groups.
* @return a collection with all the groups that the user may include in his roster.
*/
public Collection<Group> getSharedGroups(String username) {
Collection<Group> answer = new HashSet<Group>();
Collection<Group> groups = GroupManager.getInstance().getSharedGroups();
for (Group group : groups) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("onlyGroup".equals(showInRoster)) {
if (group.isUser(username)) {
// The user belongs to the group so add the group to the answer
answer.add(group);
}
else {
// Check if the user belongs to a group that may see this group
Collection<Group> groupList = parseGroups(group.getProperties().get("sharedRoster.groupList"));
for (Group groupInList : groupList) {
if (groupInList.isUser(username)) {
answer.add(group);
}
}
}
}
else if ("everybody".equals(showInRoster)) {
// Anyone can see this group so add the group to the answer
answer.add(group);
}
}
return answer;
}
/**
* Returns the list of shared groups whose visibility is public.
*
* @return the list of shared groups whose visibility is public.
*/
public Collection<Group> getPublicSharedGroups() {
Collection<Group> answer = new HashSet<Group>();
Collection<Group> groups = GroupManager.getInstance().getSharedGroups();
for (Group group : groups) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("everybody".equals(showInRoster)) {
// Anyone can see this group so add the group to the answer
answer.add(group);
}
}
return answer;
}
/**
* Returns a collection of Groups obtained by parsing a comma delimited String with the name
* of groups.
*
* @param groupNames a comma delimited string with group names.
* @return a collection of Groups obtained by parsing a comma delimited String with the name
* of groups.
*/
private Collection<Group> parseGroups(String groupNames) {
Collection<Group> answer = new HashSet<Group>();
for (String groupName : parseGroupNames(groupNames)) {
try {
answer.add(GroupManager.getInstance().getGroup(groupName));
}
catch (GroupNotFoundException e) {
// Do nothing. Silently ignore the invalid reference to the group
}
}
return answer;
}
/**
* Returns a collection of Groups obtained by parsing a comma delimited String with the name
* of groups.
*
* @param groupNames a comma delimited string with group names.
* @return a collection of Groups obtained by parsing a comma delimited String with the name
* of groups.
*/
private static Collection<String> parseGroupNames(String groupNames) {
Collection<String> answer = new HashSet<String>();
if (groupNames != null) {
StringTokenizer tokenizer = new StringTokenizer(groupNames, ",");
while (tokenizer.hasMoreTokens()) {
answer.add(tokenizer.nextToken());
}
}
return answer;
}
public void groupCreated(Group group, Map params) {
//Do nothing
}
public void groupDeleting(Group group, Map params) {
// Get group members
Collection<JID> users = new HashSet<JID>(group.getMembers());
users.addAll(group.getAdmins());
// Get users whose roster will be updated
Collection<JID> affectedUsers = getAffectedUsers(group);
// Iterate on group members and update rosters of affected users
for (JID deletedUser : users) {
groupUserDeleted(group, affectedUsers, deletedUser);
}
}
public void groupModified(Group group, Map params) {
// Do nothing if no group property has been modified
if ("propertyDeleted".equals(params.get("type"))) {
return;
}
String keyChanged = (String) params.get("propertyKey");
String originalValue = (String) params.get("originalValue");
if ("sharedRoster.showInRoster".equals(keyChanged)) {
String currentValue = group.getProperties().get("sharedRoster.showInRoster");
// Nothing has changed so do nothing.
if (currentValue.equals(originalValue)) {
return;
}
// Get the users of the group
Collection<JID> users = new HashSet<JID>(group.getMembers());
users.addAll(group.getAdmins());
// Get the users whose roster will be affected
Collection<JID> affectedUsers = getAffectedUsers(group, originalValue,
group.getProperties().get("sharedRoster.groupList"));
// Remove the group members from the affected rosters
for (JID deletedUser : users) {
groupUserDeleted(group, affectedUsers, deletedUser);
}
// Simulate that the group users has been added to the group. This will cause to push
// roster items to the "affected" users for the group users
for (JID user : users) {
groupUserAdded(group, user);
}
}
else if ("sharedRoster.groupList".equals(keyChanged)) {
String currentValue = group.getProperties().get("sharedRoster.groupList");
// Nothing has changed so do nothing.
if (currentValue.equals(originalValue)) {
return;
}
// Get the users of the group
Collection<JID> users = new HashSet<JID>(group.getMembers());
users.addAll(group.getAdmins());
// Get the users whose roster will be affected
Collection<JID> affectedUsers = getAffectedUsers(group,
group.getProperties().get("sharedRoster.showInRoster"), originalValue);
// Remove the group members from the affected rosters
for (JID deletedUser : users) {
groupUserDeleted(group, affectedUsers, deletedUser);
}
// Simulate that the group users has been added to the group. This will cause to push
// roster items to the "affected" users for the group users
for (JID user : users) {
groupUserAdded(group, user);
}
}
else if ("sharedRoster.displayName".equals(keyChanged)) {
String currentValue = group.getProperties().get("sharedRoster.displayName");
// Nothing has changed so do nothing.
if (currentValue.equals(originalValue)) {
return;
}
// Do nothing if the group is not being shown in users' rosters
if (!isSharedGroup(group)) {
return;
}
// Get all the affected users
Collection<JID> users = getAffectedUsers(group);
// Iterate on all the affected users and update their rosters
for (JID updatedUser : users) {
// Get the roster to update.
Roster roster = null;
if (server.isLocal(updatedUser)) {
roster = rosterCache.get(updatedUser.getNode());
}
if (roster != null) {
// Update the roster with the new group display name
roster.shareGroupRenamed(users);
}
}
}
}
@Override
public void initialize(XMPPServer server) {
super.initialize(server);
this.server = server;
this.routingTable = server.getRoutingTable();
RosterEventDispatcher.addListener(new RosterEventListener() {
public void rosterLoaded(Roster roster) {
// Do nothing
}
public boolean addingContact(Roster roster, RosterItem item, boolean persistent) {
// Do nothing
return true;
}
public void contactAdded(Roster roster, RosterItem item) {
// Set object again in cache. This is done so that other cluster nodes
// get refreshed with latest version of the object
rosterCache.put(roster.getUsername(), roster);
}
public void contactUpdated(Roster roster, RosterItem item) {
// Set object again in cache. This is done so that other cluster nodes
// get refreshed with latest version of the object
rosterCache.put(roster.getUsername(), roster);
}
public void contactDeleted(Roster roster, RosterItem item) {
// Set object again in cache. This is done so that other cluster nodes
// get refreshed with latest version of the object
rosterCache.put(roster.getUsername(), roster);
}
});
}
/**
* Returns true if the specified Group may be included in a user roster. The decision is made
* based on the group properties that are configurable through the Admin Console.
*
* @param group the group to check if it may be considered a shared group.
* @return true if the specified Group may be included in a user roster.
*/
public static boolean isSharedGroup(Group group) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("onlyGroup".equals(showInRoster) || "everybody".equals(showInRoster)) {
return true;
}
return false;
}
/**
* Returns true if the specified Group may be seen by all users in the system. The decision
* is made based on the group properties that are configurable through the Admin Console.
*
* @param group the group to check if it may be seen by all users in the system.
* @return true if the specified Group may be seen by all users in the system.
*/
public static boolean isPublicSharedGroup(Group group) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("everybody".equals(showInRoster)) {
return true;
}
return false;
}
public void memberAdded(Group group, Map params) {
JID addedUser = new JID((String) params.get("member"));
// Do nothing if the user was an admin that became a member
if (group.getAdmins().contains(addedUser)) {
return;
}
if (!isSharedGroup(group)) {
for (Group visibleGroup : getVisibleGroups(group)) {
// Get the list of affected users
Collection<JID> users = new HashSet<JID>(visibleGroup.getMembers());
users.addAll(visibleGroup.getAdmins());
groupUserAdded(visibleGroup, users, addedUser);
}
}
else {
groupUserAdded(group, addedUser);
}
}
public void memberRemoved(Group group, Map params) {
String member = (String) params.get("member");
if (member == null) {
return;
}
JID deletedUser = new JID(member);
// Do nothing if the user is still an admin
if (group.getAdmins().contains(deletedUser)) {
return;
}
if (!isSharedGroup(group)) {
for (Group visibleGroup : getVisibleGroups(group)) {
// Get the list of affected users
Collection<JID> users = new HashSet<JID>(visibleGroup.getMembers());
users.addAll(visibleGroup.getAdmins());
groupUserDeleted(visibleGroup, users, deletedUser);
}
}
else {
groupUserDeleted(group, deletedUser);
}
}
public void adminAdded(Group group, Map params) {
JID addedUser = new JID((String) params.get("admin"));
// Do nothing if the user was a member that became an admin
if (group.getMembers().contains(addedUser)) {
return;
}
if (!isSharedGroup(group)) {
for (Group visibleGroup : getVisibleGroups(group)) {
// Get the list of affected users
Collection<JID> users = new HashSet<JID>(visibleGroup.getMembers());
users.addAll(visibleGroup.getAdmins());
groupUserAdded(visibleGroup, users, addedUser);
}
}
else {
groupUserAdded(group, addedUser);
}
}
public void adminRemoved(Group group, Map params) {
JID deletedUser = new JID((String) params.get("admin"));
// Do nothing if the user is still a member
if (group.getMembers().contains(deletedUser)) {
return;
}
// Do nothing if the group is not being shown in group members' rosters
if (!isSharedGroup(group)) {
for (Group visibleGroup : getVisibleGroups(group)) {
// Get the list of affected users
Collection<JID> users = new HashSet<JID>(visibleGroup.getMembers());
users.addAll(visibleGroup.getAdmins());
groupUserDeleted(visibleGroup, users, deletedUser);
}
}
else {
groupUserDeleted(group, deletedUser);
}
}
/**
* A new user has been created so members of public shared groups need to have
* their rosters updated. Members of public shared groups need to have a roster
* item with subscription FROM for the new user since the new user can see them.
*
* @param newUser the newly created user.
* @param params event parameters.
*/
public void userCreated(User newUser, Map<String,Object> params) {
JID newUserJID = server.createJID(newUser.getUsername(), null);
// Shared public groups that are public should have a presence subscription
// of type FROM for the new user
for (Group group : getPublicSharedGroups()) {
// Get group members of public group
Collection<JID> users = new HashSet<JID>(group.getMembers());
users.addAll(group.getAdmins());
// Update the roster of each group member to include a subscription of type FROM
for (JID userToUpdate : users) {
// Get the roster to update
Roster roster = null;
if (server.isLocal(userToUpdate)) {
// Check that the user exists, if not then continue with the next user
try {
UserManager.getInstance().getUser(userToUpdate.getNode());
}
catch (UserNotFoundException e) {
continue;
}
roster = rosterCache.get(userToUpdate.getNode());
}
// Only update rosters in memory
if (roster != null) {
roster.addSharedUser(group, newUserJID);
}
if (!server.isLocal(userToUpdate)) {
// Susbcribe to the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(newUserJID, userToUpdate, true);
}
}
}
}
public void userDeleting(User user, Map<String,Object> params) {
// Shared public groups that have a presence subscription of type FROM
// for the deleted user should no longer have a reference to the deleted user
JID userJID = server.createJID(user.getUsername(), null);
// Shared public groups that are public should have a presence subscription
// of type FROM for the new user
for (Group group : getPublicSharedGroups()) {
// Get group members of public group
Collection<JID> users = new HashSet<JID>(group.getMembers());
users.addAll(group.getAdmins());
// Update the roster of each group member to include a subscription of type FROM
for (JID userToUpdate : users) {
// Get the roster to update
Roster roster = null;
if (server.isLocal(userToUpdate)) {
// Check that the user exists, if not then continue with the next user
try {
UserManager.getInstance().getUser(userToUpdate.getNode());
}
catch (UserNotFoundException e) {
continue;
}
roster = rosterCache.get(userToUpdate.getNode());
}
// Only update rosters in memory
if (roster != null) {
roster.deleteSharedUser(group, userJID);
}
if (!server.isLocal(userToUpdate)) {
// Unsusbcribe from the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(userJID, userToUpdate, false);
}
}
}
deleteRoster(userJID);
}
public void userModified(User user, Map<String,Object> params) {
//Do nothing
}
/**
* Notification that a Group user has been added. Update the group users' roster accordingly.
*
* @param group the group where the user was added.
* @param addedUser the username of the user that has been added to the group.
*/
private void groupUserAdded(Group group, JID addedUser) {
groupUserAdded(group, getAffectedUsers(group), addedUser);
}
/**
* Notification that a Group user has been added. Update the group users' roster accordingly.
*
* @param group the group where the user was added.
* @param users the users to update their rosters
* @param addedUser the username of the user that has been added to the group.
*/
private void groupUserAdded(Group group, Collection<JID> users, JID addedUser) {
// Get the roster of the added user.
Roster addedUserRoster = null;
if (server.isLocal(addedUser)) {
addedUserRoster = rosterCache.get(addedUser.getNode());
}
// Iterate on all the affected users and update their rosters
for (JID userToUpdate : users) {
if (!addedUser.equals(userToUpdate)) {
// Get the roster to update
Roster roster = null;
if (server.isLocal(userToUpdate)) {
// Check that the user exists, if not then continue with the next user
try {
UserManager.getInstance().getUser(userToUpdate.getNode());
}
catch (UserNotFoundException e) {
continue;
}
roster = rosterCache.get(userToUpdate.getNode());
}
// Only update rosters in memory
if (roster != null) {
roster.addSharedUser(group, addedUser);
}
// Check if the roster is still not in memory
if (addedUserRoster == null && server.isLocal(addedUser)) {
addedUserRoster =
rosterCache.get(addedUser.getNode());
}
// Update the roster of the newly added group user.
if (addedUserRoster != null) {
Collection<Group> groups = GroupManager.getInstance().getGroups(userToUpdate);
addedUserRoster.addSharedUser(userToUpdate, groups, group);
}
if (!server.isLocal(addedUser)) {
// Susbcribe to the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(userToUpdate, addedUser, true);
}
if (!server.isLocal(userToUpdate)) {
// Susbcribe to the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(addedUser, userToUpdate, true);
}
}
}
}
/**
* Notification that a Group user has been deleted. Update the group users' roster accordingly.
*
* @param group the group from where the user was deleted.
* @param deletedUser the username of the user that has been deleted from the group.
*/
private void groupUserDeleted(Group group, JID deletedUser) {
groupUserDeleted(group, getAffectedUsers(group), deletedUser);
}
/**
* Notification that a Group user has been deleted. Update the group users' roster accordingly.
*
* @param group the group from where the user was deleted.
* @param users the users to update their rosters
* @param deletedUser the username of the user that has been deleted from the group.
*/
private void groupUserDeleted(Group group, Collection<JID> users, JID deletedUser) {
// Get the roster of the deleted user.
Roster deletedUserRoster = null;
if (server.isLocal(deletedUser)) {
deletedUserRoster = rosterCache.get(deletedUser.getNode());
}
// Iterate on all the affected users and update their rosters
for (JID userToUpdate : users) {
// Get the roster to update
Roster roster = null;
if (server.isLocal(userToUpdate)) {
// Check that the user exists, if not then continue with the next user
try {
UserManager.getInstance().getUser(userToUpdate.getNode());
}
catch (UserNotFoundException e) {
continue;
}
roster = rosterCache.get(userToUpdate.getNode());
}
// Only update rosters in memory
if (roster != null) {
roster.deleteSharedUser(group, deletedUser);
}
// Check if the roster is still not in memory
if (deletedUserRoster == null && server.isLocal(deletedUser)) {
deletedUserRoster =
rosterCache.get(deletedUser.getNode());
}
// Update the roster of the newly deleted group user.
if (deletedUserRoster != null) {
deletedUserRoster.deleteSharedUser(userToUpdate, group);
}
if (!server.isLocal(deletedUser)) {
// Unsusbcribe from the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(userToUpdate, deletedUser, false);
}
if (!server.isLocal(userToUpdate)) {
// Unsusbcribe from the presence of the remote user. This is only necessary for
// remote users and may only work with remote users that **automatically**
// accept presence subscription requests
sendSubscribeRequest(deletedUser, userToUpdate, false);
}
}
}
private void sendSubscribeRequest(JID sender, JID recipient, boolean isSubscribe) {
Presence presence = new Presence();
presence.setFrom(sender);
presence.setTo(recipient);
if (isSubscribe) {
presence.setType(Presence.Type.subscribe);
}
else {
presence.setType(Presence.Type.unsubscribe);
}
routingTable.routePacket(recipient, presence, false);
}
private Collection<Group> getVisibleGroups(Group groupToCheck) {
Collection<Group> answer = new HashSet<Group>();
Collection<Group> groups = GroupManager.getInstance().getSharedGroups();
for (Group group : groups) {
if (group.equals(groupToCheck)) {
continue;
}
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("onlyGroup".equals(showInRoster)) {
// Check if the user belongs to a group that may see this group
Collection<String> groupList =
parseGroupNames(group.getProperties().get("sharedRoster.groupList"));
if (groupList.contains(groupToCheck.getName())) {
answer.add(group);
}
}
else if ("everybody".equals(showInRoster)) {
answer.add(group);
}
}
return answer;
}
/**
* Returns true if a given group is visible to a given user. That means, if the user can
* see the group in his roster.
*
* @param group the group to check if the user can see.
* @param user the JID of the user to check if he may see the group.
* @return true if a given group is visible to a given user.
*/
boolean isGroupVisible(Group group, JID user) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("everybody".equals(showInRoster)) {
return true;
}
else if ("onlyGroup".equals(showInRoster)) {
if (group.isUser(user)) {
return true;
}
// Check if the user belongs to a group that may see this group
Collection<Group> groupList = parseGroups(group.getProperties().get(
"sharedRoster.groupList"));
for (Group groupInList : groupList) {
if (groupInList.isUser(user)) {
return true;
}
}
}
return false;
}
/**
* Returns all the users that are related to a shared group. This is the logic that we are
* using: 1) If the group visiblity is configured as "Everybody" then all users in the system or
* all logged users in the system will be returned (configurable thorugh the "filterOffline"
* flag), 2) if the group visiblity is configured as "onlyGroup" then all the group users will
* be included in the answer and 3) if the group visiblity is configured as "onlyGroup" and
* the group allows other groups to include the group in the groups users' roster then all
* the users of the allowed groups will be included in the answer.
*/
private Collection<JID> getAffectedUsers(Group group) {
return getAffectedUsers(group, group.getProperties().get("sharedRoster.showInRoster"),
group.getProperties().get("sharedRoster.groupList"));
}
/**
* This method is similar to {@link #getAffectedUsers(Group)} except that it receives
* some group properties. The group properties are passed as parameters since the called of this
* method may want to obtain the related users of the group based in some properties values.
*
* This is useful when the group is being edited and some properties has changed and we need to
* obtain the related users of the group based on the previous group state.
*/
private Collection<JID> getAffectedUsers(Group group, String showInRoster, String groupNames) {
// Answer an empty collection if the group is not being shown in users' rosters
if (!"onlyGroup".equals(showInRoster) && !"everybody".equals(showInRoster)) {
return new ArrayList<JID>();
}
// Add the users of the group
Collection<JID> users = new HashSet<JID>(group.getMembers());
users.addAll(group.getAdmins());
// Check if anyone can see this shared group
if ("everybody".equals(showInRoster)) {
// Add all users in the system
for (String username : UserManager.getInstance().getUsernames()) {
users.add(server.createJID(username, null, true));
}
// Add all logged users. We don't need to add all users in the system since only the
// logged ones will be affected.
//users.addAll(SessionManager.getInstance().getSessionUsers());
}
else {
// Add the users that may see the group
Collection<Group> groupList = parseGroups(groupNames);
for (Group groupInList : groupList) {
users.addAll(groupInList.getMembers());
users.addAll(groupInList.getAdmins());
}
}
return users;
}
Collection<JID> getSharedUsersForRoster(Group group, Roster roster) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
String groupNames = group.getProperties().get("sharedRoster.groupList");
// Answer an empty collection if the group is not being shown in users' rosters
if (!"onlyGroup".equals(showInRoster) && !"everybody".equals(showInRoster)) {
return new ArrayList<JID>();
}
// Add the users of the group
Collection<JID> users = new HashSet<JID>(group.getMembers());
users.addAll(group.getAdmins());
// If the user of the roster belongs to the shared group then we should return
// users that need to be in the roster with subscription "from"
if (group.isUser(roster.getUsername())) {
// Check if anyone can see this shared group
if ("everybody".equals(showInRoster)) {
// Add all users in the system
for (String username : UserManager.getInstance().getUsernames()) {
users.add(server.createJID(username, null, true));
}
}
else {
// Add the users that may see the group
Collection<Group> groupList = parseGroups(groupNames);
for (Group groupInList : groupList) {
users.addAll(groupInList.getMembers());
users.addAll(groupInList.getAdmins());
}
}
}
return users;
}
/**
* Returns true if a group in the first collection may mutually see a group of the
* second collection. More precisely, return true if both collections contain a public
* group (i.e. anybody can see the group) or if both collection have a group that may see
* each other and the users are members of those groups or if one group is public and the
* other group allowed the public group to see it.
*
* @param user the name of the user associated to the first collection of groups. This is always a local user.
* @param groups a collection of groups to check against the other collection of groups.
* @param otherUser the JID of the user associated to the second collection of groups.
* @param otherGroups the other collection of groups to check against the first collection.
* @return true if a group in the first collection may mutually see a group of the
* second collection.
*/
boolean hasMutualVisibility(String user, Collection<Group> groups, JID otherUser,
Collection<Group> otherGroups) {
for (Group group : groups) {
for (Group otherGroup : otherGroups) {
// Skip this groups if the users are not group users of the groups
if (!group.isUser(user) || !otherGroup.isUser(otherUser)) {
continue;
}
if (group.equals(otherGroup)) {
return true;
}
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
String otherShowInRoster = otherGroup.getProperties().get("sharedRoster.showInRoster");
// Return true if both groups are public groups (i.e. anybody can see them)
if ("everybody".equals(showInRoster) && "everybody".equals(otherShowInRoster)) {
return true;
}
else if ("onlyGroup".equals(showInRoster) && "onlyGroup".equals(otherShowInRoster)) {
String groupNames = group.getProperties().get("sharedRoster.groupList");
String otherGroupNames = otherGroup.getProperties().get("sharedRoster.groupList");
// Return true if each group may see the other group
if (groupNames != null && otherGroupNames != null) {
if (groupNames.contains(otherGroup.getName()) &&
otherGroupNames.contains(group.getName())) {
return true;
}
// Check if each shared group can be seen by a group where each user belongs
Collection<Group> groupList = parseGroups(groupNames);
Collection<Group> otherGroupList = parseGroups(otherGroupNames);
for (Group groupName : groupList) {
if (groupName.isUser(otherUser)) {
for (Group otherGroupName : otherGroupList) {
if (otherGroupName.isUser(user)) {
return true;
}
}
}
}
}
}
else if ("everybody".equals(showInRoster) && "onlyGroup".equals(otherShowInRoster)) {
// Return true if one group is public and the other group allowed the public
// group to see him
String otherGroupNames = otherGroup.getProperties().get("sharedRoster.groupList");
if (otherGroupNames != null && otherGroupNames.contains(group.getName())) {
return true;
}
}
else if ("onlyGroup".equals(showInRoster) && "everybody".equals(otherShowInRoster)) {
// Return true if one group is public and the other group allowed the public
// group to see him
String groupNames = group.getProperties().get("sharedRoster.groupList");
// Return true if each group may see the other group
if (groupNames != null && groupNames.contains(otherGroup.getName())) {
return true;
}
}
}
}
return false;
}
@Override
public void start() throws IllegalStateException {
super.start();
// Add this module as a user event listener so we can update
// rosters when users are created or deleted
UserEventDispatcher.addListener(this);
// Add the new instance as a listener of group events
GroupEventDispatcher.addListener(this);
}
@Override
public void stop() {
super.stop();
// Remove this module as a user event listener
UserEventDispatcher.removeListener(this);
// Remove this module as a listener of group events
GroupEventDispatcher.removeListener(this);
}
public static RosterItemProvider getRosterItemProvider() {
return XMPPServer.getInstance().getRosterManager().provider;
}
private void initProvider() {
JiveGlobals.migrateProperty("provider.roster.className");
String className = JiveGlobals.getProperty("provider.roster.className",
"org.jivesoftware.openfire.roster.DefaultRosterItemProvider");
if (provider == null || !className.equals(provider.getClass().getName())) {
try {
Class c = ClassUtils.forName(className);
provider = (RosterItemProvider) c.newInstance();
}
catch (Exception e) {
Log.error("Error loading roster provider: " + className, e);
provider = new DefaultRosterItemProvider();
}
}
}
}
......@@ -26,6 +26,7 @@ import org.jivesoftware.openfire.container.Plugin;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.roster.RosterItem;
import org.jivesoftware.openfire.roster.RosterItemProvider;
import org.jivesoftware.openfire.roster.RosterManager;
import org.jivesoftware.openfire.user.User;
import org.jivesoftware.openfire.user.UserAlreadyExistsException;
import org.jivesoftware.openfire.user.UserManager;
......@@ -210,7 +211,7 @@ public class ImportExportPlugin implements Plugin {
List<String> invalidUsers = new ArrayList<String>();
UserManager userManager = UserManager.getInstance();
RosterItemProvider rosterItemProvider = RosterItemProvider.getInstance();
RosterItemProvider rosterItemProvider = RosterManager.getRosterItemProvider();
Element users = document.getRootElement();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment