Add a parameter to control LDAP read timeout time

This is to solve b/13730776 (by allowing a user to add a line to
the adaptor-config.properties file to increase the timeout time).
diff --git a/src/com/google/enterprise/adaptor/ad/AdAdaptor.java b/src/com/google/enterprise/adaptor/ad/AdAdaptor.java
index 104b569..b18118f 100644
--- a/src/com/google/enterprise/adaptor/ad/AdAdaptor.java
+++ b/src/com/google/enterprise/adaptor/ad/AdAdaptor.java
@@ -65,6 +65,7 @@
   private Map<String, String> localizedStrings;
   private boolean feedBuiltinGroups;
   private GroupCatalog lastCompleteGroupCatalog = null;
+  private String ldapTimeoutInMillis;
 
   @Override
   public void initConfig(Config config) {
@@ -78,6 +79,7 @@
     config.addKey("ad.localized.AuthenticatedUsers", "Authenticated Users");
     config.addKey("ad.localized.Builtin", "BUILTIN");
     config.addKey("ad.feedBuiltinGroups", "false");
+    config.addKey("ad.ldapReadTimeoutSecs", "90");
   }
 
   @Override
@@ -90,6 +92,8 @@
         config.getValue("ad.defaultPassword"));
     feedBuiltinGroups = Boolean.parseBoolean(
         config.getValue("ad.feedBuiltinGroups"));
+    ldapTimeoutInMillis = parseLdapTimeoutInMillis(
+        config.getValue("ad.ldapReadTimeoutSecs"));
     // register for incremental pushes
     context.setPollingIncrementalLister(this);
     List<Map<String, String>> serverConfigs
@@ -126,7 +130,8 @@
         throw new IllegalStateException("password not specified for host "
             + host);
       }
-      AdServer adServer = newAdServer(method, host, port, principal, passwd);
+      AdServer adServer = newAdServer(method, host, port, principal, passwd,
+          ldapTimeoutInMillis);
       adServer.initialize();
       servers.add(adServer);
       Map<String, String> dup = new TreeMap<String, String>(singleServerConfig);
@@ -142,8 +147,22 @@
    */
   @VisibleForTesting
   AdServer newAdServer(Method method, String host, int port,
-      String principal, String passwd) {
-    return new AdServer(method, host, port, principal, passwd);
+      String principal, String passwd, String ldapTimeoutInMillis) {
+    return new AdServer(method, host, port, principal, passwd,
+        ldapTimeoutInMillis);
+  }
+
+  private static String parseLdapTimeoutInMillis(String timeInSeconds) {
+    if (timeInSeconds.equals("0") || timeInSeconds.trim().equals("")) {
+      timeInSeconds = "90";
+      log.log(Level.CONFIG, "ad.ldapReadTimeoutSecs set to default of 90 sec.");
+    }
+    try {
+      return String.valueOf(1000L * Integer.parseInt(timeInSeconds));
+    } catch (NumberFormatException e) {
+      throw new IllegalArgumentException("invalid ad.ldapReadTimeoutSecs: " +
+          timeInSeconds);
+    }
   }
 
   /** This adaptor does not serve documents. */
diff --git a/src/com/google/enterprise/adaptor/ad/AdServer.java b/src/com/google/enterprise/adaptor/ad/AdServer.java
index 4c9480c..39a0c90 100644
--- a/src/com/google/enterprise/adaptor/ad/AdServer.java
+++ b/src/com/google/enterprise/adaptor/ad/AdServer.java
@@ -62,15 +62,17 @@
   private long highestCommittedUSN;
   private String invocationID;
   private String dnsRoot;
+  private String ldapTimeoutInMillis;
 
   public AdServer(Method connectMethod, String hostName,
-      int port, String principal, String password) {
+      int port, String principal, String password, String ldapTimeoutInMillis) {
     this(hostName, createLdapContext(connectMethod, hostName, port,
-        principal, password));
+        principal, password, ldapTimeoutInMillis));
     this.connectMethod = connectMethod;
     this.port = port;
     this.principal = principal;
     this.password = password;
+    this.ldapTimeoutInMillis = ldapTimeoutInMillis;
   }
 
   @VisibleForTesting
@@ -85,7 +87,8 @@
    * Called (only) by public constructor
    */
   private static LdapContext createLdapContext(Method connectMethod,
-      String hostName, int port, String principal, String password) {
+      String hostName, int port, String principal, String password,
+      String ldapTimeoutInMillis) {
     Hashtable<String, String> env = new Hashtable<String, String>();
     if (null == connectMethod || null == hostName
         || null == principal || null == password) {
@@ -106,8 +109,7 @@
         "com.sun.jndi.ldap.LdapCtxFactory");
     // Connecting to configuration naming context is very slow for crawl users
     // in large multidomain environment, which belong to thousands of groups
-    // TODO(pjo or myk): make this configurable
-    env.put("com.sun.jndi.ldap.read.timeout", "90000");
+    env.put("com.sun.jndi.ldap.read.timeout", ldapTimeoutInMillis);
     env.put(Context.SECURITY_AUTHENTICATION, "simple");
     env.put(Context.SECURITY_PRINCIPAL, principal);
     env.put(Context.SECURITY_CREDENTIALS, password);
@@ -125,7 +127,7 @@
   @VisibleForTesting
   void recreateLdapContext() {
     ldapContext = createLdapContext(connectMethod, hostName, port, principal,
-        password);
+        password, ldapTimeoutInMillis);
   }
 
   /**
@@ -146,6 +148,15 @@
           "Reconnecting to AdServer after detecting issue", ce);
       recreateLdapContext();
       attributes = ldapContext.getAttributes("");
+    } catch (NamingException ne) {
+      if (ne.getMessage() != null
+          && ne.getMessage().contains("read timed out")) {
+        LOGGER.log(Level.WARNING, "Read timeout insufficient", ne);
+        LOGGER.log(Level.WARNING, "Consider increasing the value of "
+            + "``ad.ldapReadTimeoutSeconds'' in the config file.");
+      }
+      // rethrow the exception, whether or not we were able to give advice.
+      throw(ne);
     }
     dn = attributes.get("defaultNamingContext").get(0).toString();
     dsServiceName = attributes.get("dsServiceName").get(0).toString();
diff --git a/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java b/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java
index f5cc466..14b4228 100644
--- a/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java
@@ -756,6 +756,32 @@
     configEntries.put("ad.servers.server2.method", "standard");
     configEntries.put("ad.defaultUser", "defaultUser");
     configEntries.put("ad.defaultPassword", "password");
+    configEntries.put("ad.ldapReadTimeoutSecs", "");
+    configEntries.put("server.port", "5680");
+    configEntries.put("server.dashboardPort", "5681");
+    pushGroupDefinitions(adAdaptor, configEntries, pusher, /*fullPush=*/ true);
+    Map<GroupPrincipal, Collection<Principal>> results = pusher.getGroups();
+    // the above (eventually) calls AdAdaptor.init() with the specified config.
+  }
+
+  @Test
+  public void testFakeAdaptorInitZeroTimeout() throws Exception {
+    AdAdaptor adAdaptor = new FakeAdaptor();
+    AccumulatingDocIdPusher pusher = new AccumulatingDocIdPusher();
+    Map<String, String> configEntries = new HashMap<String, String>();
+    configEntries.put("gsa.hostname", "localhost");
+    configEntries.put("ad.servers", "server1,server2");
+    configEntries.put("ad.servers.server1.host", "localhost");
+    configEntries.put("ad.servers.server1.port", "1234");
+    configEntries.put("ad.servers.server1.user", "user-override");
+    configEntries.put("ad.servers.server1.method", "ssl");
+    configEntries.put("ad.servers.server2.host", "localhost");
+    configEntries.put("ad.servers.server2.port", "1234");
+    configEntries.put("ad.servers.server2.password", "password-override");
+    configEntries.put("ad.servers.server2.method", "standard");
+    configEntries.put("ad.defaultUser", "defaultUser");
+    configEntries.put("ad.defaultPassword", "password");
+    configEntries.put("ad.ldapReadTimeoutSecs", "0");
     configEntries.put("server.port", "5680");
     configEntries.put("server.dashboardPort", "5681");
     pushGroupDefinitions(adAdaptor, configEntries, pusher, /*fullPush=*/ true);
@@ -824,6 +850,27 @@
   }
 
   @Test
+  public void testFakeAdaptorInitBadTimeout() throws Exception {
+    AdAdaptor adAdaptor = new FakeAdaptor();
+    Map<String, String> configEntries = new HashMap<String, String>();
+    configEntries.put("gsa.hostname", "localhost");
+    configEntries.put("ad.servers", "server1");
+    configEntries.put("ad.servers.server1.host", "localhost");
+    configEntries.put("ad.servers.server1.port", "1234");
+    configEntries.put("ad.defaultUser", "defaultUser");
+    configEntries.put("ad.defaultPassword", "password");
+    configEntries.put("ad.ldapReadTimeoutSecs", "bogus");
+    configEntries.put("server.port", "5680");
+    configEntries.put("server.dashboardPort", "5681");
+    try {
+      initializeAdaptorConfig(adAdaptor, configEntries);
+      fail("Did not catch expected exception");
+    } catch (IllegalArgumentException iae) {
+      assertTrue(iae.toString().contains("invalid ad.ldapReadTimeoutSecs"));
+    }
+  }
+
+  @Test
   public void testFakeAdaptorGetDocIds() throws Exception {
     AdAdaptor adAdaptor = new FakeAdaptor();
     AccumulatingDocIdPusher pusher = new AccumulatingDocIdPusher();
@@ -864,7 +911,7 @@
           + "(&(objectClass=user)(objectCategory=person)))";
       @Override
       AdServer newAdServer(Method method, String host, int port,
-          String principal, String passwd) {
+          String principal, String passwd, String ldapTimeoutInMillis) {
         MockLdapContext ldapContext = null;
         try {
           ldapContext = mockLdapContextForMakeDefs(false);
@@ -1150,7 +1197,7 @@
   public class FakeAdaptor extends AdAdaptor {
     @Override
     AdServer newAdServer(Method method, String host, int port,
-        String principal, String passwd) {
+        String principal, String passwd, String ldapTimeoutInMillis) {
       MockLdapContext ldapContext = null;
       try {
         ldapContext = mockLdapContextForMakeDefs(false);
diff --git a/test/com/google/enterprise/adaptor/ad/AdServerTest.java b/test/com/google/enterprise/adaptor/ad/AdServerTest.java
index 0d95a6c..bb6b5ab 100644
--- a/test/com/google/enterprise/adaptor/ad/AdServerTest.java
+++ b/test/com/google/enterprise/adaptor/ad/AdServerTest.java
@@ -40,56 +40,71 @@
   @Test
   public void testNPEOnNullConnectMethod() {
     thrown.expect(NullPointerException.class);
-    AdServer adServer = new AdServer(null, "hostname", 1234, "principal", "pw");
+    AdServer adServer = new AdServer(null, "hostname", 1234, "principal", "pw",
+        "90000");
   }
 
   @Test
   public void testNPEOnNullHostname() {
     thrown.expect(NullPointerException.class);
-    AdServer adServer = new AdServer(Method.SSL, null, 1234, "principal", "pw");
+    AdServer adServer = new AdServer(Method.SSL, null, 1234, "principal", "pw",
+        "90000");
   }
 
   @Test
   public void testIAEOnEmptyHostname() {
     thrown.expect(IllegalArgumentException.class);
-    AdServer adServer = new AdServer(Method.SSL, "", 1234, "principal", "pw");
+    AdServer adServer = new AdServer(Method.SSL, "", 1234, "principal", "pw",
+        "90000");
   }
 
   @Test
   public void testNPEOnNullPrincipal() {
     thrown.expect(NullPointerException.class);
-    AdServer adServer = new AdServer(Method.SSL, "hostname", 1234, null, "pw");
+    AdServer adServer = new AdServer(Method.SSL, "hostname", 1234, null, "pw",
+        "90000");
   }
 
   @Test
   public void testIAEOnEmptyPrincipal() throws Exception {
     thrown.expect(IllegalArgumentException.class);
-    AdServer adServer = new AdServer(Method.SSL, "hostname", 1234, "", "pw");
+    AdServer adServer = new AdServer(Method.SSL, "hostname", 1234, "", "pw",
+        "90000");
   }
 
   @Test
   public void testNPEOnNullPassword() {
     thrown.expect(NullPointerException.class);
-    AdServer adServer = new AdServer(Method.SSL, "host", 1234, "princ", null);
+    AdServer adServer = new AdServer(Method.SSL, "host", 1234, "princ", null,
+        "90000");
   }
 
   @Test
   public void testIAEOnEmptyPassword() {
     thrown.expect(IllegalArgumentException.class);
-    AdServer adServer = new AdServer(Method.SSL, "hostname", 1234, "princ", "");
+    AdServer adServer = new AdServer(Method.SSL, "hostname", 1234, "princ", "",
+        "90000");
+  }
+
+  @Test
+  public void testIAEOnBogusTimeout() {
+    thrown.expect(IllegalArgumentException.class);
+    AdServer adServer = new AdServer(Method.SSL, "", 1234, "principal", "pw",
+        "bogusTimeout");
   }
 
   @Test
   public void testPublicSSLConstructor() {
     thrown.expect(AssertionError.class);
-    AdServer adServer = new AdServer(Method.SSL, "localhost", 389, " ", " ");
+    AdServer adServer = new AdServer(Method.SSL, "localhost", 389, " ", " ",
+        "90000");
   }
 
   @Test
   public void testPublicStandardConstructor() {
     thrown.expect(AssertionError.class);
     AdServer adServer =
-        new AdServer(Method.STANDARD, "localhost", 389, " ", " ");
+        new AdServer(Method.STANDARD, "localhost", 389, " ", " ", "90000");
   }
 
   @Test
@@ -179,6 +194,44 @@
   }
 
   @Test
+  public void testEnsureOnetimeException() throws Exception {
+    MockLdapContext ldapContext = new MockLdapContext() {
+      boolean firstTime = true;
+      @Override
+      public Attributes getAttributes(String name) throws NamingException {
+        if (firstTime) {
+          firstTime = false;
+          throw new CommunicationException("testing");
+        } else {
+          return super.getAttributes(name);
+        }
+      }
+    };
+    addStandardKeysAndResults(ldapContext);
+    AdServer adServer = new AdServer("localhost", ldapContext) {
+      @Override
+      void recreateLdapContext() {
+        // do nothing
+      }
+    };
+    adServer.initialize();
+  }
+
+  @Test
+  public void testEnsureConnectionTimesOut() throws Exception {
+    thrown.expect(RuntimeException.class);
+    MockLdapContext ldapContext = new MockLdapContext() {
+      @Override
+      public Attributes getAttributes(String name) throws NamingException {
+        throw new NamingException("read timed out");
+      }
+    };
+    addStandardKeysAndResults(ldapContext);
+    AdServer adServer = new AdServer("localhost", ldapContext);
+    adServer.initialize();
+  }
+
+  @Test
   public void testSearchReturnsOneUser() throws Exception {
     MockLdapContext ldapContext = new MockLdapContext();
     addStandardKeysAndResults(ldapContext);