Commit 6e5db356 authored by Tom Evans's avatar Tom Evans Committed by tevans

OF-278: Avoid loading all shared groups into memory/cache when possible.

NOTE: This is a partial refactor of the Openfire groups component. Additional review and testing is requested. Please update the corresponding JIRA ticket with comments or concerns.

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@13380 b35dd754-fafc-0310-a699-88a17e54d16e
parent ec7e0ff9
......@@ -18,25 +18,31 @@
*/
package org.jivesoftware.openfire.clearspace;
import org.dom4j.Element;
import org.dom4j.Node;
import org.jivesoftware.openfire.XMPPServer;
import static org.jivesoftware.openfire.clearspace.ClearspaceManager.HttpType.GET;
import static org.jivesoftware.openfire.clearspace.WSUtils.getReturn;
import static org.jivesoftware.openfire.clearspace.WSUtils.parseStringArray;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.dom4j.Element;
import org.dom4j.Node;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.group.AbstractReadOnlyGroupProvider;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.group.GroupAlreadyExistsException;
import org.jivesoftware.openfire.group.GroupCollection;
import org.jivesoftware.openfire.group.GroupNotFoundException;
import org.jivesoftware.openfire.group.GroupProvider;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.xmpp.packet.JID;
import java.util.*;
/**
* @author Daniel Henninger
*/
public class ClearspaceGroupProvider implements GroupProvider {
public class ClearspaceGroupProvider extends AbstractReadOnlyGroupProvider {
protected static final String URL_PREFIX = "socialGroupService/";
private static final String TYPE_ID_OWNER = "0";
......@@ -45,26 +51,10 @@ public class ClearspaceGroupProvider implements GroupProvider {
public ClearspaceGroupProvider() {
}
public Group createGroup(String name) throws UnsupportedOperationException, GroupAlreadyExistsException {
throw new UnsupportedOperationException("Could not create groups.");
}
public void deleteGroup(String name) throws UnsupportedOperationException {
throw new UnsupportedOperationException("Could not delete groups.");
}
public Group getGroup(String name) throws GroupNotFoundException {
return translateGroup(getGroupByName(name));
}
public void setName(String oldName, String newName) throws UnsupportedOperationException, GroupAlreadyExistsException {
throw new UnsupportedOperationException("Could not modify groups.");
}
public void setDescription(String name, String description) throws GroupNotFoundException {
throw new UnsupportedOperationException("Could not modify groups.");
}
public int getGroupCount() {
try {
String path = URL_PREFIX + "socialGroupCount";
......@@ -76,11 +66,28 @@ public class ClearspaceGroupProvider implements GroupProvider {
}
}
public Collection<String> getSharedGroupsNames() {
public boolean isSharingSupported() {
return true;
}
public Collection<String> getSharedGroupNames() {
// Return all social group names since every social group is a shared group
return getGroupNames();
}
public Collection<String> getSharedGroupNames(JID user) {
// TODO: is there a better way to get the shared Clearspace groups for a given user?
Collection<String> result = new ArrayList<String>();
Iterator<Group> sharedGroups = new GroupCollection(getGroupNames()).iterator();
while (sharedGroups.hasNext()) {
Group group = sharedGroups.next();
if (group.isUser(user)) {
result.add(group.getName());
}
}
return result;
}
public Collection<String> getGroupNames() {
try {
String path = URL_PREFIX + "socialGroupNames";
......@@ -120,22 +127,6 @@ public class ClearspaceGroupProvider implements GroupProvider {
}
}
public void addMember(String groupName, JID user, boolean administrator) throws UnsupportedOperationException {
throw new UnsupportedOperationException("Could not modify groups.");
}
public void updateMember(String groupName, JID user, boolean administrator) throws UnsupportedOperationException {
throw new UnsupportedOperationException("Could not modify groups.");
}
public void deleteMember(String groupName, JID user) throws UnsupportedOperationException {
throw new UnsupportedOperationException("Could not modify groups.");
}
public boolean isReadOnly() {
return true;
}
public Collection<String> search(String query) {
throw new UnsupportedOperationException("Group search is not supported");
}
......
package org.jivesoftware.openfire.group;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import org.jivesoftware.util.Immutable;
import org.xmpp.packet.JID;
/**
* Common base class for immutable (read-only) GroupProvider implementations.
*
* @author Tom Evans
*/
public abstract class AbstractReadOnlyGroupProvider implements GroupProvider {
// Mutator methods are marked final for read-only group providers
/**
* @throws UnsupportedOperationException
*/
public final void addMember(String groupName, JID user, boolean administrator) throws UnsupportedOperationException
{
throw new UnsupportedOperationException("Cannot add members to read-only groups");
}
/**
* @throws UnsupportedOperationException
*/
public final void updateMember(String groupName, JID user, boolean administrator) throws UnsupportedOperationException
{
throw new UnsupportedOperationException("Cannot update members for read-only groups");
}
/**
* @throws UnsupportedOperationException
*/
public final void deleteMember(String groupName, JID user) throws UnsupportedOperationException
{
throw new UnsupportedOperationException("Cannot remove members from read-only groups");
}
/**
* Always true for a read-only provider
*/
public final boolean isReadOnly() {
return true;
}
/**
* @throws UnsupportedOperationException
*/
public final Group createGroup(String name) throws UnsupportedOperationException {
throw new UnsupportedOperationException("Cannot create groups via read-only provider");
}
/**
* @throws UnsupportedOperationException
*/
public final void deleteGroup(String name) throws UnsupportedOperationException {
throw new UnsupportedOperationException("Cannot remove groups via read-only provider");
}
/**
* @throws UnsupportedOperationException
*/
public final void setName(String oldName, String newName) throws UnsupportedOperationException {
throw new UnsupportedOperationException("Cannot modify read-only groups");
}
/**
* @throws UnsupportedOperationException
*/
public final void setDescription(String name, String description) throws UnsupportedOperationException {
throw new UnsupportedOperationException("Cannot modify read-only groups");
}
// Search methods may be overridden by read-only group providers
/**
* Returns true if the provider supports group search capability. This implementation
* always returns false.
*/
public boolean isSearchSupported() {
return false;
}
/**
* Returns a collection of group search results. This implementation
* returns an empty collection.
*/
public Collection<String> search(String query) {
return Collections.emptyList();
}
/**
* Returns a collection of group search results. This implementation
* returns an empty collection.
*/
public Collection<String> search(String query, int startIndex, int numResults) {
return Collections.emptyList();
}
/**
* Returns a collection of group search results. This implementation
* returns an empty collection.
*/
public Collection<String> search(String key, String value) {
return Collections.emptyList();
}
// Shared group methods may be overridden by read-only group providers
/**
* Returns true if the provider supports group sharing. This implementation
* always returns false.
*/
public boolean isSharingSupported() {
return false;
}
/**
* Returns a collection of shared group names. This implementation
* returns an empty collection.
*/
public Collection<String> getSharedGroupNames() {
return Collections.emptyList();
}
/**
* Returns a collection of shared group names for the given user. This
* implementation returns an empty collection.
*/
public Collection<String> getSharedGroupNames(JID user) {
return Collections.emptyList();
}
/**
* Returns a collection of shared public group names. This
* implementation returns an empty collection.
*/
public Collection<String> getPublicSharedGroupNames() {
return Collections.emptyList();
}
/**
* Returns a collection of groups shared with the given group. This
* implementation returns an empty collection.
*/
public Collection<String> getVisibleGroupNames(String userGroup) {
return Collections.emptyList();
}
/**
* Returns a map of properties for the given group. This
* implementation returns an empty immutable map.
*/
public Map<String, String> loadProperties(Group group) {
return new Immutable.Map<String,String>();
}
}
package org.jivesoftware.openfire.group;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.event.GroupEventDispatcher;
import org.jivesoftware.util.Immutable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Default implementation of a writable {@link Map} to manage group properties.
* Updates made to the elements in this map will also be applied to the database.
* Note this implementation assumes group property changes will be relatively
* infrequent and therefore does not try to optimize database I/O for performance.
* Each call to a {@link Map} mutator method (direct or indirect via {@link Iterator})
* will result in a corresponding synchronous update to the database.
*
* @param <K> Property key
* @param <V> Property value
*/
public class DefaultGroupPropertyMap<K,V> extends HashMap<K,V> {
private static final long serialVersionUID = 3128889631577167040L;
private static final Logger logger = LoggerFactory.getLogger(DefaultGroupPropertyMap.class);
// moved from {@link Group} as these are specific to the default provider
private static final String DELETE_PROPERTY =
"DELETE FROM ofGroupProp WHERE groupName=? AND name=?";
private static final String DELETE_ALL_PROPERTIES =
"DELETE FROM ofGroupProp WHERE groupName=?";
private static final String UPDATE_PROPERTY =
"UPDATE ofGroupProp SET propValue=? WHERE name=? AND groupName=?";
private static final String INSERT_PROPERTY =
"INSERT INTO ofGroupProp (groupName, name, propValue) VALUES (?, ?, ?)";
private Group group;
/**
* Group properties map constructor; requires associated {@link Group} instance
* @param group The group that owns these properties
*/
public DefaultGroupPropertyMap(Group group) {
this.group = group;
}
/**
* Custom method to put properties into the map, optionally without
* triggering persistence. This is used when the map is being
* initially loaded from the database.
*
* @param key The property name
* @param value The property value
* @param persist True if the changes should be persisted to the database
* @return The original value or null if the property did not exist
*/
public V put(K key, V value, boolean persist) {
V originalValue = super.put(key, value);
// we only support persistence for <String, String>
if (persist && key instanceof String && value instanceof String) {
if (originalValue instanceof String) { // existing property
updateProperty((String)key, (String)value, (String)originalValue);
} else {
insertProperty((String)key, (String)value);
}
}
return originalValue;
}
@Override
public V put(K key, V value) {
if (value == null) { // treat null value as "remove"
return remove(key);
} else {
return put(key, value, true);
}
}
@Override
public V remove(Object key) {
V result = super.remove(key);
if (key instanceof String) {
deleteProperty((String)key);
}
return result;
}
@Override
public void clear() {
super.clear();
deleteAllProperties();
}
@Override
public Set<K> keySet() {
// custom class needed here to handle key.remove()
return new PersistenceAwareKeySet<K>(super.keySet());
}
@Override
public Collection<V> values() {
// custom class needed here to suppress value.remove()
return (Collection<V>) new Immutable.Collection<V>(super.values());
}
@Override
public Set<Entry<K, V>> entrySet() {
// custom class needed here to handle entrySet mutators
return new PersistenceAwareEntrySet<Entry<K,V>>(super.entrySet());
}
/**
* Persistence-aware {@link Set} for group property keys. This class returns
* a custom iterator that can handle property removal.
*/
private class PersistenceAwareKeySet<E> extends AbstractSet<K> {
private Set<K> delegate;
/**
* Sole constructor; requires wrapped {@link Set} for delegation
* @param delegate A collection of keys from the map
*/
public PersistenceAwareKeySet(Set<K> delegate) {
this.delegate = delegate;
}
@Override
public Iterator<K> iterator() {
return new KeyIterator<E>(delegate.iterator());
}
@Override
public int size() {
return delegate.size();
}
}
/**
* This iterator updates the database when a property key is removed.
*/
private class KeyIterator<E> implements Iterator<K> {
private Iterator<K> delegate;
private K current;
/**
* Sole constructor; requires wrapped {@link Iterator} for delegation
* @param delegate An iterator for all the keys from the map
*/
public KeyIterator(Iterator<K> delegate) {
this.delegate = delegate;
}
/**
* Delegated to corresponding method in the backing {@link Iterator}
*/
public boolean hasNext() {
return delegate.hasNext();
}
/**
* Delegated to corresponding method in the backing {@link Iterator}
*/
public K next() {
current = delegate.next();
return current;
}
/**
* Removes the property corresponding to the current key from
* the underlying map. Also applies update to the database.
*/
public void remove() {
delegate.remove();
if (current instanceof String) {
deleteProperty((String)current);
}
current = null;
}
}
/**
* Persistence-aware {@link Set} for group properties (as {@link Map.Entry})
*/
private class PersistenceAwareEntrySet<E> implements Set<Entry<K, V>> {
private Set<Entry<K, V>> delegate;
/**
* Sole constructor; requires wrapped {@link Set} for delegation
* @param delegate A collection of entries ({@link Map.Entry}) from the map
*/
public PersistenceAwareEntrySet(Set<Entry<K, V>> delegate) {
this.delegate = delegate;
}
/**
* Returns a custom iterator for the entries in the backing map
*/
public Iterator<Entry<K, V>> iterator() {
return new EntryIterator<Entry<K,V>>(delegate.iterator());
}
/**
* Removes the given key from the backing map, and applies the
* corresponding update to the database.
*
* @param o A {@link Map.Entry} within this set
* @return True if the set contained the given key
*/
public boolean remove(Object o) {
boolean propertyExists = delegate.remove(o);
if (propertyExists) {
deleteProperty((String)((Entry<K,V>)o).getKey());
}
return propertyExists;
}
/**
* Removes all the elements in the set, and applies the
* corresponding update to the database.
*/
public void clear() {
delegate.clear();
deleteAllProperties();
}
// these methods are problematic (and not really necessary),
// so they are not implemented
/**
* @throws UnsupportedOperationException
*/
public boolean removeAll(Collection<?> c) {
throw new UnsupportedOperationException();
}
/**
* @throws UnsupportedOperationException
*/
public boolean retainAll(Collection<?> c) {
throw new UnsupportedOperationException();
}
// per docs for {@link Map.entrySet}, these methods are not supported
/**
* @throws UnsupportedOperationException
*/
public boolean add(Entry<K, V> o) {
return delegate.add(o);
}
/**
* @throws UnsupportedOperationException
*/
public boolean addAll(Collection<? extends Entry<K, V>> c) {
return delegate.addAll(c);
}
// remaining {@link Set} methods can be delegated safely
/**
* Delegated to corresponding method in the backing {@link Set}
*/
public int size() {
return delegate.size();
}
/**
* Delegated to corresponding method in the backing {@link Set}
*/
public boolean isEmpty() {
return delegate.isEmpty();
}
/**
* Delegated to corresponding method in the backing {@link Set}
*/
public boolean contains(Object o) {
return delegate.contains(o);
}
/**
* Delegated to corresponding method in the backing {@link Set}
*/
public Object[] toArray() {
return delegate.toArray();
}
/**
* Delegated to corresponding method in the backing {@link Set}
*/
public <T> T[] toArray(T[] a) {
return delegate.toArray(a);
}
/**
* Delegated to corresponding method in the backing {@link Set}
*/
public boolean containsAll(Collection<?> c) {
return delegate.containsAll(c);
}
/**
* Delegated to corresponding method in the backing {@link Set}
*/
public boolean equals(Object o) {
return delegate.equals(o);
}
/**
* Delegated to corresponding method in the backing {@link Set}
*/
public int hashCode() {
return delegate.hashCode();
}
}
/**
* Remove group property from the database when the {@link Iterator.remove}
* method is invoked via the {@link Map.entrySet} set
*/
private class EntryIterator<E> implements Iterator<Entry<K, V>> {
private Iterator<Entry<K,V>> delegate;
private EntryWrapper<E> current;
/**
* Sole constructor; requires wrapped {@link Iterator} for delegation
* @param delegate An iterator for all the keys from the map
*/
public EntryIterator(Iterator<Entry<K,V>> delegate) {
this.delegate = delegate;
}
/**
* Delegated to corresponding method in the backing {@link Iterator}
*/
public boolean hasNext() {
return delegate.hasNext();
}
/**
* Delegated to corresponding method in the backing {@link Iterator}
*/
public Entry<K,V> next() {
current = new EntryWrapper<E>(delegate.next());
return current;
}
/**
* Removes the property corresponding to the current key from
* the underlying map. Also applies update to the database.
*/
public void remove() {
delegate.remove();
K key = current.getKey();
if (key instanceof String) {
deleteProperty((String)key);
}
current = null;
}
}
/**
* Update the database when a group property is updated via {@link Map.Entry.setValue}
*/
private class EntryWrapper<E> implements Entry<K,V> {
private Entry<K,V> delegate;
/**
* Sole constructor; requires wrapped {@link Map.Entry} for delegation
* @param delegate The corresponding entry from the map
*/
public EntryWrapper(Entry<K,V> delegate) {
this.delegate = delegate;
}
/**
* Delegated to corresponding method in the backing {@link Map.Entry}
*/
public K getKey() {
return delegate.getKey();
}
/**
* Delegated to corresponding method in the backing {@link Map.Entry}
*/
public V getValue() {
return delegate.getValue();
}
/**
* Set the value of the property corresponding to this entry. This
* method also updates the database as needed depending on the new
* property value. A null value will cause the property to be deleted
* from the database.
*
* @param value The new property value
* @return The old value of the corresponding property
*/
public V setValue(V value) {
V oldValue = delegate.setValue(value);
K key = delegate.getKey();
if (key instanceof String) {
if (value instanceof String) {
if (oldValue == null) {
insertProperty((String) key, (String) value);
} else if (!value.equals(oldValue)) {
updateProperty((String)key,(String)value, (String)oldValue);
}
} else {
deleteProperty((String)key);
}
}
return oldValue;
}
}
/**
* Persist a new group property to the database for the current group
*
* @param key Property name
* @param value Property value
*/
private synchronized void insertProperty(String key, String value) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(INSERT_PROPERTY);
pstmt.setString(1, group.getName());
pstmt.setString(2, key);
pstmt.setString(3, value);
pstmt.executeUpdate();
}
catch (SQLException e) {
logger.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
Map<String, Object> event = new HashMap<String, Object>();
event.put("propertyKey", key);
event.put("type", "propertyAdded");
GroupEventDispatcher.dispatchEvent(group,
GroupEventDispatcher.EventType.group_modified, event);
}
/**
* Update the value of an existing group property for the current group
*
* @param key Property name
* @param value Property value
* @param originalValue Original property value
*/
private synchronized void updateProperty(String key, String value, String originalValue) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(UPDATE_PROPERTY);
pstmt.setString(1, key);
pstmt.setString(2, value);
pstmt.setString(3, group.getName());
pstmt.executeUpdate();
}
catch (SQLException e) {
logger.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
Map<String, Object> event = new HashMap<String, Object>();
event.put("propertyKey", key);
event.put("type", "propertyModified");
event.put("originalValue", originalValue);
GroupEventDispatcher.dispatchEvent(group,
GroupEventDispatcher.EventType.group_modified, event);
}
/**
* Delete a group property from the database for the current group
*
* @param key Property name
*/
private synchronized void deleteProperty(String key) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(DELETE_PROPERTY);
pstmt.setString(1, group.getName());
pstmt.setString(2, key);
pstmt.executeUpdate();
}
catch (SQLException e) {
logger.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
Map<String, Object> event = new HashMap<String, Object>();
event.put("type", "propertyDeleted");
event.put("propertyKey", key);
GroupEventDispatcher.dispatchEvent(group,
GroupEventDispatcher.EventType.group_modified, event);
}
/**
* Delete all properties from the database for the current group
*/
private synchronized void deleteAllProperties() {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(DELETE_ALL_PROPERTIES);
pstmt.setString(1, group.getName());
pstmt.executeUpdate();
}
catch (SQLException e) {
logger.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
Map<String, Object> event = new HashMap<String, Object>();
event.put("type", "propertyDeleted");
event.put("propertyKey", "*");
GroupEventDispatcher.dispatchEvent(group,
GroupEventDispatcher.EventType.group_modified, event);
}
}
......@@ -27,7 +27,10 @@ import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.XMPPServer;
......@@ -75,8 +78,25 @@ public class DefaultGroupProvider implements GroupProvider {
"UPDATE ofGroupUser SET administrator=? WHERE groupName=? AND username=?";
private static final String USER_GROUPS =
"SELECT groupName FROM ofGroupUser WHERE username=?";
private static final String GROUPLIST_CONTAINERS =
"SELECT groupName from ofGroupProp " +
"where name='sharedRoster.groupList' " +
"AND propValue LIKE ?";
private static final String PUBLIC_GROUPS =
"SELECT groupName from ofGroupProp " +
"WHERE name='sharedRoster.showInRoster' " +
"AND propValue='everybody'";
private static final String GROUPS_FOR_PROP =
"SELECT groupName from ofGroupProp " +
"WHERE name=? " +
"AND propValue=?";
private static final String ALL_GROUPS = "SELECT groupName FROM ofGroup ORDER BY groupName";
private static final String SEARCH_GROUP_NAME = "SELECT groupName FROM ofGroup WHERE groupName LIKE ? ORDER BY groupName";
private static final String LOAD_SHARED_GROUPS =
"SELECT groupName FROM ofGroupProp WHERE name='sharedRoster.showInRoster' " +
"AND propValue IS NOT NULL AND propValue <> 'nobody'";
private static final String LOAD_PROPERTIES =
"SELECT name, propValue FROM ofGroupProp WHERE groupName=?";
private XMPPServer server = XMPPServer.getInstance();
......@@ -240,9 +260,111 @@ public class DefaultGroupProvider implements GroupProvider {
return count;
}
public Collection<String> getSharedGroupsNames() {
// Get the list of shared groups from the database
return Group.getSharedGroupsNames();
/**
* Returns the name of the groups that are shared groups.
*
* @return the name of the groups that are shared groups.
*/
public Collection<String> getSharedGroupNames() {
Collection<String> groupNames = new HashSet<String>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_SHARED_GROUPS);
rs = pstmt.executeQuery();
while (rs.next()) {
groupNames.add(rs.getString(1));
}
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return groupNames;
}
public Collection<String> getSharedGroupNames(JID user) {
Set<String> answer = new HashSet<String>();
Collection<String> userGroups = getGroupNames(user);
answer.addAll(userGroups);
for (String userGroup : userGroups) {
answer.addAll(getVisibleGroupNames(userGroup));
}
answer.addAll(getPublicSharedGroupNames());
return answer;
}
public Collection<String> getVisibleGroupNames(String userGroup) {
Set<String> groupNames = new HashSet<String>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(GROUPLIST_CONTAINERS);
pstmt.setString(1, "%" + userGroup + "%");
rs = pstmt.executeQuery();
while (rs.next()) {
groupNames.add(rs.getString(1));
}
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return groupNames;
}
public Collection<String> search(String key, String value) {
Set<String> groupNames = new HashSet<String>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(GROUPS_FOR_PROP);
pstmt.setString(1, key);
pstmt.setString(2, value);
rs = pstmt.executeQuery();
while (rs.next()) {
groupNames.add(rs.getString(1));
}
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return groupNames;
}
public Collection<String> getPublicSharedGroupNames() {
Set<String> groupNames = new HashSet<String>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(PUBLIC_GROUPS);
rs = pstmt.executeQuery();
while (rs.next()) {
groupNames.add(rs.getString(1));
}
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return groupNames;
}
public Collection<String> getGroupNames() {
......@@ -432,6 +554,10 @@ public class DefaultGroupProvider implements GroupProvider {
return true;
}
public boolean isSharingSupported() {
return true;
}
private Collection<JID> getMembers(String groupName, boolean adminsOnly) {
List<JID> members = new ArrayList<JID>();
Connection con = null;
......@@ -470,4 +596,50 @@ public class DefaultGroupProvider implements GroupProvider {
}
return members;
}
/**
* Returns a custom {@link Map} that updates the database whenever
* a property value is added, changed, or deleted.
*
* @param name The target group
* @return The properties for the given group
*/
public Map<String,String> loadProperties(Group group) {
// custom map implementation persists group property changes
// whenever one of the standard mutator methods are called
String name = group.getName();
DefaultGroupPropertyMap<String,String> result = new DefaultGroupPropertyMap<String,String>(group);
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_PROPERTIES);
pstmt.setString(1, name);
rs = pstmt.executeQuery();
while (rs.next()) {
String key = rs.getString(1);
String value = rs.getString(2);
if (key != null) {
if (value == null) {
result.remove(key);
Log.warn("Deleted null property " + key + " for group: " + name);
} else {
result.put(key, value, false); // skip persistence during load
}
}
else { // should not happen, but ...
Log.warn("Ignoring null property key for group: " + name);
}
}
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return result;
}
}
\ No newline at end of file
......@@ -24,22 +24,14 @@ import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.AbstractCollection;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.event.GroupEventDispatcher;
import org.jivesoftware.util.cache.CacheSizes;
......@@ -65,54 +57,15 @@ public class Group implements Cacheable, Externalizable {
private static final Logger Log = LoggerFactory.getLogger(Group.class);
private static final String LOAD_PROPERTIES =
"SELECT name, propValue FROM ofGroupProp WHERE groupName=?";
private static final String DELETE_PROPERTY =
"DELETE FROM ofGroupProp WHERE groupName=? AND name=?";
private static final String UPDATE_PROPERTY =
"UPDATE ofGroupProp SET propValue=? WHERE name=? AND groupName=?";
private static final String INSERT_PROPERTY =
"INSERT INTO ofGroupProp (groupName, name, propValue) VALUES (?, ?, ?)";
private static final String LOAD_SHARED_GROUPS =
"SELECT groupName FROM ofGroupProp WHERE name='sharedRoster.showInRoster' " +
"AND propValue IS NOT NULL AND propValue <> 'nobody'";
private transient GroupProvider provider;
private transient GroupManager groupManager;
private transient Map<String, String> properties;
private String name;
private String description;
private Map<String, String> properties;
private Set<JID> members;
private Set<JID> administrators;
/**
* Returns the name of the groups that are shared groups.
*
* @return the name of the groups that are shared groups.
*/
public static Set<String> getSharedGroupsNames() {
Set<String> groupNames = new HashSet<String>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_SHARED_GROUPS);
rs = pstmt.executeQuery();
while (rs.next()) {
groupNames.add(rs.getString(1));
}
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return groupNames;
}
/**
* Constructor added for Externalizable. Do not use this constructor.
*/
......@@ -161,37 +114,21 @@ public class Group implements Cacheable, Externalizable {
this.members = new HashSet<JID>(members);
this.administrators = new HashSet<JID>(administrators);
this.properties = new ConcurrentHashMap<String, String>();
// Load the properties that this groups has in the DB
loadProperties();
this.properties = provider.loadProperties(this);
// Check if we have to create or update some properties
if (!provider.isReadOnly()) {
// Apply the given properties to the group
for (Map.Entry<String, String> property : properties.entrySet()) {
// If the DB contains this property
if (this.properties.containsKey(property.getKey())) {
// then check if we need to update it
if (!property.getValue().equals(this.properties.get(property.getKey()))) {
// update the properties map
this.properties.put(property.getKey(), property.getValue());
// and the DB
updateProperty(property.getKey(), property.getValue());
}
} // else we need to add it
else {
// add to the properties map
this.properties.put(property.getKey(), property.getValue());
// and insert it to the DB
insertProperty(property.getKey(), property.getValue());
}
// Remove obsolete properties
Iterator<String> oldProps = this.properties.keySet().iterator();
while (oldProps.hasNext()) {
if (!properties.containsKey(oldProps.next())) {
oldProps.remove();
}
// Check if we have to delete some properties
for (String oldPropName : this.properties.keySet()) {
if (!properties.containsKey(oldPropName)) {
// delete it from the property map
this.properties.remove(oldPropName);
// delete it from the DB
deleteProperty(oldPropName);
}
}
}
......@@ -219,10 +156,8 @@ public class Group implements Cacheable, Externalizable {
}
try {
String originalName = this.name;
provider.setName(this.name, name);
groupManager.groupCache.remove(this.name);
provider.setName(originalName, name);
this.name = name;
groupManager.groupCache.put(name, this);
// Fire event.
Map<String, Object> params = new HashMap<String, Object>();
......@@ -231,8 +166,8 @@ public class Group implements Cacheable, Externalizable {
GroupEventDispatcher.dispatchEvent(this, GroupEventDispatcher.EventType.group_modified,
params);
}
catch (Exception e) {
Log.error(e.getMessage(), e);
catch (GroupAlreadyExistsException e) {
Log.error("Failed to change group name; group already exists");
}
}
......@@ -291,12 +226,11 @@ public class Group implements Cacheable, Externalizable {
public Map<String,String> getProperties() {
synchronized (this) {
if (properties == null) {
properties = new ConcurrentHashMap<String, String>();
loadProperties();
properties = provider.loadProperties(this);
}
}
// Return a wrapper that will intercept add and remove commands.
return new PropertiesMap();
return properties;
}
/**
......@@ -516,186 +450,6 @@ public class Group implements Cacheable, Externalizable {
}
}
/**
* Map implementation that updates the database when properties are modified.
*/
private class PropertiesMap extends AbstractMap {
@Override
public Object put(Object key, Object value) {
if (key == null || value == null) {
throw new NullPointerException();
}
Map<String, Object> eventParams = new HashMap<String, Object>();
Object answer;
String keyString = (String) key;
synchronized (keyString.intern()) {
if (properties.containsKey(keyString)) {
String originalValue = properties.get(keyString);
// if is the same value don't update it.
if (originalValue != null && originalValue.equals(value)) {
return value;
}
answer = properties.put(keyString, (String)value);
updateProperty(keyString, (String)value);
// Configure event.
eventParams.put("type", "propertyModified");
eventParams.put("propertyKey", key);
eventParams.put("originalValue", originalValue);
}
else {
answer = properties.put(keyString, (String)value);
insertProperty(keyString, (String)value);
// Configure event.
eventParams.put("type", "propertyAdded");
eventParams.put("propertyKey", key);
}
}
// Fire event.
GroupEventDispatcher.dispatchEvent(Group.this,
GroupEventDispatcher.EventType.group_modified, eventParams);
return answer;
}
@Override
public Set<Entry> entrySet() {
return new PropertiesEntrySet();
}
}
/**
* Set implementation that updates the database when properties are deleted.
*/
private class PropertiesEntrySet extends AbstractSet {
@Override
public int size() {
return properties.entrySet().size();
}
@Override
public Iterator iterator() {
return new Iterator() {
Iterator iter = properties.entrySet().iterator();
Map.Entry current = null;
public boolean hasNext() {
return iter.hasNext();
}
public Object next() {
current = (Map.Entry)iter.next();
return current;
}
public void remove() {
if (current == null) {
throw new IllegalStateException();
}
String key = (String)current.getKey();
deleteProperty(key);
iter.remove();
// Fire event.
Map<String, Object> params = new HashMap<String, Object>();
params.put("type", "propertyDeleted");
params.put("propertyKey", key);
GroupEventDispatcher.dispatchEvent(Group.this,
GroupEventDispatcher.EventType.group_modified, params);
}
};
}
}
private void loadProperties() {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_PROPERTIES);
pstmt.setString(1, name);
rs = pstmt.executeQuery();
while (rs.next()) {
String key = rs.getString(1);
String value = rs.getString(2);
if (key != null) {
if (value == null) {
value = "";
Log.warn("There is a group property whose value is null of Group: " + name);
}
properties.put(key, value);
}
else {
Log.warn("There is a group property whose key is null of Group: " + name);
}
}
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
private void insertProperty(String propName, String propValue) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(INSERT_PROPERTY);
pstmt.setString(1, name);
pstmt.setString(2, propName);
pstmt.setString(3, propValue);
pstmt.executeUpdate();
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}
private void updateProperty(String propName, String propValue) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(UPDATE_PROPERTY);
pstmt.setString(1, propValue);
pstmt.setString(2, propName);
pstmt.setString(3, name);
pstmt.executeUpdate();
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}
private void deleteProperty(String propName) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(DELETE_PROPERTY);
pstmt.setString(1, name);
pstmt.setString(2, propName);
pstmt.executeUpdate();
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}
public void writeExternal(ObjectOutput out) throws IOException {
ExternalizableUtil.getInstance().writeSafeUTF(out, name);
ExternalizableUtil.getInstance().writeBoolean(out, description != null);
......
......@@ -51,7 +51,7 @@ public class GroupCollection extends AbstractCollection {
@Override
public Iterator iterator() {
return new UserIterator();
return new GroupIterator();
}
@Override
......@@ -59,7 +59,7 @@ public class GroupCollection extends AbstractCollection {
return elements.length;
}
private class UserIterator implements Iterator {
private class GroupIterator implements Iterator {
private int currentIndex = -1;
private Object nextElement = null;
......
......@@ -31,13 +31,10 @@ import org.jivesoftware.openfire.event.GroupEventListener;
import org.jivesoftware.openfire.event.UserEventDispatcher;
import org.jivesoftware.openfire.event.UserEventListener;
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.TaskEngine;
import org.jivesoftware.util.cache.Cache;
import org.jivesoftware.util.cache.CacheFactory;
import org.slf4j.Logger;
......@@ -61,6 +58,7 @@ public class GroupManager {
private static final String GROUP_COUNT_KEY = "GROUP_COUNT";
private static final String SHARED_GROUPS_KEY = "SHARED_GROUPS";
private static final String GROUP_NAMES_KEY = "GROUP_NAMES";
private static final String PUBLIC_GROUPS = "PUBLIC_GROUPS";
/**
* Returns a singleton instance of GroupManager.
......@@ -71,8 +69,8 @@ public class GroupManager {
return GroupManagerContainer.instance;
}
Cache<String, Group> groupCache;
Cache<String, Object> groupMetaCache;
private Cache<String, Group> groupCache;
private Cache<String, Object> groupMetaCache;
private GroupProvider provider;
private GroupManager() {
......@@ -99,26 +97,52 @@ public class GroupManager {
// Since the group could be created by the provider, add it possible again
groupCache.put(group.getName(), group);
groupMetaCache.clear();
// Evict only the information related to Groups.
// Do not evict groups with 'user' as keys.
groupMetaCache.remove(GROUP_COUNT_KEY);
groupMetaCache.remove(GROUP_NAMES_KEY);
groupMetaCache.remove(SHARED_GROUPS_KEY);
// Evict cached information for affected users
evictCachedUsersForGroup(group);
}
public void groupDeleting(Group group, Map params) {
// Since the group could be deleted by the provider, remove it possible again
groupCache.remove(group.getName());
groupMetaCache.clear();
// Evict only the information related to Groups.
// Do not evict groups with 'user' as keys.
groupMetaCache.remove(GROUP_COUNT_KEY);
groupMetaCache.remove(GROUP_NAMES_KEY);
groupMetaCache.remove(SHARED_GROUPS_KEY);
// Evict cached information for affected users
evictCachedUsersForGroup(group);
}
public void groupModified(Group group, Map params) {
String type = (String)params.get("type");
// If shared group settings changed, expire the cache.
if (type != null && (type.equals("propertyModified") ||
type.equals("propertyDeleted") || type.equals("propertyAdded")))
if (type != null) {
if (type.equals("propertyModified") ||
type.equals("propertyDeleted") || type.equals("propertyAdded"))
{
if (params.get("propertyKey") != null &&
params.get("propertyKey").equals("sharedRoster.showInRoster"))
Object key = params.get("propertyKey");
if (key instanceof String && (key.equals("sharedRoster.showInRoster") || key.equals("*")))
{
groupMetaCache.clear();
groupMetaCache.remove(GROUP_NAMES_KEY);
groupMetaCache.remove(SHARED_GROUPS_KEY);
}
}
// clean up cache for old group name
if (type.equals("nameModified")) {
String originalName = (String) params.get("originalValue");
if (originalName != null) {
groupMetaCache.remove(originalName);
}
// Evict cached information for affected users
evictCachedUsersForGroup(group);
}
}
// Set object again in cache. This is done so that other cluster nodes
......@@ -127,31 +151,51 @@ public class GroupManager {
}
public void memberAdded(Group group, Map params) {
groupMetaCache.clear();
// Set object again in cache. This is done so that other cluster nodes
// get refreshed with latest version of the object
groupCache.put(group.getName(), group);
// Remove only the collection of groups the member belongs to.
String member = (String) params.get("member");
if(member != null) {
groupMetaCache.remove(member);
}
}
public void memberRemoved(Group group, Map params) {
groupMetaCache.clear();
// Set object again in cache. This is done so that other cluster nodes
// get refreshed with latest version of the object
groupCache.put(group.getName(), group);
// Remove only the collection of groups the member belongs to.
String member = (String) params.get("member");
if(member != null) {
groupMetaCache.remove(member);
}
}
public void adminAdded(Group group, Map params) {
groupMetaCache.clear();
// Set object again in cache. This is done so that other cluster nodes
// get refreshed with latest version of the object
groupCache.put(group.getName(), group);
// Remove only the collection of groups the member belongs to.
String member = (String) params.get("admin");
if(member != null) {
groupMetaCache.remove(member);
}
}
public void adminRemoved(Group group, Map params) {
groupMetaCache.clear();
// Set object again in cache. This is done so that other cluster nodes
// get refreshed with latest version of the object
groupCache.put(group.getName(), group);
// Remove only the collection of groups the member belongs to.
String member = (String) params.get("admin");
if(member != null) {
groupMetaCache.remove(member);
}
}
});
......@@ -191,29 +235,6 @@ public class GroupManager {
}
};
PropertyEventDispatcher.addListener(propListener);
// Pre-load shared groups. This will provide a faster response
// time to the first client that logs in.
Runnable task = new Runnable() {
public void run() {
Collection<Group> groups = getSharedGroups();
// Load each group into cache.
for (Group group : groups) {
// Load each user in the group into cache.
for (JID jid : group.getMembers()) {
try {
if (XMPPServer.getInstance().isLocal(jid)) {
UserManager.getInstance().getUser(jid.getNode());
}
}
catch (UserNotFoundException unfe) {
// Ignore.
}
}
}
}
};
TaskEngine.getInstance().submit(task);
}
private void initProvider() {
......@@ -362,6 +383,11 @@ public class GroupManager {
/**
* Returns an unmodifiable Collection of all groups in the system.
*
* NOTE: Iterating through the resulting collection has the effect of loading
* every group into memory. This may be an issue for large deployments. You
* may call the size() method on the resulting collection to determine the best
* approach to take before iterating over (and thus instantiating) the groups.
*
* @return an unmodifiable Collection of all groups.
*/
public Collection<Group> getGroups() {
......@@ -381,6 +407,11 @@ public class GroupManager {
/**
* Returns an unmodifiable Collection of all shared groups in the system.
*
* NOTE: Iterating through the resulting collection has the effect of loading all
* shared groups into memory. This may be an issue for large deployments. You
* may call the size() method on the resulting collection to determine the best
* approach to take before iterating over (and thus instantiating) the groups.
*
* @return an unmodifiable Collection of all shared groups.
*/
public Collection<Group> getSharedGroups() {
......@@ -389,7 +420,7 @@ public class GroupManager {
synchronized(SHARED_GROUPS_KEY.intern()) {
groupNames = (Collection<String>)groupMetaCache.get(SHARED_GROUPS_KEY);
if (groupNames == null) {
groupNames = provider.getSharedGroupsNames();
groupNames = provider.getSharedGroupNames();
groupMetaCache.put(SHARED_GROUPS_KEY, groupNames);
}
}
......@@ -397,6 +428,79 @@ public class GroupManager {
return new GroupCollection(groupNames);
}
/**
* Returns an unmodifiable Collection of all shared groups in the system for a given userName.
*
* @return an unmodifiable Collection of all shared groups for the given userName.
*/
public Collection<Group> getSharedGroups(String userName) {
Collection<String> groupNames = (Collection<String>)groupMetaCache.get(userName);
if (groupNames == null) {
synchronized(userName.intern()) {
groupNames = (Collection<String>)groupMetaCache.get(userName);
if (groupNames == null) {
// assume this is a local user
groupNames = provider.getSharedGroupNames(new JID(userName,
XMPPServer.getInstance().getServerInfo().getXMPPDomain(), null));
groupMetaCache.put(userName, groupNames);
}
}
}
return new GroupCollection(groupNames);
}
/**
* Returns an unmodifiable Collection of all shared groups in the system for a given userName.
*
* @return an unmodifiable Collection of all shared groups for the given userName.
*/
public Collection<Group> getVisibleGroups(Group groupToCheck) {
// Get all the public shared groups.
Collection<String> groupNames = (Collection<String>)groupMetaCache.get(PUBLIC_GROUPS);
if (groupNames == null) {
synchronized(PUBLIC_GROUPS.intern()) {
groupNames = (Collection<String>)groupMetaCache.get(PUBLIC_GROUPS);
if (groupNames == null) {
groupNames = provider.getPublicSharedGroupNames();
groupMetaCache.put(PUBLIC_GROUPS, groupNames);
}
}
}
// Now get all visible groups to the given group.
groupNames.addAll(provider.getVisibleGroupNames(groupToCheck.getName()));
return new GroupCollection(groupNames);
}
/**
* Returns an unmodifiable Collection of all public shared groups in the system.
*
* @return an unmodifiable Collection of all shared groups.
*/
public Collection<Group> getPublicSharedGroups() {
Collection<String> groupNames = (Collection<String>)groupMetaCache.get(PUBLIC_GROUPS);
if (groupNames == null) {
synchronized(PUBLIC_GROUPS.intern()) {
groupNames = (Collection<String>)groupMetaCache.get(PUBLIC_GROUPS);
if (groupNames == null) {
groupNames = provider.getPublicSharedGroupNames();
groupMetaCache.put(PUBLIC_GROUPS, groupNames);
}
}
}
return new GroupCollection(groupNames);
}
/**
* Returns an unmodifiable Collection of all groups in the system that
* match given propValue for the specified propName.
*
* @return an unmodifiable Collection of all shared groups.
*/
public Collection<Group> search(String propName, String propValue) {
Collection<String> groupsWithProps = provider.search(propName, propValue);
return new GroupCollection(groupsWithProps);
}
/**
* Returns all groups given a start index and desired number of results. This is
* useful to support pagination in a GUI where you may only want to display a certain
......@@ -528,4 +632,14 @@ public class GroupManager {
public GroupProvider getProvider() {
return provider;
}
private void evictCachedUsersForGroup(Group group) {
// Evict cached information for affected users
for (JID user : group.getAdmins()) {
groupMetaCache.remove(user.getNode());
}
for (JID user : group.getMembers()) {
groupMetaCache.remove(user.getNode());
}
}
}
\ No newline at end of file
......@@ -20,9 +20,10 @@
package org.jivesoftware.openfire.group;
import org.xmpp.packet.JID;
import java.util.Collection;
import java.util.Map;
import org.xmpp.packet.JID;
/**
* Provider interface for groups. Users that wish to integrate with
......@@ -37,6 +38,8 @@ import java.util.Collection;
* &lt;/group&gt;
* &lt;/provider&gt;</pre>
*
* @see AbstractReadOnlyGroupProvider
*
* @author Matt Tucker
*/
public interface GroupProvider {
......@@ -112,12 +115,45 @@ public interface GroupProvider {
*/
Collection<String> getGroupNames();
/**
* Returns true if this GroupProvider allows group sharing. Shared groups
* enable roster sharing.
*
* @return true if the group provider supports group sharing.
*/
boolean isSharingSupported();
/**
* Returns an unmodifiable Collection of all shared groups in the system.
*
* @return unmodifiable Collection of all shared groups in the system.
*/
Collection<String> getSharedGroupsNames();
Collection<String> getSharedGroupNames();
/**
* Returns an unmodifiable Collection of all shared groups in the system for a given user.
*
* @param JID The bare JID for the user (node@domain)
* @return unmodifiable Collection of all shared groups in the system for a given user.
*/
Collection<String> getSharedGroupNames(JID user);
/**
* Returns an unmodifiable Collection of all public shared groups in the system.
*
* @return unmodifiable Collection of all public shared groups in the system.
*/
Collection<String> getPublicSharedGroupNames();
/**
* Returns an unmodifiable Collection of groups that are visible
* to the members of the given group.
*
* @param userGroup The given group
* @return unmodifiable Collection of group names that are visible
* to the given group.
*/
Collection<String> getVisibleGroupNames(String userGroup);
/**
* Returns the Collection of all groups in the system.
......@@ -209,10 +245,45 @@ public interface GroupProvider {
*/
Collection<String> search(String query, int startIndex, int numResults);
/**
* Returns the names of groups that have a property matching the given
* key/value pair. This provides an simple extensible search mechanism
* for providers with differing property sets and storage models.
*
* The semantics of the key/value matching (wildcard support, scoping, etc.)
* are unspecified by the interface and may vary for each implementation.
*
* Before searching or showing a search UI, use the {@link #isSearchSupported} method
* to ensure that searching is supported.
*
* @param key The name of a group property (e.g. "sharedRoster.showInRoster")
* @param value The value to match for the given property
* @return unmodifiable Collection of group names that match the
* given key/value pair.
*/
Collection<String> search(String key, String value);
/**
* Returns true if group searching is supported by the provider.
*
* @return true if searching is supported.
*/
boolean isSearchSupported();
/**
* Loads the group properties (if any) from the backend data store. If
* the properties can be changed, the provider implementation must ensure
* that updates to the resulting {@link Map} are persisted to the
* backend data store. Otherwise if a mutator method is called, the
* implementation should throw an {@link UnsupportedOperationException}.
*
* If there are no corresponding properties for the given group, or if the
* provider does not support group properties, this method should return
* an empty Map rather than null.
*
* @param group The target group
* @return The properties for the given group
*/
Map<String,String> loadProperties(Group group);
}
\ No newline at end of file
......@@ -27,10 +27,13 @@ import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.util.Immutable;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -53,7 +56,9 @@ import org.xmpp.packet.JID;
* <li><tt>jdbcProvider.driver = com.mysql.jdbc.Driver</tt></li>
* <li><tt>jdbcProvider.connectionString = jdbc:mysql://localhost/dbname?user=username&amp;password=secret</tt></li>
* <li><tt>jdbcGroupProvider.groupCountSQL = SELECT count(*) FROM myGroups</tt></li>
* <li><tt>jdbcGroupProvider.groupPropsSQL = SELECT propName, propValue FROM myGroupProps WHERE groupName=?</tt></li>
* <li><tt>jdbcGroupProvider.allGroupsSQL = SELECT groupName FROM myGroups</tt></li>
* <li><tt>jdbcGroupProvider.sharedGroupsSQL = SELECT groupName FROM myGroups WHERE shared='true'</tt></li>
* <li><tt>jdbcGroupProvider.userGroupsSQL = SELECT groupName FORM myGroupUsers WHERE username=?</tt></li>
* <li><tt>jdbcGroupProvider.descriptionSQL = SELECT groupDescription FROM myGroups WHERE groupName=?</tt></li>
* <li><tt>jdbcGroupProvider.loadMembersSQL = SELECT username FORM myGroupUsers WHERE groupName=? AND isAdmin='N'</tt></li>
......@@ -69,7 +74,7 @@ import org.xmpp.packet.JID;
*
* @author David Snopek
*/
public class JDBCGroupProvider implements GroupProvider {
public class JDBCGroupProvider extends AbstractReadOnlyGroupProvider {
private static final Logger Log = LoggerFactory.getLogger(JDBCGroupProvider.class);
......@@ -77,7 +82,9 @@ public class JDBCGroupProvider implements GroupProvider {
private String groupCountSQL;
private String descriptionSQL;
private String groupPropsSQL;
private String allGroupsSQL;
private String sharedGroupsSQL;
private String userGroupsSQL;
private String loadMembersSQL;
private String loadAdminsSQL;
......@@ -93,7 +100,9 @@ public class JDBCGroupProvider implements GroupProvider {
JiveGlobals.migrateProperty("jdbcProvider.driver");
JiveGlobals.migrateProperty("jdbcProvider.connectionString");
JiveGlobals.migrateProperty("jdbcGroupProvider.groupCountSQL");
JiveGlobals.migrateProperty("jdbcGroupProvider.groupPropsSQL");
JiveGlobals.migrateProperty("jdbcGroupProvider.allGroupsSQL");
JiveGlobals.migrateProperty("jdbcGroupProvider.sharedGroupsSQL");
JiveGlobals.migrateProperty("jdbcGroupProvider.userGroupsSQL");
JiveGlobals.migrateProperty("jdbcGroupProvider.descriptionSQL");
JiveGlobals.migrateProperty("jdbcGroupProvider.loadMembersSQL");
......@@ -116,33 +125,15 @@ public class JDBCGroupProvider implements GroupProvider {
// Load SQL statements
groupCountSQL = JiveGlobals.getProperty("jdbcGroupProvider.groupCountSQL");
groupPropsSQL = JiveGlobals.getProperty("jdbcGroupProvider.groupPropsSQL");
allGroupsSQL = JiveGlobals.getProperty("jdbcGroupProvider.allGroupsSQL");
sharedGroupsSQL = JiveGlobals.getProperty("jdbcGroupProvider.sharedGroupsSQL");
userGroupsSQL = JiveGlobals.getProperty("jdbcGroupProvider.userGroupsSQL");
descriptionSQL = JiveGlobals.getProperty("jdbcGroupProvider.descriptionSQL");
loadMembersSQL = JiveGlobals.getProperty("jdbcGroupProvider.loadMembersSQL");
loadAdminsSQL = JiveGlobals.getProperty("jdbcGroupProvider.loadAdminsSQL");
}
/**
* Always throws an UnsupportedOperationException because JDBC groups are read-only.
*
* @param name the name of the group to create.
* @throws UnsupportedOperationException when called.
*/
public Group createGroup(String name) throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
/**
* Always throws an UnsupportedOperationException because JDBC groups are read-only.
*
* @param name the name of the group to delete
* @throws UnsupportedOperationException when called.
*/
public void deleteGroup(String name) throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
private Connection getConnection() throws SQLException {
if (useConnectionProvider)
return DbConnectionManager.getConnection();
......@@ -220,29 +211,6 @@ public class JDBCGroupProvider implements GroupProvider {
return members;
}
/**
* Always throws an UnsupportedOperationException because JDBC groups are read-only.
*
* @param oldName the current name of the group.
* @param newName the desired new name of the group.
* @throws UnsupportedOperationException when called.
*/
public void setName(String oldName, String newName) throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
/**
* Always throws an UnsupportedOperationException because JDBC groups are read-only.
*
* @param name the group name.
* @param description the group description.
* @throws UnsupportedOperationException when called.
*/
public void setDescription(String name, String description)
throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
public int getGroupCount() {
int count = 0;
Connection con = null;
......@@ -265,9 +233,29 @@ public class JDBCGroupProvider implements GroupProvider {
return count;
}
public Collection<String> getSharedGroupsNames() {
// Get the list of shared groups from the database
return Group.getSharedGroupsNames();
public Collection<String> getSharedGroupNames() {
if (sharedGroupsSQL == null) {
return Collections.emptyList();
}
List<String> groupNames = new ArrayList<String>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sharedGroupsSQL);
rs = pstmt.executeQuery();
while (rs.next()) {
groupNames.add(rs.getString(1));
}
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return groupNames;
}
public Collection<String> getGroupNames() {
......@@ -340,65 +328,28 @@ public class JDBCGroupProvider implements GroupProvider {
return groupNames;
}
/**
* Always throws an UnsupportedOperationException because JDBC groups are read-only.
*
* @param groupName name of a group.
* @param user the JID of the user to add
* @param administrator true if is an administrator.
* @throws UnsupportedOperationException when called.
*/
public void addMember(String groupName, JID user, boolean administrator)
throws UnsupportedOperationException
{
throw new UnsupportedOperationException();
}
/**
* Always throws an UnsupportedOperationException because JDBC groups are read-only.
*
* @param groupName the naame of a group.
* @param user the JID of the user with new privileges
* @param administrator true if is an administrator.
* @throws UnsupportedOperationException when called.
*/
public void updateMember(String groupName, JID user, boolean administrator)
throws UnsupportedOperationException
{
throw new UnsupportedOperationException();
public Map<String, String> loadProperties(Group group) {
Map<String,String> properties = new HashMap<String,String>();
if (groupPropsSQL != null) {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(groupPropsSQL);
pstmt.setString(1, group.getName());
rs = pstmt.executeQuery();
while (rs.next()) {
properties.put(rs.getString(1), rs.getString(2));
}
/**
* Always throws an UnsupportedOperationException because JDBC groups are read-only.
*
* @param groupName the name of a group.
* @param user the JID of the user to delete.
* @throws UnsupportedOperationException when called.
*/
public void deleteMember(String groupName, JID user)
throws UnsupportedOperationException
{
throw new UnsupportedOperationException();
}
/**
* Always returns true because JDBC groups are read-only.
*
* @return true because all JDBC functions are read-only.
*/
public boolean isReadOnly() {
return true;
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
public Collection<String> search(String query) {
return Collections.emptyList();
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
public Collection<String> search(String query, int startIndex, int numResults) {
return Collections.emptyList();
}
public boolean isSearchSupported() {
return false;
return new Immutable.Map<String,String>(properties);
}
}
......@@ -38,9 +38,9 @@ import javax.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.group.AbstractReadOnlyGroupProvider;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.group.GroupNotFoundException;
import org.jivesoftware.openfire.group.GroupProvider;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveConstants;
......@@ -54,7 +54,7 @@ import org.xmpp.packet.JID;
*
* @author Matt Tucker, Greg Ferguson and Cameron Moore
*/
public class LdapGroupProvider implements GroupProvider {
public class LdapGroupProvider extends AbstractReadOnlyGroupProvider {
private static final Logger Log = LoggerFactory.getLogger(LdapGroupProvider.class);
......@@ -76,26 +76,6 @@ public class LdapGroupProvider implements GroupProvider {
standardAttributes[2] = manager.getGroupMemberField();
}
/**
* Always throws an UnsupportedOperationException because LDAP groups are read-only.
*
* @param name the name of the group to create.
* @throws UnsupportedOperationException when called.
*/
public Group createGroup(String name) throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
/**
* Always throws an UnsupportedOperationException because LDAP groups are read-only.
*
* @param name the name of the group to delete
* @throws UnsupportedOperationException when called.
*/
public void deleteGroup(String name) throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
public Group getGroup(String groupName) throws GroupNotFoundException {
LdapContext ctx = null;
try {
......@@ -123,30 +103,6 @@ public class LdapGroupProvider implements GroupProvider {
}
}
/**
* Always throws an UnsupportedOperationException because LDAP groups are read-only.
*
* @param oldName the current name of the group.
* @param newName the desired new name of the group.
* @throws UnsupportedOperationException when called.
*/
public void setName(String oldName, String newName) throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
/**
* Always throws an UnsupportedOperationException because LDAP groups are read-only.
*
* @param name the group name.
* @param description the group description.
* @throws UnsupportedOperationException when called.
*/
public void setDescription(String name, String description)
throws UnsupportedOperationException
{
throw new UnsupportedOperationException();
}
public int getGroupCount() {
if (manager.isDebugEnabled()) {
Log.debug("LdapGroupProvider: Trying to get the number of groups in the system.");
......@@ -163,11 +119,6 @@ public class LdapGroupProvider implements GroupProvider {
return this.groupCount;
}
public Collection<String> getSharedGroupsNames() {
// Get the list of shared groups from the database
return Group.getSharedGroupsNames();
}
public Collection<String> getGroupNames() {
return getGroupNames(-1, -1);
}
......@@ -207,13 +158,17 @@ public class LdapGroupProvider implements GroupProvider {
if (username == null || "".equals(username)) {
return Collections.emptyList();
}
return search(manager.getGroupMemberField(), username);
}
public Collection<String> search(String key, String value) {
StringBuilder filter = new StringBuilder();
filter.append("(&");
filter.append(MessageFormat.format(manager.getGroupSearchFilter(), "*"));
filter.append("(").append(manager.getGroupMemberField()).append("=").append(username);
filter.append("(").append(key).append("=").append(value);
filter.append("))");
if (Log.isDebugEnabled()) {
Log.debug("Trying to find group names for user: " + user + " using query: " + filter.toString());
Log.debug("Trying to find group names using query: " + filter.toString());
}
// Perform the LDAP query
return manager.retrieveList(
......@@ -225,53 +180,6 @@ public class LdapGroupProvider implements GroupProvider {
);
}
/**
* Always throws an UnsupportedOperationException because LDAP groups are read-only.
*
* @param groupName name of a group.
* @param user the JID of the user to add
* @param administrator true if is an administrator.
* @throws UnsupportedOperationException when called.
*/
public void addMember(String groupName, JID user, boolean administrator)
throws UnsupportedOperationException
{
throw new UnsupportedOperationException();
}
/**
* Always throws an UnsupportedOperationException because LDAP groups are read-only.
*
* @param groupName the naame of a group.
* @param user the JID of the user with new privileges
* @param administrator true if is an administrator.
* @throws UnsupportedOperationException when called.
*/
public void updateMember(String groupName, JID user, boolean administrator)
throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
/**
* Always throws an UnsupportedOperationException because LDAP groups are read-only.
*
* @param groupName the name of a group.
* @param user the JID of the user to delete.
* @throws UnsupportedOperationException when called.
*/
public void deleteMember(String groupName, JID user) throws UnsupportedOperationException {
throw new UnsupportedOperationException();
}
/**
* Returns true because LDAP groups are read-only.
*
* @return true because all LDAP functions are read-only.
*/
public boolean isReadOnly() {
return true;
}
public Collection<String> search(String query) {
return search(query, -1, -1);
}
......
......@@ -326,10 +326,15 @@ public class Roster implements Cacheable, Externalizable {
throws UserAlreadyExistsException, SharedGroupException {
if (groups != null && !groups.isEmpty()) {
// Raise an error if the groups the item belongs to include a shared group
Collection<Group> sharedGroups = GroupManager.getInstance().getSharedGroups();
for (String group : groups) {
for (Group sharedGroup : sharedGroups) {
if (group.equals(sharedGroup.getProperties().get("sharedRoster.displayName"))) {
for (String groupDisplayName : groups) {
Collection<Group> groupsWithProp = GroupManager
.getInstance()
.search("sharedRoster.displayName", groupDisplayName);
Iterator<Group> itr = groupsWithProp.iterator();
while(itr.hasNext()) {
Group group = itr.next();
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if(showInRoster != null && !showInRoster.equals("nobody")) {
throw new SharedGroupException("Cannot add an item to a shared group");
}
}
......
......@@ -359,41 +359,24 @@ public class RosterItem implements Cacheable, Externalizable {
}
// Remove shared groups from the param
Collection<Group> existingGroups = GroupManager.getInstance().getSharedGroups();
for (Iterator<String> it=groups.iterator(); it.hasNext();) {
String groupName = it.next();
try {
// Optimistic approach for performance reasons. Assume first that the shared
// group name is the same as the display name for the shared roster
// Check if exists a shared group with this name
Group group = GroupManager.getInstance().getGroup(groupName);
// Get the display name of the group
String displayName = group.getProperties().get("sharedRoster.displayName");
if (displayName != null && displayName.equals(groupName)) {
// Remove the shared group from the list (since it exists)
try {
if (RosterManager.isSharedGroup(group)) {
it.remove();
}
catch (IllegalStateException e) {
// Do nothing
}
}
}
catch (GroupNotFoundException e) {
} catch (GroupNotFoundException e) {
// Check now if there is a group whose display name matches the requested group
for (Group group : existingGroups) {
// Get the display name of the group
String displayName = group.getProperties().get("sharedRoster.displayName");
if (displayName != null && displayName.equals(groupName)) {
// Remove the shared group from the list (since it exists)
try {
Collection<Group> groupsWithProp = GroupManager
.getInstance()
.search("sharedRoster.displayName", groupName);
Iterator<Group> itr = groupsWithProp.iterator();
while(itr.hasNext()) {
Group group = itr.next();
if (RosterManager.isSharedGroup(group)) {
it.remove();
}
catch (IllegalStateException ise) {
// Do nothing
}
}
}
}
}
......
......@@ -187,7 +187,7 @@ public class RosterManager extends BasicModule implements GroupEventListener, Us
*/
public Collection<Group> getSharedGroups(String username) {
Collection<Group> answer = new HashSet<Group>();
Collection<Group> groups = GroupManager.getInstance().getSharedGroups();
Collection<Group> groups = GroupManager.getInstance().getSharedGroups(username);
for (Group group : groups) {
String showInRoster = group.getProperties().get("sharedRoster.showInRoster");
if ("onlyGroup".equals(showInRoster)) {
......@@ -219,16 +219,7 @@ public class RosterManager extends BasicModule implements GroupEventListener, Us
* @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;
return GroupManager.getInstance().getPublicSharedGroups();
}
/**
......@@ -753,26 +744,7 @@ public class RosterManager extends BasicModule implements GroupEventListener, Us
}
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;
return GroupManager.getInstance().getVisibleGroups(groupToCheck);
}
/**
......
package org.jivesoftware.util;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Collections;
import java.util.Set;
public class Immutable {
/**
* Wraps an existing {@link Collection} to provide read-only access to its contents.
*/
public static class Collection<V> extends java.util.AbstractCollection<V> {
private java.util.Collection<V> delegate;
public Collection(java.util.Collection<V> delegate) {
this.delegate = delegate;
}
@Override
public Iterator<V> iterator() {
return new Iterator<V>(delegate.iterator());
}
@Override
public int size() {
return delegate.size();
}
}
/**
* Read-only {@link Iterator} prevents removal of objects
*/
public static class Iterator<V> implements java.util.Iterator<V> {
private java.util.Iterator<V> delegate;
public Iterator(java.util.Iterator<V> delegate) {
this.delegate = delegate;
}
public boolean hasNext() {
return delegate.hasNext();
}
public V next() {
return delegate.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
}
/**
* Wraps a {@link Map} to provide read-only access to its elements.
*/
public static class Map<K,V> extends AbstractMap<K,V> {
private java.util.Map<K,V> backingMap;
/**
* Use this constructor to provide a pre-populated map that will be
* made read-only via this wrapper class
* @param backingMap
*/
public Map(java.util.Map<K,V> backingMap) {
this.backingMap = backingMap;
}
/**
* Default constructor (empty map)
*/
public Map() { }
@Override
public Set<Map.Entry<K,V>> entrySet() {
if (backingMap == null) {
return Collections.emptySet();
} else {
return new AbstractSet<Map.Entry<K,V>>() {
public Iterator<Map.Entry<K,V>> iterator() {
return new Iterator<Entry<K, V>>(backingMap.entrySet().iterator());
}
public int size() {
return backingMap.size();
}
};
}
}
}
}
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