/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE
* or https://OpenDS.dev.java.net/OpenDS.LICENSE.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
* add the following below this CDDL HEADER, with the fields enclosed
* by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2008-2010 Sun Microsystems, Inc.
* Portions copyright 2011-2012 ForgeRock AS
*/
package org.opends.admin.ads;
import static org.opends.messages.QuickSetupMessages
.INFO_ERROR_READING_CONFIG_LDAP_CERTIFICATE_SERVER;
import static org.opends.messages.QuickSetupMessages
.INFO_NOT_GLOBAL_ADMINISTRATOR_PROVIDED;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapName;
import org.opends.admin.ads.ADSContext.ServerProperty;
import org.opends.admin.ads.util.ApplicationTrustManager;
import org.opends.admin.ads.util.ConnectionUtils;
import org.opends.admin.ads.util.PreferredConnection;
import org.opends.admin.ads.util.ServerLoader;
import org.opends.messages.Message;
import org.opends.quicksetup.util.Utils;
/**
* This class allows to read the configuration of the different servers that are
* registered in a given ADS server. It provides a read only view of the
* configuration of the servers and of the replication topologies that might be
* configured between them.
*/
public class TopologyCache
{
private final ADSContext adsContext;
private final ApplicationTrustManager trustManager;
private final int timeout;
private final String bindDN;
private final String bindPwd;
private final Set<ServerDescriptor> servers =
new HashSet<ServerDescriptor>();
private final Set<SuffixDescriptor> suffixes =
new HashSet<SuffixDescriptor>();
private final LinkedHashSet<PreferredConnection> preferredConnections =
new LinkedHashSet<PreferredConnection>();
private final TopologyCacheFilter filter = new TopologyCacheFilter();
private final boolean isMultiThreaded = true;
private final static int MULTITHREAD_TIMEOUT = 90 * 1000;
private static final Logger LOG =
Logger.getLogger(TopologyCache.class.getName());
/**
* Constructor of the TopologyCache.
*
* @param adsContext the adsContext to the ADS registry.
* @param trustManager the ApplicationTrustManager that must be used to trust
* certificates when we create connections to the registered servers to read
* their configuration.
* @param timeout the timeout to establish the connection in milliseconds.
* Use {@code 0} to express no timeout.
*/
public TopologyCache(ADSContext adsContext,
ApplicationTrustManager trustManager,
int timeout)
{
this.adsContext = adsContext;
this.trustManager = trustManager;
this.timeout = timeout;
bindDN = ConnectionUtils.getBindDN(adsContext.getDirContext());
bindPwd = ConnectionUtils.getBindPassword(adsContext.getDirContext());
}
/**
* Reads the configuration of the registered servers.
*
* @throws TopologyCacheException if there is an issue reading the
* configuration of the registered servers.
*/
public void reloadTopology() throws TopologyCacheException
{
suffixes.clear();
servers.clear();
try
{
Set<Map<ServerProperty, Object>> adsServers =
adsContext.readServerRegistry();
Set<ServerLoader> threadSet = new HashSet<ServerLoader>();
for (Map<ServerProperty, Object> serverProperties : adsServers)
{
ServerLoader t = getServerLoader(serverProperties);
if (isMultiThreaded)
{
t.start();
threadSet.add(t);
}
else
{
t.run();
}
}
if (isMultiThreaded)
{
joinThreadSet(threadSet);
}
/*
* Try to consolidate things (even if the data is not complete).
*/
HashMap<LdapName, Set<SuffixDescriptor>> hmSuffixes =
new HashMap<LdapName, Set<SuffixDescriptor>>();
for (ServerLoader loader : threadSet)
{
ServerDescriptor descriptor = loader.getServerDescriptor();
for (ReplicaDescriptor replica : descriptor.getReplicas())
{
LOG.log(Level.INFO, "Handling replica with dn: "
+ replica.getSuffix().getDN());
boolean suffixFound = false;
LdapName dn = new LdapName(replica.getSuffix().getDN());
Set<SuffixDescriptor> sufs = hmSuffixes.get(dn);
if (sufs != null)
{
Iterator<SuffixDescriptor> it = sufs.iterator();
while (it.hasNext() && !suffixFound)
{
SuffixDescriptor suffix = it.next();
Iterator<String> it2 = suffix.getReplicationServers().iterator();
while (it2.hasNext() && !suffixFound)
{
if (replica.getReplicationServers().contains(it2.next()))
{
suffixFound = true;
Set<ReplicaDescriptor> replicas = suffix.getReplicas();
replicas.add(replica);
suffix.setReplicas(replicas);
replica.setSuffix(suffix);
}
}
}
}
if (!suffixFound)
{
if (sufs == null)
{
sufs = new HashSet<SuffixDescriptor>();
hmSuffixes.put(dn, sufs);
}
sufs.add(replica.getSuffix());
suffixes.add(replica.getSuffix());
}
}
servers.add(descriptor);
}
// Figure out the replication monitoring if it is required.
if (getFilter().searchMonitoringInformation())
{
readReplicationMonitoring();
}
}
catch (ADSContextException ade)
{
throw new TopologyCacheException(ade);
}
catch (Throwable t)
{
throw new TopologyCacheException(TopologyCacheException.Type.BUG, t);
}
}
/**
* Returns the trust manager used by this class.
*
* @return the trust manager used by this class.
*/
public ApplicationTrustManager getTrustManager()
{
return trustManager;
}
/**
* Returns the timeout to establish the connection in milliseconds.
*
* @return the timeout to establish the connection in milliseconds. Returns
* {@code 0} to express no timeout.
*/
public int getConnectTimeout()
{
return timeout;
}
/**
* Reads the replication monitoring.
*
* @throws NamingException if an error occurs reading the replication
* monitoring.
*/
private void readReplicationMonitoring()
{
Set<ReplicaDescriptor> replicasToUpdate = new HashSet<ReplicaDescriptor>();
for (ServerDescriptor server : getServers())
{
for (ReplicaDescriptor replica : server.getReplicas())
{
if (replica.isReplicated())
{
replicasToUpdate.add(replica);
}
}
}
for (ServerDescriptor server : getServers())
{
if (server.isReplicationServer())
{
// If is replication server, then at least we were able to read the
// configuration, so assume that we might be able to read monitoring
// (even if an exception occurred before).
Set<ReplicaDescriptor> candidateReplicas =
new HashSet<ReplicaDescriptor>();
// It contains replication information: analyze it.
String repServer = server.getReplicationServerHostPort();
for (SuffixDescriptor suffix : getSuffixes())
{
Set<String> repServers = suffix.getReplicationServers();
for (String r : repServers)
{
if (r.equalsIgnoreCase(repServer))
{
candidateReplicas.addAll(suffix.getReplicas());
break;
}
}
}
if (!candidateReplicas.isEmpty())
{
Set<ReplicaDescriptor> updatedReplicas =
new HashSet<ReplicaDescriptor>();
try
{
updateReplicas(server, candidateReplicas, updatedReplicas);
}
catch (NamingException ne)
{
server.setLastException(new TopologyCacheException(
TopologyCacheException.Type.GENERIC_READING_SERVER, ne));
}
replicasToUpdate.removeAll(updatedReplicas);
}
}
if (replicasToUpdate.isEmpty())
{
break;
}
}
}
/**
* Sets the list of LDAP URLs and connection type that are preferred to be
* used to connect to the servers. When we have a server to which we can
* connect using a URL on the list we will try to use it.
*
* @param cnx the list of preferred connections.
*/
public void setPreferredConnections(LinkedHashSet<PreferredConnection> cnx)
{
preferredConnections.clear();
preferredConnections.addAll(cnx);
}
/**
* Returns the list of LDAP URLs and connection type that are preferred to be
* used to connect to the servers. If a URL is on this list, when we have a
* server to which we can connect using that URL and the associated connection
* type we will try to use it.
*
* @return the list of preferred connections.
*/
public LinkedHashSet<PreferredConnection> getPreferredConnections()
{
return new LinkedHashSet<PreferredConnection>(preferredConnections);
}
/**
* Returns a Set containing all the servers that are registered in the ADS.
*
* @return a Set containing all the servers that are registered in the ADS.
*/
public Set<ServerDescriptor> getServers()
{
HashSet<ServerDescriptor> copy = new HashSet<ServerDescriptor>();
copy.addAll(servers);
return copy;
}
/**
* Returns a Set containing the suffixes (replication topologies) that could
* be retrieved after the last call to reloadTopology.
*
* @return a Set containing the suffixes (replication topologies) that could
* be retrieved after the last call to reloadTopology.
*/
public Set<SuffixDescriptor> getSuffixes()
{
HashSet<SuffixDescriptor> copy = new HashSet<SuffixDescriptor>();
copy.addAll(suffixes);
return copy;
}
/**
* Returns the filter to be used when retrieving information.
*
* @return the filter to be used when retrieving information.
*/
public TopologyCacheFilter getFilter()
{
return filter;
}
/**
* Method used to wait at most a certain time (MULTITHREAD_TIMEOUT) for the
* different threads to finish.
*
* @param threadSet the list of threads (we assume that they are started) that
* we must wait for.
*/
private void joinThreadSet(Set<ServerLoader> threadSet)
{
Date startDate = new Date();
for (ServerLoader t : threadSet)
{
long timeToJoin = MULTITHREAD_TIMEOUT - System.currentTimeMillis()
+ startDate.getTime();
try
{
if (timeToJoin > 0)
{
t.join(MULTITHREAD_TIMEOUT);
}
}
catch (InterruptedException ie)
{
LOG.log(Level.INFO, ie + " caught and ignored", ie);
}
if (t.isAlive())
{
t.interrupt();
}
}
Date endDate = new Date();
long workingTime = endDate.getTime() - startDate.getTime();
LOG.log(Level.INFO, "Loading ended at " + workingTime + " ms");
}
/**
* Creates a ServerLoader object based on the provided server properties.
*
* @param serverProperties the server properties to be used to generate the
* ServerLoader.
* @return a ServerLoader object based on the provided server properties.
*/
private ServerLoader getServerLoader(
Map<ServerProperty, Object> serverProperties)
{
return new ServerLoader(serverProperties, bindDN, bindPwd,
trustManager == null ? null : trustManager.createCopy(),
timeout,
getPreferredConnections(), getFilter());
}
/**
* Returns the adsContext used by this TopologyCache.
*
* @return the adsContext used by this TopologyCache.
*/
public ADSContext getAdsContext()
{
return adsContext;
}
/**
* Returns a set of error messages encountered in the TopologyCache.
*
* @return a set of error messages encountered in the TopologyCache.
*/
public LinkedHashSet<Message> getErrorMessages()
{
Set<TopologyCacheException> exceptions =
new HashSet<TopologyCacheException>();
Set<ServerDescriptor> theServers = getServers();
LinkedHashSet<Message> exceptionMsgs = new LinkedHashSet<Message>();
for (ServerDescriptor server : theServers)
{
TopologyCacheException e = server.getLastException();
if (e != null)
{
exceptions.add(e);
}
}
/*
* Check the exceptions and see if we throw them or not.
*/
for (TopologyCacheException e : exceptions)
{
switch (e.getType())
{
case NOT_GLOBAL_ADMINISTRATOR:
exceptionMsgs.add(INFO_NOT_GLOBAL_ADMINISTRATOR_PROVIDED.get());
break;
case GENERIC_CREATING_CONNECTION:
if ((e.getCause() != null)
&& Utils.isCertificateException(e.getCause()))
{
exceptionMsgs.add(
INFO_ERROR_READING_CONFIG_LDAP_CERTIFICATE_SERVER.get(
e.getHostPort(), e.getCause().getMessage()));
}
else
{
exceptionMsgs.add(Utils.getMessage(e));
}
break;
default:
exceptionMsgs.add(Utils.getMessage(e));
}
}
return exceptionMsgs;
}
/**
* Updates the monitoring information of the provided replicas using the
* information located in cn=monitor of a given replication server.
*
* @param replicationServer the replication server.
* @param candidateReplicas the collection of replicas that must be updated.
* @param updatedReplicas the collection of replicas that are actually
* updated. This list is updated by the method.
*/
private void updateReplicas(ServerDescriptor replicationServer,
Collection<ReplicaDescriptor> candidateReplicas,
Collection<ReplicaDescriptor> updatedReplicas)
throws NamingException
{
SearchControls ctls = new SearchControls();
ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
ctls.setReturningAttributes(
new String[]
{
"approx-older-change-not-synchronized-millis", "missing-changes",
"domain-name", "server-id"
});
InitialLdapContext ctx = null;
NamingEnumeration<SearchResult> monitorEntries = null;
try
{
ServerLoader loader =
getServerLoader(replicationServer.getAdsProperties());
ctx = loader.createContext();
monitorEntries = ctx.search(
new LdapName("cn=monitor"), "(missing-changes=*)", ctls);
while (monitorEntries.hasMore())
{
SearchResult sr = monitorEntries.next();
String dn = ConnectionUtils.getFirstValue(sr, "domain-name");
int replicaId = -1;
try
{
replicaId =
new Integer(ConnectionUtils.getFirstValue(sr, "server-id"));
}
catch (Throwable t)
{
LOG.log(Level.WARNING, "Unexpected error reading replica ID: " + t,
t);
}
for (ReplicaDescriptor replica : candidateReplicas)
{
if (Utils.areDnsEqual(dn, replica.getSuffix().getDN())
&& replica.isReplicated()
&& (replica.getReplicationId() == replicaId))
{
// This statistic is optional.
String s = ConnectionUtils.getFirstValue(sr,
"approx-older-change-not-synchronized-millis");
if (s != null)
{
try
{
replica.setAgeOfOldestMissingChange(Long.valueOf(s));
}
catch (Throwable t)
{
LOG.log(Level.WARNING,
"Unexpected error reading age of oldest change: " + t, t);
}
}
s = ConnectionUtils.getFirstValue(sr, "missing-changes");
if (s != null)
{
try
{
replica.setMissingChanges(new Integer(s));
}
catch (Throwable t)
{
LOG.log(Level.WARNING,
"Unexpected error reading missing changes: " + t, t);
}
}
updatedReplicas.add(replica);
}
}
}
}
catch (NameNotFoundException nse)
{
}
finally
{
if (monitorEntries != null)
{
try
{
monitorEntries.close();
}
catch (Throwable t)
{
LOG.log(Level.WARNING,
"Unexpected error closing enumeration on monitor entries" + t, t);
}
}
if (ctx != null)
{
ctx.close();
}
}
}
}