Commit 4818ae96 authored by Dave Cridland's avatar Dave Cridland Committed by GitHub

Merge pull request #812 from guusdk/OF-1336_UserPropertyProvider

OF-1336: Introduce UserPropertyProvider
parents 9df410ae 47798ec7
......@@ -16,24 +16,6 @@
package org.jivesoftware.openfire.user;
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.AbstractMap;
import java.util.AbstractSet;
import java.util.Date;
import java.util.HashMap;
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.auth.AuthFactory;
import org.jivesoftware.openfire.auth.ConnectionException;
......@@ -49,30 +31,28 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.resultsetmanagement.Result;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.*;
/**
* Encapsulates information about a user. New users are created using
* {@link UserManager#createUser(String, String, String, String)}. All user
* properties are loaded on demand and are read from the <tt>ofUserProp</tt>
* database table. The currently-installed {@link UserProvider} is used for
* setting all other user data and some operations may not be supported
* depending on the capabilities of the {@link UserProvider}.
* Encapsulates information about a user.
*
* New users are created using {@link UserManager#createUser(String, String, String, String)}.
*
* The currently-installed {@link UserProvider} is used for setting all other user data and some operations may not be
* supported depending on the capabilities of the {@link UserProvider}.
*
* All user properties are loaded on demand from the currently-installed
* {@link org.jivesoftware.openfire.user.property.UserPropertyProvider}.
*
* @author Matt Tucker
*/
public class User implements Cacheable, Externalizable, Result {
private static final Logger Log = LoggerFactory.getLogger(User.class);
private static final String LOAD_PROPERTIES =
"SELECT name, propValue FROM ofUserProp WHERE username=?";
private static final String LOAD_PROPERTY =
"SELECT propValue FROM ofUserProp WHERE username=? AND name=?";
private static final String DELETE_PROPERTY =
"DELETE FROM ofUserProp WHERE username=? AND name=?";
private static final String UPDATE_PROPERTY =
"UPDATE ofUserProp SET propValue=? WHERE name=? AND username=?";
private static final String INSERT_PROPERTY =
"INSERT INTO ofUserProp (username, name, propValue) VALUES (?, ?, ?)";
private static final Logger Log = LoggerFactory.getLogger(User.class);
// The name of the name visible property
private static final String NAME_VISIBLE_PROPERTY = "name.visible";
......@@ -98,29 +78,10 @@ public class User implements Cacheable, Externalizable, Result {
* @param username the username of the user to get a specific property value.
* @param propertyName the name of the property to return its value.
* @return the value of the specified property for the given username.
* @throws UserNotFoundException Depending on the installed user provider (some will return null instead).
*/
public static String getPropertyValue(String username, String propertyName) {
String propertyValue = null;
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_PROPERTY);
pstmt.setString(1, username);
pstmt.setString(2, propertyName);
rs = pstmt.executeQuery();
while (rs.next()) {
propertyValue = rs.getString(1);
}
}
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return propertyValue;
public static String getPropertyValue(String username, String propertyName) throws UserNotFoundException {
return UserManager.getUserPropertyProvider().loadProperty( username, propertyName );
}
/**
......@@ -195,35 +156,35 @@ public class User implements Cacheable, Externalizable, Result {
Log.error(e.getMessage(), e);
}
}
public String getStoredKey() {
return storedKey;
}
public void setStoredKey(String storedKey) {
this.storedKey = storedKey;
}
public String getServerKey() {
return serverKey;
}
public void setServerKey(String serverKey) {
this.serverKey = serverKey;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
public int getIterations() {
return iterations;
}
public void setIterations(int iterations) {
this.iterations = iterations;
}
......@@ -398,8 +359,11 @@ public class User implements Cacheable, Externalizable, Result {
public Map<String,String> getProperties() {
synchronized (this) {
if (properties == null) {
properties = new ConcurrentHashMap<>();
loadProperties();
try {
properties = UserManager.getUserPropertyProvider().loadProperties( username );
} catch (UserNotFoundException e ) {
Log.error( "Unable to retrieve properties for user " + username, e );
}
}
}
// Return a wrapper that will intercept add and remove commands.
......@@ -472,28 +436,34 @@ public class User implements Cacheable, Externalizable, Result {
String answer;
String keyString = key;
synchronized (getName() + keyString.intern()) {
if (properties.containsKey(keyString)) {
String originalValue = properties.get(keyString);
answer = properties.put(keyString, value);
updateProperty(keyString, value);
// Configure event.
eventParams.put("type", "propertyModified");
eventParams.put("propertyKey", key);
eventParams.put("originalValue", originalValue);
}
else {
answer = properties.put(keyString, value);
insertProperty(keyString, value);
// Configure event.
eventParams.put("type", "propertyAdded");
eventParams.put("propertyKey", key);
try {
synchronized ((getName() + keyString).intern()) {
if (properties.containsKey(keyString)) {
String originalValue = properties.get(keyString);
answer = properties.put(keyString, value);
UserManager.getUserPropertyProvider().updateProperty(username, keyString, value);
// Configure event.
eventParams.put("type", "propertyModified");
eventParams.put("propertyKey", key);
eventParams.put("originalValue", originalValue);
}
else {
answer = properties.put(keyString, value);
UserManager.getUserPropertyProvider().insertProperty(username, keyString, value);
// Configure event.
eventParams.put("type", "propertyAdded");
eventParams.put("propertyKey", key);
}
}
// Fire event.
UserEventDispatcher.dispatchEvent(User.this,
UserEventDispatcher.EventType.user_modified, eventParams);
return answer;
} catch (UserNotFoundException e ) {
Log.error( "Unable to put property for user " + username, e );
}
// Fire event.
UserEventDispatcher.dispatchEvent(User.this,
UserEventDispatcher.EventType.user_modified, eventParams);
return answer;
return null;
}
@Override
......@@ -536,96 +506,23 @@ public class User implements Cacheable, Externalizable, Result {
throw new IllegalStateException();
}
String key = current.getKey();
deleteProperty(key);
iter.remove();
// Fire event.
Map<String,Object> params = new HashMap<>();
params.put("type", "propertyDeleted");
params.put("propertyKey", key);
UserEventDispatcher.dispatchEvent(User.this,
UserEventDispatcher.EventType.user_modified, params);
try {
UserManager.getUserPropertyProvider().deleteProperty(username, key);
iter.remove();
// Fire event.
Map<String,Object> params = new HashMap<>();
params.put("type", "propertyDeleted");
params.put("propertyKey", key);
UserEventDispatcher.dispatchEvent(User.this,
UserEventDispatcher.EventType.user_modified, params);
} catch (UserNotFoundException e ) {
Log.error( "Unable to delete property for user " + username, e );
}
}
};
}
}
private void loadProperties() {
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement(LOAD_PROPERTIES);
pstmt.setString(1, username);
rs = pstmt.executeQuery();
while (rs.next()) {
properties.put(rs.getString(1), rs.getString(2));
}
}
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, username);
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, username);
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, username);
pstmt.setString(2, propName);
pstmt.executeUpdate();
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(pstmt, con);
}
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
ExternalizableUtil.getInstance().writeSafeUTF(out, username);
......
......@@ -29,6 +29,8 @@ import org.dom4j.Element;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.event.UserEventDispatcher;
import org.jivesoftware.openfire.event.UserEventListener;
import org.jivesoftware.openfire.user.property.DefaultUserPropertyProvider;
import org.jivesoftware.openfire.user.property.UserPropertyProvider;
import org.jivesoftware.util.ClassUtils;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.PropertyEventDispatcher;
......@@ -50,7 +52,7 @@ import org.xmpp.packet.JID;
*/
public class UserManager implements IQResultListener {
private static final Logger Log = LoggerFactory.getLogger(UserManager.class);
private static final Logger Log = LoggerFactory.getLogger(UserManager.class);
// Wrap this guy up so we can mock out the UserManager class.
private static class UserManagerContainer {
......@@ -69,6 +71,21 @@ public class UserManager implements IQResultListener {
return UserManagerContainer.instance.provider;
}
/**
* Returns the currently-installed UserPropertyProvider.
*
* <b>Warning:</b> in virtually all cases the user property provider should not be used directly. Instead, use the
* Map returned by {@link User#getProperties() ) to create, read, update or delete user properties. Failure to do so
* is likely to result in inconsistent data behavior and race conditions. Direct access to the user property
* provider is only provided for special-case logic.
*
* @return the current UserPropertyProvider.
* @see User#getProperties
*/
public static UserPropertyProvider getUserPropertyProvider() {
return UserManagerContainer.instance.propertyProvider;
}
/**
* Returns a singleton UserManager instance.
*
......@@ -83,14 +100,16 @@ public class UserManager implements IQResultListener {
/** Cache if a local or remote user exists. */
private Cache<String, Boolean> remoteUsersCache;
private UserProvider provider;
private UserPropertyProvider propertyProvider;
private UserManager() {
// Initialize caches.
userCache = CacheFactory.createCache("User");
remoteUsersCache = CacheFactory.createCache("Remote Users Existence");
// Load a user provider.
// Load a user & property provider.
initProvider();
initPropertyProvider();
// Detect when a new auth provider class is set
PropertyEventListener propListener = new PropertyEventListener() {
......@@ -99,6 +118,9 @@ public class UserManager implements IQResultListener {
if ("provider.user.className".equals(property)) {
initProvider();
}
if ("provider.userproperty.className".equals(property)) {
initPropertyProvider();
}
}
@Override
......@@ -106,6 +128,9 @@ public class UserManager implements IQResultListener {
if ("provider.user.className".equals(property)) {
initProvider();
}
if ("provider.userproperty.className".equals(property)) {
initPropertyProvider();
}
}
@Override
......@@ -492,4 +517,23 @@ public class UserManager implements IQResultListener {
}
}
}
private void initPropertyProvider() {
// Convert XML based provider setup to Database based
JiveGlobals.migrateProperty("provider.userproperty.className");
String className = JiveGlobals.getProperty("provider.userproperty.className",
"org.jivesoftware.openfire.user.property.DefaultUserPropertyProvider");
// Check if we need to reset the provider class
if (propertyProvider == null || !className.equals(propertyProvider.getClass().getName())) {
try {
Class c = ClassUtils.forName(className);
propertyProvider = (UserPropertyProvider) c.newInstance();
}
catch (Exception e) {
Log.error("Error loading user property provider: " + className, e);
propertyProvider = new DefaultUserPropertyProvider();
}
}
}
}
\ No newline at end of file
/*
* Copyright 2017 IgniteRealtime.org
*
* 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.user.property;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.user.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Default implementation of the UserPropertyProvider interface, which reads and writes data from the
* <tt>ofUserProp</tt> database table.
*
* This implementation will not explicitly verify if a user exists, when operating on its properties. The methods of
* this implementation will <em>not</em> throw {@link org.jivesoftware.openfire.user.UserNotFoundException}.
*
* <b>Warning:</b> in virtually all cases a user property provider should not be used directly. Instead, use the
* Map returned by {@link User#getProperties() ) to create, read, update or delete user properties. Failure to do so
* is likely to result in inconsistent data behavior and race conditions. Direct access to the user property
* provider is only provided for special-case logic.
*
* @author Guus der Kinderen, guus@goodbytes.nl
* @see User#getProperties
*/
public class DefaultUserPropertyProvider implements UserPropertyProvider
{
private static final Logger Log = LoggerFactory.getLogger( DefaultUserPropertyProvider.class );
private static final String LOAD_PROPERTIES = "SELECT name, propValue FROM ofUserProp WHERE username=?";
private static final String LOAD_PROPERTY = "SELECT propValue FROM ofUserProp WHERE username=? AND name=?";
private static final String DELETE_PROPERTY = "DELETE FROM ofUserProp WHERE username=? AND name=?";
private static final String UPDATE_PROPERTY = "UPDATE ofUserProp SET propValue=? WHERE name=? AND username=?";
private static final String INSERT_PROPERTY = "INSERT INTO ofUserProp (username, name, propValue) VALUES (?, ?, ?)";
@Override
public Map<String, String> loadProperties( String username )
{
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
final Map<String, String> properties = new ConcurrentHashMap<>();
try
{
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement( LOAD_PROPERTIES );
pstmt.setString( 1, username );
rs = pstmt.executeQuery();
while ( rs.next() )
{
properties.put( rs.getString( 1 ), rs.getString( 2 ) );
}
}
catch ( SQLException sqle )
{
Log.error( sqle.getMessage(), sqle );
}
finally
{
DbConnectionManager.closeConnection( rs, pstmt, con );
}
return properties;
}
@Override
public String loadProperty( String username, String propertyName )
{
String propertyValue = null;
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try
{
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement( LOAD_PROPERTY );
pstmt.setString( 1, username );
pstmt.setString( 2, propertyName );
rs = pstmt.executeQuery();
while ( rs.next() )
{
propertyValue = rs.getString( 1 );
}
}
catch ( SQLException sqle )
{
Log.error( sqle.getMessage(), sqle );
}
finally
{
DbConnectionManager.closeConnection( rs, pstmt, con );
}
return propertyValue;
}
@Override
public void insertProperty( String username, String propName, String propValue )
{
Connection con = null;
PreparedStatement pstmt = null;
try
{
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement( INSERT_PROPERTY );
pstmt.setString( 1, username );
pstmt.setString( 2, propName );
pstmt.setString( 3, propValue );
pstmt.executeUpdate();
}
catch ( SQLException e )
{
Log.error( e.getMessage(), e );
}
finally
{
DbConnectionManager.closeConnection( pstmt, con );
}
}
@Override
public void updateProperty( String username, 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, username );
pstmt.executeUpdate();
}
catch ( SQLException e )
{
Log.error( e.getMessage(), e );
}
finally
{
DbConnectionManager.closeConnection( pstmt, con );
}
}
@Override
public void deleteProperty( String username, String propName )
{
Connection con = null;
PreparedStatement pstmt = null;
try
{
con = DbConnectionManager.getConnection();
pstmt = con.prepareStatement( DELETE_PROPERTY );
pstmt.setString( 1, username );
pstmt.setString( 2, propName );
pstmt.executeUpdate();
}
catch ( SQLException e )
{
Log.error( e.getMessage(), e );
}
finally
{
DbConnectionManager.closeConnection( pstmt, con );
}
}
@Override
public boolean isReadOnly()
{
return false;
}
}
/*
* Copyright 2017 IgniteRealtime.org
*
* 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.user.property;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Delegate UserPropertyProvider operations among up to three configurable provider implementation classes.
*
* This implementation will not explicitly verify if a user exists, when operating on its properties. The methods of
* this implementation will <em>not</em> throw {@link org.jivesoftware.openfire.user.UserNotFoundException}.
*
* This class related to, but is distinct from {@link MappedUserPropertyProvider}. The Hybrid variant of the provider
* iterates over providers, operating on the first applicable instance. The Mapped variant, however, maps each user to
* exactly one provider.
*
* To use this provider, use the following system property definition:
*
* <ul>
* <li><tt>provider.userproperty.className = org.jivesoftware.openfire.user.HybridUserPropertyProvider</tt></li>
* </ul>
*
* Next, configure up to three providers, by setting these properties:
* <ol>
* <li><tt>hybridUserPropertyProvider.primaryProvider.className = fully.qualified.ClassUserPropertyProvider</tt></li>
* <li><tt>hybridUserPropertyProvider.secondaryProvider.className = fully.qualified.ClassUserPropertyProvider</tt></li>
* <li><tt>hybridUserPropertyProvider.tertiaryProvider.className = fully.qualified.ClassUserPropertyProvider</tt></li>
* </ol>
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class HybridUserPropertyProvider implements UserPropertyProvider
{
private static final Logger Log = LoggerFactory.getLogger( HybridUserPropertyProvider.class );
private final List<UserPropertyProvider> providers = new ArrayList<>();
public HybridUserPropertyProvider()
{
// Migrate user provider properties
JiveGlobals.migrateProperty( "hybridUserPropertyProvider.primaryProvider.className" );
JiveGlobals.migrateProperty( "hybridUserPropertyProvider.secondaryProvider.className" );
JiveGlobals.migrateProperty( "hybridUserPropertyProvider.tertiaryProvider.className" );
// Load primary, secondary, and tertiary user providers.
final UserPropertyProvider primary = MappedUserPropertyProvider.instantiate( "hybridUserPropertyProvider.primaryProvider.className" );
if ( primary != null )
{
providers.add( primary );
}
final UserPropertyProvider secondary = MappedUserPropertyProvider.instantiate( "hybridUserPropertyProvider.secondaryProvider.className" );
if ( secondary != null )
{
providers.add( secondary );
}
final UserPropertyProvider tertiary = MappedUserPropertyProvider.instantiate( "hybridUserPropertyProvider.tertiaryProvider.className" );
if ( tertiary != null )
{
providers.add( tertiary );
}
// Verify that there's at least one provider available.
if ( providers.isEmpty() )
{
Log.error( "At least one UserPropertyProvider must be specified via openfire.xml or the system properties!" );
}
}
/**
* Returns the properties from the first provider that returns a non-empty collection.
*
* When none of the providers provide properties an empty collection is returned.
*
* @param username The identifier of the user (cannot be null or empty).
* @return A collection, possibly empty, never null.
*/
@Override
public Map<String, String> loadProperties( String username )
{
for ( final UserPropertyProvider provider : providers )
{
try
{
final Map<String, String> properties = provider.loadProperties( username );
if ( !properties.isEmpty() )
{
return properties;
}
}
catch ( UserNotFoundException e )
{
// User not in this provider. Try other providers;
}
}
return Collections.emptyMap();
}
/**
* Returns a property from the first provider that returns a non-null value.
*
* This method will return null when the desired property was not defined in any provider.
*
* @param username The identifier of the user (cannot be null or empty).
* @param propName The property name (cannot be null or empty).
* @return The property value (possibly null).
*/
@Override
public String loadProperty( String username, String propName )
{
for ( final UserPropertyProvider provider : providers )
{
try
{
final String property = provider.loadProperty( username, propName );
if ( property != null )
{
return property;
}
}
catch ( UserNotFoundException e )
{
// User not in this provider. Try other providers;
}
}
return null;
}
/**
* Adds a new property, updating a previous property value if one already exists.
*
* Note that the implementation of this method is equal to that of {@link #updateProperty(String, String, String)}.
*
* First, tries to find a provider that has the property for the provided user. If that provider is read-only, an
* UnsupportedOperationException is thrown. If the provider is not read-only, the existing property value will be
* updated.
*
* When the property is not defined in any provider, it will be added in the first non-read-only provider.
*
* When all providers are read-only, an UnsupportedOperationException is thrown.
*
* @param username The identifier of the user (cannot be null or empty).
* @param propName The property name (cannot be null or empty).
* @param propValue The property value (cannot be null).
*/
@Override
public void insertProperty( String username, String propName, String propValue ) throws UnsupportedOperationException
{
updateProperty( username, propName, propValue );
}
/**
* Updates a property (or adds a new property when the property does not exist).
*
* Note that the implementation of this method is equal to that of {@link #insertProperty(String, String, String)}.
*
* First, tries to find a provider that has the property for the provided user. If that provider is read-only, an
* UnsupportedOperationException is thrown. If the provider is not read-only, the existing property value will be
* updated.
*
* When the property is not defined in any provider, it will be added in the first non-read-only provider.
*
* When all providers are read-only, an UnsupportedOperationException is thrown.
*
* @param username The identifier of the user (cannot be null or empty).
* @param propName The property name (cannot be null or empty).
* @param propValue The property value (cannot be null).
*/
@Override
public void updateProperty( String username, String propName, String propValue ) throws UnsupportedOperationException
{
for ( final UserPropertyProvider provider : providers )
{
try
{
if ( provider.loadProperty( username, propName ) != null )
{
provider.updateProperty( username, propName, propValue );
return;
}
}
catch ( UserNotFoundException e )
{
// User not in this provider. Try other providers;
}
}
for ( final UserPropertyProvider provider : providers )
{
try
{
if ( !provider.isReadOnly() )
{
provider.insertProperty( username, propName, propValue );
return;
}
}
catch ( UserNotFoundException e )
{
// User not in this provider. Try other providers;
}
}
throw new UnsupportedOperationException();
}
/**
* Removes a property from all non-read-only providers.
*
* @param username The identifier of the user (cannot be null or empty).
* @param propName The property name (cannot be null or empty).
*/
@Override
public void deleteProperty( String username, String propName ) throws UnsupportedOperationException
{
// all providers are read-only
if ( isReadOnly() )
{
throw new UnsupportedOperationException();
}
for ( final UserPropertyProvider provider : providers )
{
if ( provider.isReadOnly() )
{
continue;
}
try
{
provider.deleteProperty( username, propName );
}
catch ( UserNotFoundException e )
{
// User not in this provider. Try other providers;
}
}
}
/**
* Returns whether <em>all</em> backing providers are read-only. When read-only, properties can not be created,
* deleted, or modified. If at least one provider is not read-only, this method returns false.
*
* @return true when all backing providers are read-only, otherwise false.
*/
@Override
public boolean isReadOnly()
{
// TODO Make calls concurrent for improved throughput.
for ( final UserPropertyProvider provider : providers )
{
// If at least one provider is not readonly, neither is this proxy.
if ( !provider.isReadOnly() )
{
return false;
}
}
return true;
}
}
/*
* Copyright 2017 IgniteRealtime.org
*
* 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.user.property;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
import java.util.HashMap;
import java.util.Map;
/**
* The JDBC user property provider allows you to use an external database to define the user properties. It is best used
* with the JDBCUserProvider, JDBCAuthProvider &and JDBCGroupProvider to provide integration between your external
* system and Openfire. All data is treated as read-only so any set operations will result in an exception.
*
* This implementation will not explicitly verify if a user exists, when operating on its properties. The methods of
* this implementation will <em>not</em> throw {@link org.jivesoftware.openfire.user.UserNotFoundException}.
*
* To enable this provider, set the following in the system properties:
*
* <ul>
* <li><tt>provider.userproperty.className = org.jivesoftware.openfire.user.property.JDBCUserPropertyProvider</tt></li>
* </ul>
*
* Then you need to set your driver, connection string and SQL statements:
*
* <ul>
* <li><tt>jdbcUserPropertyProvider.driver = com.mysql.jdbc.Driver</tt></li>
* <li><tt>jdbcUserPropertyProvider.connectionString = jdbc:mysql://localhost/dbname?user=username&amp;password=secret</tt></li>
* <li><tt>jdbcUserPropertyProvider.loadPropertySQL = SELECT propName, propValue FROM myUser WHERE user = ? AND propName = ?</tt></li>
* <li><tt>jdbcUserPropertyProvider.loadPropertiesSQL = SELECT propValue FROM myUser WHERE user = ?</tt></li>
* </ul>
*
* In order to use the configured JDBC connection provider do not use a JDBCconnection string, set the following
* property:
*
* <tt>jdbcUserPropertyProvider.useConnectionProvider = true</tt></li>
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class JDBCUserPropertyProvider implements UserPropertyProvider
{
private static final Logger Log = LoggerFactory.getLogger( JDBCUserPropertyProvider.class );
private String loadPropertySQL;
private String loadPropertiesSQL;
private String connectionString;
private boolean useConnectionProvider;
/**
* Constructs a new JDBC user property provider.
*/
public JDBCUserPropertyProvider()
{
// Convert XML based provider setup to Database based
JiveGlobals.migrateProperty( "jdbcUserPropertyProvider.driver" );
JiveGlobals.migrateProperty( "jdbcUserPropertyProvider.connectionString" );
JiveGlobals.migrateProperty( "jdbcUserPropertyProvider.loadPropertySQL" );
JiveGlobals.migrateProperty( "jdbcUserPropertyProvider.loadPropertiesSQL" );
useConnectionProvider = JiveGlobals.getBooleanProperty( "jdbcUserProvider.useConnectionProvider" );
// Load the JDBC driver and connection string.
if ( !useConnectionProvider )
{
String jdbcDriver = JiveGlobals.getProperty( "jdbcUserPropertyProvider.driver" );
try
{
Class.forName( jdbcDriver ).newInstance();
}
catch ( Exception e )
{
Log.error( "Unable to load JDBC driver: " + jdbcDriver, e );
return;
}
connectionString = JiveGlobals.getProperty( "jdbcProvider.connectionString" );
}
// Load database statements for user data.
loadPropertySQL = JiveGlobals.getProperty( "jdbcUserPropertyProvider.loadPropertySQL" );
loadPropertiesSQL = JiveGlobals.getProperty( "jdbcUserPropertyProvider.loadPropertiesSQL" );
}
private Connection getConnection() throws SQLException
{
if ( useConnectionProvider )
{
return DbConnectionManager.getConnection();
}
else
{
return DriverManager.getConnection( connectionString );
}
}
@Override
public Map<String, String> loadProperties( String username ) throws UnsupportedOperationException
{
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try
{
con = getConnection();
pstmt = con.prepareStatement( loadPropertiesSQL );
pstmt.setString( 1, username );
rs = pstmt.executeQuery();
final Map<String, String> result = new HashMap<>();
while ( rs.next() )
{
final String propName = rs.getString( 1 );
final String propValue = rs.getString( 2 );
result.put( propName, propValue );
}
return result;
}
catch ( Exception e )
{
throw new UnsupportedOperationException( e );
}
finally
{
DbConnectionManager.closeConnection( rs, pstmt, con );
}
}
@Override
public String loadProperty( String username, String propName )
{
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try
{
con = getConnection();
pstmt = con.prepareStatement( loadPropertySQL );
pstmt.setString( 1, username );
pstmt.setString( 2, propName );
rs = pstmt.executeQuery();
final Map<String, String> result = new HashMap<>();
if ( rs.next() )
{
return rs.getString( 1 );
}
return null;
}
catch ( Exception e )
{
throw new UnsupportedOperationException( e );
}
finally
{
DbConnectionManager.closeConnection( rs, pstmt, con );
}
}
@Override
public void insertProperty( String username, String propName, String propValue ) throws UnsupportedOperationException
{
throw new UnsupportedOperationException();
}
@Override
public void updateProperty( String username, String propName, String propValue ) throws UnsupportedOperationException
{
throw new UnsupportedOperationException();
}
@Override
public void deleteProperty( String username, String propName ) throws UnsupportedOperationException
{
throw new UnsupportedOperationException();
}
@Override
public boolean isReadOnly()
{
return true;
}
}
/*
* Copyright 2017 IgniteRealtime.org
*
* 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.user.property;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.ClassUtils;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
/**
* A {@link UserPropertyProvider} that delegates to a user-specific UserPropertyProvider.
*
* This implementation will explicitly verify if a user exists, when operating on its properties, but only if the
* corresponding mapped provider does so. If that is the case, then the methods of this implementation will throw
* {@link org.jivesoftware.openfire.user.UserNotFoundException}.
*
* This class related to, but is distinct from {@link HybridUserPropertyProvider}. The Hybrid variant of the provider
* iterates over providers, operating on the first applicable instance. This Mapped variant, however, maps each user to
* exactly one provider.
*
* To use this provider, use the following system property definition:
*
* <ul>
* <li><tt>provider.userproperty.className = org.jivesoftware.openfire.user.MappedUserPropertyProvider</tt></li>
* </ul>
*
* To be usable, a {@link UserPropertyProviderMapper} must be configured using the <tt>mappedUserPropertyProvider.mapper.className</tt>
* system property. It is of importance to note that most UserPropertyProviderMapper implementations will require additional
* configuration.
*
* @author Guus der Kinderen, guus@goodbytes.nl
*/
public class MappedUserPropertyProvider implements UserPropertyProvider
{
/**
* Name of the property of which the value is expected to be the classname of the UserPropertyProviderMapper
* instance to be used by instances of this class.
*/
public static final String PROPERTY_MAPPER_CLASSNAME = "mappedUserPropertyProvider.mapper.className";
private static final Logger Log = LoggerFactory.getLogger( MappedUserPropertyProvider.class );
/**
* Used to determine what provider is to be used to operate on a particular user.
*/
protected final UserPropertyProviderMapper mapper;
public MappedUserPropertyProvider()
{
// Migrate properties.
JiveGlobals.migrateProperty( PROPERTY_MAPPER_CLASSNAME );
// Instantiate mapper.
final String mapperClass = JiveGlobals.getProperty( PROPERTY_MAPPER_CLASSNAME );
if ( mapperClass == null )
{
throw new IllegalStateException( "A mapper must be specified via openfire.xml or the system properties." );
}
try
{
final Class c = ClassUtils.forName( mapperClass );
mapper = (UserPropertyProviderMapper) c.newInstance();
}
catch ( Exception e )
{
throw new IllegalStateException( "Unable to create new instance of UserPropertyProviderMapper class: " + mapperClass, e );
}
}
/**
* Instantiates a UserPropertyProvider based on a property value (that is expected to be a class name). When the
* property is not set, this method returns null. When the property is set, but an exception occurs while
* instantiating the class, this method logs the error and returns null.
*
* UserProvider classes are required to have a public, no-argument constructor.
*
* @param propertyName A property name (cannot ben ull).
* @return A user provider (can be null).
*/
public static UserPropertyProvider instantiate( String propertyName )
{
final String className = JiveGlobals.getProperty( propertyName );
if ( className == null )
{
Log.debug( "Property '{}' is undefined. Skipping.", propertyName );
return null;
}
Log.debug( "About to to instantiate an UserPropertyProvider '{}' based on the value of property '{}'.", className, propertyName );
try
{
final Class c = ClassUtils.forName( className );
final UserPropertyProvider provider = (UserPropertyProvider) c.newInstance();
Log.debug( "Instantiated UserPropertyProvider '{}'", className );
return provider;
}
catch ( Exception e )
{
Log.error( "Unable to load UserPropertyProvider '{}'. Users in this provider will be disabled.", className, e );
return null;
}
}
@Override
public Map<String, String> loadProperties( String username ) throws UserNotFoundException
{
return mapper.getUserPropertyProvider( username ).loadProperties( username );
}
@Override
public String loadProperty( String username, String propName ) throws UserNotFoundException
{
return mapper.getUserPropertyProvider( username ).loadProperty( username, propName );
}
@Override
public void insertProperty( String username, String propName, String propValue ) throws UserNotFoundException
{
mapper.getUserPropertyProvider( username ).insertProperty( username, propName, propValue );
}
@Override
public void updateProperty( String username, String propName, String propValue ) throws UserNotFoundException
{
mapper.getUserPropertyProvider( username ).updateProperty( username, propName, propValue );
}
@Override
public void deleteProperty( String username, String propName ) throws UserNotFoundException
{
mapper.getUserPropertyProvider( username ).deleteProperty( username, propName );
}
/**
* Returns whether <em>all</em> backing providers are read-only. When read-only, properties can not be created,
* deleted, or modified. If at least one provider is not read-only, this method returns false.
*
* @return true when all backing providers are read-only, otherwise false.
*/
@Override
public boolean isReadOnly()
{
// TODO Make calls concurrent for improved throughput.
for ( final UserPropertyProvider provider : mapper.getUserPropertyProviders() )
{
// If at least one provider is not readonly, neither is this proxy.
if ( !provider.isReadOnly() )
{
return false;
}
}
return true;
}
}
/*
* Copyright 2017 IgniteRealtime.org
*
* 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.user.property;
import org.jivesoftware.openfire.user.User;
import org.jivesoftware.openfire.user.UserNotFoundException;
import java.util.Map;
/**
* A provider for user properties. User properties are defined Map of String key and values that does not support null.
* value.
*
* Some, but not all, implementations are expected to store user properties in a relation to an existing user object.
* This interface definition does not require implementations to verify that a user object indeed exists, when
* processing data. As a result, methods defined here may, but are not required to throw {@link UserNotFoundException}
* when processing property data for non-existing users. Implementations should clearly document their behavior in
* this respect.
*
* <b>Warning:</b> in virtually all cases a user property provider should not be used directly. Instead, use the
* Map returned by {@link User#getProperties() ) to create, read, update or delete user properties. Failure to do so
* is likely to result in inconsistent data behavior and race conditions. Direct access to the user property
* provider is only provided for special-case logic.
*
* @author Guus der Kinderen, guus@goodbytes.nl
* @see User#getProperties
*/
public interface UserPropertyProvider
{
/**
* Returns true if this UserPropertyProvider is read-only. When read-only, properties can not be created, deleted or
* modified. Invocation of the corresponding methods should result in an {@link UnsupportedOperationException}.
*
* @return true if the user provider is read-only.
*/
boolean isReadOnly();
/**
* Retrieves all properties for a particular user.
*
* @param username The identifier of the user (cannot be null or empty).
* @return A collection, possibly empty, but never null.
*/
Map<String, String> loadProperties( String username ) throws UserNotFoundException;
/**
* Retrieves a property value for a user.
*
* This method will return null when the desired property was not defined for the user (null values are not
* supported).
*
* @param username The identifier of the user (cannot be null or empty).
* @param propName The property name (cannot be null or empty).
* @return The property value (possibly null).
*/
String loadProperty( String username, String propName ) throws UserNotFoundException;
/**
* Adds a property for an user.
*
* The behavior of inserting a duplicate property name is not defined by this interface.
*
* @param username The identifier of the user (cannot be null or empty).
* @param propName The property name (cannot be null or empty).
* @param propValue The property value (cannot be null).
*/
void insertProperty( String username, String propName, String propValue ) throws UserNotFoundException, UnsupportedOperationException;
/**
* Changes a property value for an user.
*
* The behavior of updating a non-existing property is not defined by this interface.
*
* @param username The identifier of the user (cannot be null or empty).
* @param propName The property name (cannot be null or empty).
* @param propValue The property value (cannot be null).
*/
void updateProperty( String username, String propName, String propValue ) throws UserNotFoundException, UnsupportedOperationException;
/**
* Removes one particular property for a particular user.
*
* The behavior of deleting a non-existing property is not defined by this interface.
*
* @param username The identifier of the user (cannot be null or empty).
* @param propName The property name (cannot be null or empty).
*/
void deleteProperty( String username, String propName ) throws UserNotFoundException, UnsupportedOperationException;
}
/*
* Copyright 2017 IgniteRealtime.org
*
* 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.user.property;
import java.util.Set;
/**
* Implementations are used to determine what UserPropertyProvider is to be used for a particular username.
*
* Note that the provided username need not reflect a pre-existing user (the instance might be used to determine in
* which provider a new user is to be created).
*
* Implementation must have a no-argument constructor.
*
* @author Guus der Kinderen, guus@goodbytes.nl
* @see MappedUserPropertyProvider
*/
public interface UserPropertyProviderMapper
{
/**
* Finds a suitable UserPropertyProvider for the user.
*
* Note that the provided username need not reflect a pre-existing user (the instance might be used to determine in
* which provider a new user is to be created).
*
* Implementations are expected to be able to find a UserPropertyProvider for any username. If an implementation
* fails to do so, such a failure is assumed to be the result of a problem in implementation or configuration.
*
* @param username A user identifier (cannot be null or empty).
* @return A UserPropertyProvider for the user (never null).
*/
UserPropertyProvider getUserPropertyProvider( String username );
/**
* Returns all providers that are used by this instance.
*
* The returned collection should have a consistent, predictable iteration order.
*
* @return all providers (never null).
*/
Set<UserPropertyProvider> getUserPropertyProviders();
}
......@@ -44,6 +44,12 @@
Just married Plugin Changelog
</h1>
<p><b>1.2.1</b> -- May 31, 2017</p>
<ul>
<li>Updated to match new API in Openfire 4.2.0</li>
<li>Slight optimization for copying the user properties.</li>
</ul>
<p><b>1.2.0</b> -- October 12, 2015</p>
<ul>
<li>[<a href='http://www.igniterealtime.org/issues/browse/OF-953'>OF-953</a>] - Updated JSP libraries.</li>
......
......@@ -5,8 +5,8 @@
<name>Just married</name>
<description>Allows admins to rename or copy users</description>
<author>Holger Bergunde</author>
<version>1.2.0</version>
<date>10/12/2015</date>
<version>1.2.1</version>
<date>05/31/2017</date>
<minServerVersion>4.0.0</minServerVersion>
<adminconsole>
......
......@@ -8,7 +8,7 @@
</parent>
<groupId>org.igniterealtime.openfire.plugins</groupId>
<artifactId>justmarried</artifactId>
<version>1.2.0</version>
<version>1.2.1</version>
<name>JustMarried Plugin</name>
<description>Allows admins to rename or copy users</description>
......
......@@ -98,9 +98,7 @@ public class JustMarriedPlugin implements Plugin {
}
private static void copyProperties(User currentUser, User newUser) {
for (String key : currentUser.getProperties().keySet()) {
newUser.getProperties().put(key, User.getPropertyValue(currentUser.getUsername(), key));
}
newUser.getProperties().putAll( currentUser.getProperties() );
}
private static void copyRoster(User currentUser, User newUser, String currentUserName) {
......
......@@ -44,6 +44,12 @@
REST API Plugin Changelog
</h1>
<p><b>1.2.6</b> -- May 31, 2017</p>
<ul>
<li>Updated to match new API in Openfire 4.2.0</li>
<li>Slight optimization for copying the user properties.</li>
</ul>
<p><b>1.2.5</b> -- October 14th, 2016</p>
<ul>
<li>Updated to match new API in Openfire 4.1.0</li>
......
......@@ -5,8 +5,8 @@
<name>REST API</name>
<description>Allows administration over a RESTful API.</description>
<author>Roman Soldatow</author>
<version>1.2.5</version>
<date>10/14/2016</date>
<version>1.2.6</version>
<date>05/31/2017</date>
<minServerVersion>4.1.0</minServerVersion>
<adminconsole>
<tab id="tab-server">
......
......@@ -8,7 +8,9 @@
</parent>
<groupId>org.igniterealtime.openfire.plugins</groupId>
<artifactId>restAPI</artifactId>
<version>1.2.6</version>
<name>Rest API Plugin</name>
<description>Allows administration over a RESTful API.</description>
<build>
<sourceDirectory>src/java</sourceDirectory>
......
......@@ -148,9 +148,7 @@ public class JustMarriedController {
* the new user
*/
private static void copyProperties(User currentUser, User newUser) {
for (String key : currentUser.getProperties().keySet()) {
newUser.getProperties().put(key, User.getPropertyValue(currentUser.getUsername(), key));
}
newUser.getProperties().putAll( currentUser.getProperties() );
}
/**
......
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