Commit 39e33fe1 authored by Guus der Kinderen's avatar Guus der Kinderen

OF-1336: Introduce UserPropertyProvider

This commit adds a new feature to Openfire, that allows one to store user properties in an external system. This is quite similar to concepts like UserProvider, AuthProvider, GroupProvider, and so on.

The default behavior, moved from the User class to DefaultUserPropertyProvider, continues to use the ofUserProp database table. Provider implementations matching those of other types of providers have been added.
parent 9df410ae
......@@ -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,13 +31,22 @@ 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
*/
......@@ -63,17 +54,6 @@ 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 (?, ?, ?)";
// The name of the name visible property
private static final String NAME_VISIBLE_PROPERTY = "name.visible";
// The name of the email visible property
......@@ -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 );
}
/**
......@@ -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,11 +436,12 @@ public class User implements Cacheable, Externalizable, Result {
String answer;
String keyString = key;
try {
synchronized (getName() + keyString.intern()) {
if (properties.containsKey(keyString)) {
String originalValue = properties.get(keyString);
answer = properties.put(keyString, value);
updateProperty(keyString, value);
UserManager.getUserPropertyProvider().updateProperty(username, keyString, value);
// Configure event.
eventParams.put("type", "propertyModified");
eventParams.put("propertyKey", key);
......@@ -484,16 +449,21 @@ public class User implements Cacheable, Externalizable, Result {
}
else {
answer = properties.put(keyString, value);
insertProperty(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 );
}
return null;
}
@Override
......@@ -536,7 +506,8 @@ public class User implements Cacheable, Externalizable, Result {
throw new IllegalStateException();
}
String key = current.getKey();
deleteProperty(key);
try {
UserManager.getUserPropertyProvider().deleteProperty(username, key);
iter.remove();
// Fire event.
Map<String,Object> params = new HashMap<>();
......@@ -544,85 +515,11 @@ public class User implements Cacheable, Externalizable, Result {
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);
};
}
}
......
......@@ -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;
......@@ -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();
}
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