/**********************************************************************************
* $URL: https://source.sakaiproject.org/svn/providers/trunk/jldap/src/java/edu/amc/sakai/user/SearchExecutingLdapConnectionLivenessValidator.java $
* $Id: SearchExecutingLdapConnectionLivenessValidator.java 105079 2012-02-24 23:08:11Z ottenhoff@longsight.com $
***********************************************************************************
*
* Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation
*
* Licensed under the Educational Community 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.opensource.org/licenses/ECL-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 edu.amc.sakai.user;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.MessageFormat;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.component.api.ServerConfigurationService;
import com.novell.ldap.LDAPConnection;
import com.novell.ldap.LDAPEntry;
import com.novell.ldap.LDAPException;
import com.novell.ldap.LDAPSearchConstraints;
import com.novell.ldap.LDAPSearchResults;
/**
* Tests {@link LDAPConnection} liveness by executing a search filter
* and verifying that the result contains at least one entry. This
* strategy is appropriate when {@link LDAPConnection#isConnectionAlive()}
* is not reliable, e.g. when running against a non-OpenLDAP service
* provider.
*
* <p>This particular implementation makes an effort to generate
* filters which will be easily traceable in LDAP logs. Generally,
* filters take the form
* <code>(|(objectclass=*)({attr}={unique-string}))</code>. The
* search itself is limited by {@link LDAPConnection#SCOPE_BASE},
* where "BASE" is specified by <code>baseDn</code>. Often,
* <code>baseDn</code> corresponds to the DN of the "system user"
* as whom {@link JLDAPDirectoryProvider} binds when running
* in "autoBind" mode.</p>
*
* @author dmccallum@unicon.net
*/
public class SearchExecutingLdapConnectionLivenessValidator
implements LdapConnectionLivenessValidator {
public static final String DEFAULT_SEARCH_ATTRIBUTE_NAME = "uid";
public static final LDAPSearchConstraints DEFAULT_LDAP_CONSTRAINTS =
new LDAPSearchConstraints();
{
DEFAULT_LDAP_CONSTRAINTS.setDereference(LDAPSearchConstraints.DEREF_ALWAYS);
DEFAULT_LDAP_CONSTRAINTS.setTimeLimit(5000);
DEFAULT_LDAP_CONSTRAINTS.setReferralFollowing(false);
DEFAULT_LDAP_CONSTRAINTS.setBatchSize(0);
}
public static final String DEFAULT_HOST_NAME = "UNKNOWN_HOST";
/** Class-specific logger */
private static Log log = LogFactory.getLog(SearchExecutingLdapConnectionLivenessValidator.class);
/**
* An ID for this instance
*/
private String searchStamp =
System.currentTimeMillis() + "-" + (int)(1e6*Math.random());
/**
* The attribute against which the search unique ID will
* be tested.
*/
private String searchAttributeName = DEFAULT_SEARCH_ATTRIBUTE_NAME;
/**
* The searchFilter string with a placeholder for a unique
* key for a particular execution. Treated as a {@link MessageFormat}
* pattern. Depends on {@link #searchAttributeName}
* having already been initialized to some meaningful value.
*/
private String searchFilter = newUnformattedSearchFilter();
/**
* Cached copy of constraints common to all searches
*/
private LDAPSearchConstraints searchConstraints = DEFAULT_LDAP_CONSTRAINTS;
/**
* The DN to be searched by {@link #searchFilter}. Searches
* are restricted to search this DN only.
*/
private String baseDn;
private String hostName = DEFAULT_HOST_NAME;
private ServerConfigurationService serverConfigService;
/**
* Invoke prior to testing any connections. Caches a host
* name to include in search terms.
*/
public void init() {
if (hostName.equals(DEFAULT_HOST_NAME)) {
hostName = null; // defaults again at bottom
if (hostName == null) {
try {
hostName = getLocalhostName();
} catch (UnknownHostException e) {
if (log.isDebugEnabled()) {
log.debug("Unable to get local host name", e);
}
}
}
if (hostName == null && serverConfigService != null) {
hostName = serverConfigService.getServerName();
}
if (hostName == null) {
hostName = DEFAULT_HOST_NAME;
}
}
if ( log.isDebugEnabled() ) {
log.debug("init(): cached hostName [" + hostName + "]");
}
}
/**
* Returns localhost's name as reported by
* {@link InetAddress#getLocalHost()#toString()}. Factored
* into a method to enable override during testing.
*
* @return
* @throws UnknownHostException
*/
protected String getLocalhostName() throws UnknownHostException {
return InetAddress.getLocalHost().toString();
}
public boolean isConnectionAlive(LDAPConnection connectionToTest) {
if ( log.isDebugEnabled() ) {
log.debug("isConnectionAlive(): testing connection liveness via search");
}
String formattedSearchFilter = formatSearchFilter();
try
{
if ( log.isDebugEnabled() ) {
log.debug("isConnectionAlive(): executing connection liveness search [base dn = " +
baseDn + "][filter = " + formattedSearchFilter + "][return attrib = " +
searchAttributeName + "]");
}
LDAPSearchResults searchResults =
connectionToTest.search(baseDn,
LDAPConnection.SCOPE_BASE,
formattedSearchFilter,
new String[] {searchAttributeName},
false,
searchConstraints);
if ( log.isDebugEnabled() ) {
log.debug("isConnectionAlive(): executed search [base dn = " +
baseDn + "][filter = " + formattedSearchFilter + "][return attrib = " +
searchAttributeName + "]");
}
if ( searchResults.hasMore() ) {
if ( log.isDebugEnabled() ) {
log.debug("isConnectionAlive(): search contained results [base dn = " +
baseDn + "][filter = " + formattedSearchFilter + "][return attrib = " +
searchAttributeName + "]");
}
LDAPEntry entry = searchResults.next();
boolean isNonNullEntry = entry != null;
if ( log.isDebugEnabled() ) {
log.debug("isConnectionAlive(): search [base dn = " +
baseDn + "][filter = " + formattedSearchFilter + "][return attrib = " +
searchAttributeName + "] had results, returning [" +
isNonNullEntry + "]");
}
return isNonNullEntry;
} else {
if ( log.isDebugEnabled() ) {
log.debug("isConnectionAlive(): search had no results [base dn = " +
baseDn + "][filter = " + formattedSearchFilter + "][return attrib = " +
searchAttributeName + "], returning false");
}
return false;
}
}
catch (LDAPException le)
{
if ( log.isDebugEnabled() ) {
log.debug("isConnectionAlive(): liveness test failed [base dn = " +
baseDn + "][filter = " + formattedSearchFilter + "][return attrib = " +
searchAttributeName + "]", le);
}
return false;
}
}
protected String newUnformattedSearchFilter() {
return new StringBuilder("(|(objectclass=*)(")
.append(searchAttributeName)
.append("=validateProbe-")
.append(searchStamp)
.append("-{0}))").toString();
}
/**
* Generates an executable search filter string by injecting
* the result of {@link #generateUniqueSearchFilterTerm()} into
* the current <code>searchFilter</code>. This term is usually
* treated as a portion of the value to match against
* <code>searchAttributeName</code>
*
* @return an LDAP search filter
*/
protected String formatSearchFilter() {
Object uniqueSearchFilterTerm = generateUniqueSearchFilterTerm();
return MessageFormat.format(searchFilter, uniqueSearchFilterTerm);
}
/**
* Generates a portion of the search filter which will (likely) uniquely
* identify an execution of that filter. By default concatenates a
* semi-unique token ({@link #generateUniqueToken()} and the local host
* name, separated by a dash ("-").
*
* @see #setHostName(String)
* @see #getHostName()
* @see #generateUniqueToken()
* @see #setServerConfigService(ServerConfigurationService)
* @return
*/
protected Object generateUniqueSearchFilterTerm() {
return generateUniqueToken() + "-" + hostName;
}
/**
* Just returns the current system time in millis. This
* is factored into a method so it can be overriden
* in testing (otherwise unique tokens are quite difficult
* to verify).
*
* @see System#currentTimeMillis()
* @return
*/
protected String generateUniqueToken() {
return Long.toString(System.currentTimeMillis());
}
public String getSearchStamp() {
return searchStamp;
}
public void setSearchStamp(String searchStamp) {
this.searchStamp = searchStamp;
}
public String getSearchAttributeName() {
return searchAttributeName;
}
public void setSearchAttributeName(String searchAttributeName) {
if ( searchAttributeName == null ) {
this.searchAttributeName = DEFAULT_SEARCH_ATTRIBUTE_NAME;
} else {
this.searchAttributeName = searchAttributeName;
}
this.searchFilter = newUnformattedSearchFilter();
}
public String getBaseDn() {
return baseDn;
}
public void setBaseDn(String baseDn) {
this.baseDn = baseDn;
}
public LDAPSearchConstraints getSearchConstraints() {
return searchConstraints;
}
public void setSearchConstraints(LDAPSearchConstraints searchConstraints) {
this.searchConstraints = searchConstraints;
}
public String getSearchFilter() {
return searchFilter;
}
public void setSearchFilter(String searchFilter) {
this.searchFilter = searchFilter;
}
public ServerConfigurationService getServerConfigService() {
return serverConfigService;
}
public void setServerConfigService(
ServerConfigurationService serverConfigService) {
this.serverConfigService = serverConfigService;
}
public String getHostName() {
return hostName;
}
/**
* Assign the host name to be appended to (semi) invocation-unique
* search terms. Falls back to {@link #DEFAULT_HOST_NAME} if
* argument is <code>null</code>. If this setter is not ivoked,
* {@link #init()} will control the default value.
*
* @see #init()
* @param hostName
*/
public void setHostName(String hostName) {
if ( hostName == null ) {
this.hostName = DEFAULT_HOST_NAME;
} else {
this.hostName = hostName;
}
}
}