org.jivesoftware.openfire.user.JDBCUserProvider Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2005-2008 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.openfire.user;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
/**
* The JDBC user provider allows you to use an external database to define the users.
* It is best used with the JDBCAuthProvider & 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.
* For the seach facility, the SQL will be constructed from the SQL in the search
* section below, as well as the usernameField, the nameField and the
* emailField.
* To enable this provider, set the following in the system properties:
*
* - provider.user.className = org.jivesoftware.openfire.user.JDBCUserProvider
*
*
* Then you need to set your driver, connection string and SQL statements:
*
*
* - jdbcProvider.driver = com.mysql.jdbc.Driver
* - jdbcProvider.connectionString = jdbc:mysql://localhost/dbname?user=username&password=secret
* - jdbcUserProvider.loadUserSQL = SELECT name,email FROM myUser WHERE user = ?
* - jdbcUserProvider.userCountSQL = SELECT COUNT(*) FROM myUser
* - jdbcUserProvider.allUsersSQL = SELECT user FROM myUser
* - jdbcUserProvider.searchSQL = SELECT user FROM myUser WHERE
* - jdbcUserProvider.usernameField = myUsernameField
* - jdbcUserProvider.nameField = myNameField
* - jdbcUserProvider.emailField = mymailField
*
*
* In order to use the configured JDBC connection provider do not use a JDBC
* connection string, set the following property
*
*
* - jdbcUserProvider.useConnectionProvider = true
*
*
*
* @author Huw Richards [email protected]
*/
public class JDBCUserProvider implements UserProvider {
private static final Logger Log = LoggerFactory.getLogger(JDBCUserProvider.class);
private String connectionString;
private String loadUserSQL;
private String userCountSQL;
private String allUsersSQL;
private String searchSQL;
private String usernameField;
private String nameField;
private String emailField;
private boolean useConnectionProvider;
private static final boolean IS_READ_ONLY = true;
/**
* Constructs a new JDBC user provider.
*/
public JDBCUserProvider() {
// Convert XML based provider setup to Database based
JiveGlobals.migrateProperty("jdbcProvider.driver");
JiveGlobals.migrateProperty("jdbcProvider.connectionString");
JiveGlobals.migrateProperty("jdbcUserProvider.loadUserSQL");
JiveGlobals.migrateProperty("jdbcUserProvider.userCountSQL");
JiveGlobals.migrateProperty("jdbcUserProvider.allUsersSQL");
JiveGlobals.migrateProperty("jdbcUserProvider.searchSQL");
JiveGlobals.migrateProperty("jdbcUserProvider.usernameField");
JiveGlobals.migrateProperty("jdbcUserProvider.nameField");
JiveGlobals.migrateProperty("jdbcUserProvider.emailField");
useConnectionProvider = JiveGlobals.getBooleanProperty("jdbcUserProvider.useConnectionProvider");
// Load the JDBC driver and connection string.
if (!useConnectionProvider) {
String jdbcDriver = JiveGlobals.getProperty("jdbcProvider.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.
loadUserSQL = JiveGlobals.getProperty("jdbcUserProvider.loadUserSQL");
userCountSQL = JiveGlobals.getProperty("jdbcUserProvider.userCountSQL");
allUsersSQL = JiveGlobals.getProperty("jdbcUserProvider.allUsersSQL");
searchSQL = JiveGlobals.getProperty("jdbcUserProvider.searchSQL");
usernameField = JiveGlobals.getProperty("jdbcUserProvider.usernameField");
nameField = JiveGlobals.getProperty("jdbcUserProvider.nameField");
emailField = JiveGlobals.getProperty("jdbcUserProvider.emailField");
}
@Override
public User loadUser(String username) throws UserNotFoundException {
if(username.contains("@")) {
if (!XMPPServer.getInstance().isLocal(new JID(username))) {
throw new UserNotFoundException("Cannot load user of remote server: " + username);
}
username = username.substring(0,username.lastIndexOf("@"));
}
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
pstmt = con.prepareStatement(loadUserSQL);
pstmt.setString(1, username);
rs = pstmt.executeQuery();
if (!rs.next()) {
throw new UserNotFoundException();
}
String name = rs.getString(1);
String email = rs.getString(2);
return new User(username, name, email, new Date(), new Date());
}
catch (Exception e) {
throw new UserNotFoundException(e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
}
@Override
public User createUser(String username, String password, String name, String email)
throws UserAlreadyExistsException {
// Reject the operation since the provider is read-only
throw new UnsupportedOperationException();
}
@Override
public void deleteUser(String username) {
// Reject the operation since the provider is read-only
throw new UnsupportedOperationException();
}
@Override
public int getUserCount() {
int count = 0;
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
pstmt = con.prepareStatement(userCountSQL);
rs = pstmt.executeQuery();
if (rs.next()) {
count = rs.getInt(1);
}
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return count;
}
@Override
public Collection getUsers() {
Collection usernames = getUsernames(0, Integer.MAX_VALUE);
return new UserCollection(usernames.toArray(new String[usernames.size()]));
}
@Override
public Collection getUsernames() {
return getUsernames(0, Integer.MAX_VALUE);
}
private Collection getUsernames(int startIndex, int numResults) {
List usernames = new ArrayList<>(500);
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
if ((startIndex==0) && (numResults==Integer.MAX_VALUE))
{
pstmt = con.prepareStatement(allUsersSQL);
// Set the fetch size. This will prevent some JDBC drivers from trying
// to load the entire result set into memory.
DbConnectionManager.setFetchSize(pstmt, 500);
rs = pstmt.executeQuery();
while (rs.next()) {
usernames.add(rs.getString(1));
}
}
else {
pstmt = DbConnectionManager.createScrollablePreparedStatement(con, allUsersSQL);
DbConnectionManager.limitRowsAndFetchSize(pstmt, startIndex, numResults);
rs = pstmt.executeQuery();
DbConnectionManager.scrollResultSet(rs, startIndex);
int count = 0;
while (rs.next() && count < numResults) {
usernames.add(rs.getString(1));
count++;
}
}
if (Log.isDebugEnabled()) {
Log.debug("Results: " + usernames.size());
LogResults(usernames);
}
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return usernames;
}
@Override
public Collection getUsers(int startIndex, int numResults) {
Collection usernames = getUsernames(startIndex, numResults);
return new UserCollection(usernames.toArray(new String[usernames.size()]));
}
@Override
public void setName(String username, String name) throws UserNotFoundException {
// Reject the operation since the provider is read-only
throw new UnsupportedOperationException();
}
@Override
public void setEmail(String username, String email) throws UserNotFoundException {
// Reject the operation since the provider is read-only
throw new UnsupportedOperationException();
}
@Override
public void setCreationDate(String username, Date creationDate) throws UserNotFoundException {
// Reject the operation since the provider is read-only
throw new UnsupportedOperationException();
}
@Override
public void setModificationDate(String username, Date modificationDate) throws UserNotFoundException {
// Reject the operation since the provider is read-only
throw new UnsupportedOperationException();
}
@Override
public Set getSearchFields() throws UnsupportedOperationException {
if (searchSQL == null) {
throw new UnsupportedOperationException();
}
return new LinkedHashSet<>(Arrays.asList("Username", "Name", "Email"));
}
@Override
public Collection findUsers(Set fields, String query) throws UnsupportedOperationException {
return findUsers(fields, query, 0, Integer.MAX_VALUE);
}
@Override
public Collection findUsers(Set fields, String query, int startIndex,
int numResults) throws UnsupportedOperationException
{
if (searchSQL == null) {
throw new UnsupportedOperationException();
}
if (fields.isEmpty()) {
return Collections.emptyList();
}
if (!getSearchFields().containsAll(fields)) {
throw new IllegalArgumentException("Search fields " + fields + " are not valid.");
}
if (query == null || "".equals(query)) {
return Collections.emptyList();
}
// SQL LIKE queries don't map directly into a keyword/wildcard search like we want.
// Therefore, we do a best approximiation by replacing '*' with '%' and then
// surrounding the whole query with two '%'. This will return more data than desired,
// but is better than returning less data than desired.
query = "%" + query.replace('*', '%') + "%";
if (query.endsWith("%%")) {
query = query.substring(0, query.length() - 1);
}
List usernames = new ArrayList<>(50);
Connection con = null;
PreparedStatement pstmt = null;
int queries=0;
ResultSet rs = null;
try {
StringBuilder sql = new StringBuilder(90);
sql.append(searchSQL);
boolean first = true;
if (fields.contains("Username")) {
sql.append(' ');
sql.append(usernameField);
sql.append(" LIKE ?");
queries++;
first = false;
}
if (fields.contains("Name")) {
if (!first) {
sql.append(" AND");
}
sql.append(' ');
sql.append(nameField);
sql.append(" LIKE ?");
queries++;
first = false;
}
if (fields.contains("Email")) {
if (!first) {
sql.append(" AND");
}
sql.append(' ');
sql.append(emailField);
sql.append(" LIKE ?");
queries++;
}
con = getConnection();
if ((startIndex==0) && (numResults==Integer.MAX_VALUE))
{
pstmt = con.prepareStatement(sql.toString());
for (int i=1; i<=queries; i++)
{
pstmt.setString(i, query);
}
rs = pstmt.executeQuery();
while (rs.next()) {
usernames.add(rs.getString(1));
}
} else {
pstmt = DbConnectionManager.createScrollablePreparedStatement(con, sql.toString());
DbConnectionManager.limitRowsAndFetchSize(pstmt, startIndex, numResults);
for (int i=1; i<=queries; i++)
{
pstmt.setString(i, query);
}
rs = pstmt.executeQuery();
// Scroll to the start index.
DbConnectionManager.scrollResultSet(rs, startIndex);
int count = 0;
while (rs.next() && count < numResults) {
usernames.add(rs.getString(1));
count++;
}
}
if (Log.isDebugEnabled())
{
Log.debug("Results: " + usernames.size());
LogResults(usernames);
}
}
catch (SQLException e) {
Log.error(e.getMessage(), e);
}
finally {
DbConnectionManager.closeConnection(rs, pstmt, con);
}
return new UserCollection(usernames.toArray(new String[usernames.size()]));
}
@Override
public boolean isReadOnly() {
return IS_READ_ONLY;
}
@Override
public boolean isNameRequired() {
return false;
}
@Override
public boolean isEmailRequired() {
return false;
}
/**
* Make sure that Log.isDebugEnabled()==true before calling this method.
* Twenty elements will be logged in every log line, so for 81-100 elements
* five log lines will be generated
* @param listElements a list of Strings which will be logged
*/
private void LogResults(List listElements) {
String callingMethod = Thread.currentThread().getStackTrace()[3].getMethodName();
StringBuilder sb = new StringBuilder(256);
int count = 0;
for (String element : listElements)
{
if (count > 20)
{
Log.debug(callingMethod + " results: " + sb.toString());
sb.delete(0, sb.length());
count = 0;
}
sb.append(element).append(',');
count++;
}
sb.append('.');
Log.debug(callingMethod + " results: " + sb.toString());
}
private Connection getConnection() throws SQLException {
if (useConnectionProvider) {
return DbConnectionManager.getConnection();
} else
{
return DriverManager.getConnection(connectionString);
}
}
}