Introduce incremental push logic into AdAdaptor
diff --git a/src/com/google/enterprise/adaptor/ad/AdAdaptor.java b/src/com/google/enterprise/adaptor/ad/AdAdaptor.java
index 816bb70..d318243 100644
--- a/src/com/google/enterprise/adaptor/ad/AdAdaptor.java
+++ b/src/com/google/enterprise/adaptor/ad/AdAdaptor.java
@@ -20,6 +20,7 @@
 import com.google.enterprise.adaptor.Config;
 import com.google.enterprise.adaptor.DocIdPusher;
 import com.google.enterprise.adaptor.GroupPrincipal;
+import com.google.enterprise.adaptor.PollingIncrementalLister;
 import com.google.enterprise.adaptor.Principal;
 import com.google.enterprise.adaptor.Request;
 import com.google.enterprise.adaptor.Response;
@@ -34,7 +35,8 @@
 import javax.naming.InterruptedNamingException;
 
 /** Adaptor for Active Directory. */
-public class AdAdaptor extends AbstractAdaptor {
+public class AdAdaptor extends AbstractAdaptor
+    implements PollingIncrementalLister {
   private static final Logger log
       = Logger.getLogger(AdAdaptor.class.getName());
   private static final boolean CASE_SENSITIVITY = false;
@@ -68,7 +70,8 @@
     defaultPassword = context.getConfig().getValue("ad.defaultPassword");
     feedBuiltinGroups = Boolean.parseBoolean(
         context.getConfig().getValue("ad.feedBuiltinGroups"));
-
+    // register for incremental pushes
+    context.setPollingIncrementalLister(this);
     List<Map<String, String>> serverConfigs
         = context.getConfig().getListOfConfigs("ad.servers");
     servers.clear();  // in case init gets called again
@@ -136,15 +139,36 @@
   @Override
   public void getDocIds(DocIdPusher pusher) throws InterruptedException,
       IOException {
+    readFromAllServers(pusher, false);
+  }
+
+  /**
+   * Attempts an incremntal push of all groups from all AdServers
+   *
+   * When a server cannot do an incremental push, it does a full push.
+   */
+  @Override
+  public void getModifiedDocIds(DocIdPusher pusher) throws InterruptedException,
+      IOException {
+    readFromAllServers(pusher, true);
+  }
+
+  public void readFromAllServers(DocIdPusher pusher,
+      boolean doIncrementalPushIfPossible)
+      throws InterruptedException, IOException {
     // TODO(pjo): implement built in groups
     GroupCatalog cumulativeCatalog = new GroupCatalog(localizedStrings,
         namespace, feedBuiltinGroups);
     for (AdServer server : servers) {
+      String previousServiceName = server.getDsServiceName();
+      String previousInvocationId = server.getInvocationID();
+      long previousHighestUSN = server.getHighestCommittedUSN();
       server.initialize();
       try {
         GroupCatalog catalog = new GroupCatalog(localizedStrings, namespace,
             feedBuiltinGroups);
-        catalog.readFrom(server);
+        catalog.readFrom(server, doIncrementalPushIfPossible,
+            previousServiceName, previousInvocationId, previousHighestUSN);
         cumulativeCatalog.add(catalog);
       } catch (InterruptedNamingException ine) {
         String host = server.getHostName();
@@ -237,7 +261,47 @@
       this.domain.putAll(domain);
     }
 
-    void readFrom(AdServer server) throws InterruptedNamingException {
+    void readFrom(AdServer server, boolean doIncrementalPushIfPossible,
+        String previousServiceName, String previousInvocationId,
+        long previousHighestUSN) throws InterruptedNamingException {
+      if (!doIncrementalPushIfPossible) {
+        log.log(Level.INFO, "Starting full crawl.");
+        fullCrawl(server);
+        return;
+      }
+      // TODO(myk): Determine whether adaptors should include code to get/set
+      // last full sync time, and if exceeding some threshhold should force a
+      // full crawl.
+      String currentServiceName = server.getDsServiceName();
+      String currentInvocationId = server.getInvocationID();
+      long currentHighestUSN = server.getHighestCommittedUSN();
+      if (!currentServiceName.equals(previousServiceName)) {
+        log.log(Level.WARNING,
+            "Directory Controller changed from {0} to {1} -- performing full "
+            + "recrawl.  Consider configuring AD server to connect directly to "
+            + "FQDN address of domain controller for partial updates support.",
+            new Object[]{previousServiceName, currentServiceName});
+        fullCrawl(server);
+        return;
+      }
+      if (!currentInvocationId.equals(previousInvocationId)) {
+        log.log(Level.WARNING,
+            "Directory Controller {0} has been restored from backup.  "
+            + "Performing full recrawl.", currentServiceName);
+        fullCrawl(server);
+        return;
+      }
+      if (currentHighestUSN == previousHighestUSN) {
+        log.log(Level.INFO, "No updates on server {0} -- no crawl invoked.",
+            server);
+        return;
+      }
+      log.log(Level.INFO, "Starting incremental crawl.");
+      incrementalCrawl(server, previousHighestUSN, currentHighestUSN);
+    }
+
+    @VisibleForTesting
+    void fullCrawl(AdServer server) throws InterruptedNamingException {
       // LDAP_MATCHING_RULE_BIT_AND = 1.2.840.113556.1.4.803
       // and ADS_GROUP_TYPE_SECURITY_ENABLED = 2147483648.
       entities = server.search("(|(&(objectClass=group)"
@@ -260,6 +324,37 @@
       resolvePrimaryGroups();
     }
 
+    @VisibleForTesting
+    void incrementalCrawl(AdServer server, long previousHighestUSN,
+        long currentHighestUSN) throws InterruptedNamingException {
+      // LDAP_MATCHING_RULE_BIT_AND = 1.2.840.113556.1.4.803
+      // and ADS_GROUP_TYPE_SECURITY_ENABLED = 2147483648.
+      Set<AdEntity> newEntities = server.search("(&(uSNChanged>"
+          + previousHighestUSN + ")(|(&(objectClass=group)"
+          + "(groupType:1.2.840.113556.1.4.803:=2147483648))"
+          + "(&(objectClass=user)(objectCategory=person))))",
+          /*deleted=*/ false,
+          new String[] { "uSNChanged", "sAMAccountName", "objectGUID;binary",
+              "objectSid;binary", "userPrincipalName", "primaryGroupId",
+              "member", "userAccountControl" });
+      // disabled groups handled later, in makeDefs()
+      log.log(Level.FINE, "received new {0} entities from server",
+          newEntities.size());
+      for (AdEntity e : newEntities) {
+        bySid.put(e.getSid(), e);
+        byDn.put(e.getDn(), e);
+        //TODO(myk): Handle tombstones / deleted items?
+        entities.add(e);
+        // TODO(pjo): Have AdServer put domain into AdEntity during search
+        domain.put(e, e.getSid().startsWith("S-1-5-32-") ?
+            localizedStrings.get("Builtin") : server.getnETBIOSName());
+      }
+      // TODO(myk): determine if these routines should be optimized to handle
+      // only the newly-discovered entities.
+      initializeMembers();
+      resolvePrimaryGroups();
+    }
+
     private void initializeMembers() {
       for (AdEntity entity : entities) {
         if (entity.isGroup()) {
@@ -270,7 +365,7 @@
 
     private void resolvePrimaryGroups() {
       int nadds = 0;
-      int missing_groups = 0;
+      int missingGroups = 0;
       for (AdEntity e : entities) {
         if (e.isGroup()) {
           continue;
@@ -278,7 +373,7 @@
         AdEntity user = e;
         AdEntity primaryGroup = bySid.get(user.getPrimaryGroupSid());
         if (primaryGroup == null) {
-          missing_groups++;
+          missingGroups++;
           log.log(Level.WARNING,
               "Group {0} -- primary group for user {1} -- not found",
               new Object[]{user.getPrimaryGroupSid(), user});
@@ -292,8 +387,8 @@
         nadds++;
       }
       log.log(Level.FINE, "# primary groups: {0}", members.keySet().size());
-      if (missing_groups > 0) {
-        log.log(Level.FINE, "# missing primary groups: {0}", missing_groups);
+      if (missingGroups > 0) {
+        log.log(Level.FINE, "# missing primary groups: {0}", missingGroups);
       }
       log.log(Level.FINE, "# users added to all primary groups: {0}", nadds);
     }
diff --git a/test/com/google/enterprise/adaptor/TestHelper.java b/test/com/google/enterprise/adaptor/TestHelper.java
index 0003d41..0c3b41f 100644
--- a/test/com/google/enterprise/adaptor/TestHelper.java
+++ b/test/com/google/enterprise/adaptor/TestHelper.java
@@ -34,6 +34,11 @@
       public Config getConfig() {
         return config;
       }
+
+      @Override
+      public void setPollingIncrementalLister(PollingIncrementalLister lister) {
+        // do nothing
+      }
     };
   }
 }
diff --git a/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java b/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java
index 6115d08..002e906 100644
--- a/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java
@@ -104,6 +104,8 @@
     groupCatalog.add(golden);
     groupCatalog.domain.clear();
     assertFalse(golden.equals(groupCatalog));
+
+    assertFalse(golden.hashCode() == groupCatalog.hashCode());
   }
 
   @Test
@@ -131,7 +133,8 @@
     AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
 
-    groupCatalog.readFrom(adServer);
+    groupCatalog.readFrom(adServer, /*doIncrementalPushIfPossible=*/ false,
+        "ds_service_name", "0x0123456789abc", 12345678L);
 
     final AdEntity goldenEntity = new AdEntity("S-1-0-0",
         "cn=name\\ under,DN_for_default_naming_context");
@@ -185,7 +188,8 @@
     AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
 
-    groupCatalog.readFrom(adServer);
+    groupCatalog.readFrom(adServer, /*doIncrementalPushIfPossible=*/ false,
+        "ds_service_name", "0x0123456789abc", 12345678L);
 
     AdEntity[] groupEntity = groupCatalog.entities.toArray(new AdEntity[0]);
     final AdEntity goldenEntity = groupEntity[0];
@@ -247,7 +251,8 @@
     adServer.initialize();
 
     groupCatalog.bySid.put("S-1-5-32-users", userGroup);
-    groupCatalog.readFrom(adServer);
+    groupCatalog.readFrom(adServer, /*doIncrementalPushIfPossible=*/ false,
+        "ds_service_name", "0x0123456789abc", 12345678L);
 
     final AdEntity goldenEntity = new AdEntity("S-1-5-32-544",
         "cn=name\\ under,DN_for_default_naming_context", "users", "sam");
@@ -273,7 +278,8 @@
     assertTrue(golden.equals(groupCatalog));
 
     // make sure readFrom call is idempotent
-    groupCatalog.readFrom(adServer);
+    groupCatalog.readFrom(adServer, /*doIncrementalPushIfPossible=*/ false,
+        "ds_service_name", "0x0123456789abc", 12345678L);
     assertTrue(golden.equals(groupCatalog));
   }
 
@@ -300,7 +306,8 @@
     AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
 
-    groupCatalog.readFrom(adServer);
+    groupCatalog.readFrom(adServer, /*doIncrementalPushIfPossible=*/ false,
+        "ds_service_name", "0x0123456789abc", 12345678L);
 
     final AdEntity goldenEntity = new AdEntity("S-1-5-32-544",
         "cn=name\\ under,DN_for_default_naming_context", "users", "sam");
@@ -324,7 +331,184 @@
     assertTrue(golden.equals(groupCatalog));
 
     // make sure readFrom call is idempotent
-    groupCatalog.readFrom(adServer);
+    groupCatalog.readFrom(adServer, /*doIncrementalPushIfPossible=*/ false,
+        "ds_service_name", "0x0123456789abc", 12345678L);
+    assertTrue(golden.equals(groupCatalog));
+  }
+
+  @Test
+  public void testGetModifiedDocIdsCallsReadFromAllServersCorrectly()
+      throws Exception {
+    FakeAdaptor adAdaptor = new FakeAdaptor() {
+    boolean attemptedIncrementalPush = false;
+      @Override
+      public void readFromAllServers(DocIdPusher pusher,
+          boolean doIncrementalPushIfPossible)
+          throws InterruptedException, IOException {
+        attemptedIncrementalPush = true;
+      }
+      @Override
+      public boolean equals(Object o) {
+        boolean results = attemptedIncrementalPush;
+        attemptedIncrementalPush = false;
+        return results;
+      }
+    };
+
+    assertFalse(adAdaptor.equals(null));
+    adAdaptor.getModifiedDocIds(null);
+    assertTrue(adAdaptor.equals(null));
+    assertFalse(adAdaptor.equals(null));
+  }
+
+  @Test
+  public void testFullCrawlVersusIncrementalCrawlFlow() throws Exception {
+    FakeAdaptor adAdaptor = new FakeAdaptor();
+    FakeCatalog groupCatalog = new FakeCatalog(defaultLocalizedStringMap(),
+        "example.com", false);
+    MockLdapContext ldapContext = defaultMockLdapContext();
+    AdServer adServer = new AdServer("localhost", ldapContext);
+    adServer.initialize();
+    // the following 3 lines initialize AdAdAptor.
+    AccumulatingDocIdPusher pusher = new AccumulatingDocIdPusher();
+    Map<String, String> configEntries = defaultConfig();
+    pushGroupDefinitions(adAdaptor, configEntries, pusher);
+
+    groupCatalog.resetCrawlFlags();
+    assertFalse(groupCatalog.ranFullCrawl);
+    assertFalse(groupCatalog.ranIncrementalCrawl);
+
+    groupCatalog.readFrom(adAdaptor.getServer(), false, "ds_service_name",
+        "0x0123456789abc", 12345678L);
+    assertTrue(groupCatalog.ranFullCrawl);
+    assertFalse(groupCatalog.ranIncrementalCrawl);
+
+    groupCatalog.resetCrawlFlags();
+    groupCatalog.readFrom(adAdaptor.getServer(), true, "other_ds_service_name",
+        "0x0123456789abc", 12345678L);
+    assertTrue(groupCatalog.ranFullCrawl);
+    assertFalse(groupCatalog.ranIncrementalCrawl);
+
+    groupCatalog.resetCrawlFlags();
+    groupCatalog.readFrom(adAdaptor.getServer(), true, "ds_service_name",
+        "otherInvocationId", 12345678L);
+    assertTrue(groupCatalog.ranFullCrawl);
+    assertFalse(groupCatalog.ranIncrementalCrawl);
+
+    groupCatalog.resetCrawlFlags();
+    groupCatalog.readFrom(adAdaptor.getServer(), true, "ds_service_name",
+        "0x0123456789abc", 12345678L); // last USN as previous run: no crawl
+    assertFalse(groupCatalog.ranFullCrawl);
+    assertFalse(groupCatalog.ranIncrementalCrawl);
+
+    // call Incremental call (only) when it's desired and the service name and
+    // invocation ID both match and the HighestUSN does not match.
+    groupCatalog.resetCrawlFlags();
+    groupCatalog.readFrom(adAdaptor.getServer(), true, "ds_service_name",
+        "0x0123456789abc", 12345677L);
+    assertFalse(groupCatalog.ranFullCrawl);
+    assertTrue(groupCatalog.ranIncrementalCrawl);
+  }
+
+  @Test
+  public void testGroupCatalogReadFromIncrementalCrawl() throws Exception {
+    Map<String, String> strings = defaultLocalizedStringMap();
+
+    AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
+      strings, "example.com", /*feedBuiltinGroups=*/ false);
+    MockLdapContext ldapContext = defaultMockLdapContext();
+    // add a group
+    String filter = "(|(&(objectClass=group)"
+        + "(groupType:1.2.840.113556.1.4.803:=2147483648))"
+        + "(&(objectClass=user)(objectCategory=person)))";
+    String incrementalFilter = "(&(uSNChanged>12345677)" + filter + ")";
+    String searchDn = "DN_for_default_naming_context";
+    List<String> members = Arrays.asList("dn_for_user_1", "dn_for_user_2");
+    ldapContext.addSearchResult(filter, "cn", searchDn, "group_name")
+               .addSearchResult(filter, "objectSid;binary", searchDn,
+                   hexStringToByteArray("010100000000000000000000")) // S-1-0-0
+               .addSearchResult(filter, "objectGUID;binary", searchDn,
+                   hexStringToByteArray("000102030405060708090a0b0e"))
+               .addSearchResult(filter, "member", searchDn, members)
+               .addSearchResult(filter, "sAMAccountName", searchDn,
+                   "name under");
+    ldapContext.addSearchResult(incrementalFilter, "cn", searchDn, "group_name")
+               .addSearchResult(incrementalFilter, "objectSid;binary", searchDn,
+                   hexStringToByteArray("010100000000000000000000")) // S-1-0-0
+               .addSearchResult(incrementalFilter, "objectGUID;binary",
+                   searchDn, hexStringToByteArray("000102030405060708090a0b0e"))
+               .addSearchResult(incrementalFilter, "member", searchDn, members)
+               .addSearchResult(incrementalFilter, "sAMAccountName", searchDn,
+                   "name under");
+    // and a new user (under the incremental filter)
+    ldapContext.addSearchResult(incrementalFilter, "cn", searchDn, "admin_user")
+           .addSearchResult(incrementalFilter, "objectSid;binary", searchDn,
+                      // S-1-5-32-544: local Admin. group
+           hexStringToByteArray("01020000000000052000000020020000"))
+           .addSearchResult(incrementalFilter, "objectGUID;binary", searchDn,
+           hexStringToByteArray("000102030405060708090a0b0e"))
+           .addSearchResult(incrementalFilter, "primaryGroupId", searchDn,
+               "users")
+           .addSearchResult(incrementalFilter, "sAMAccountName", searchDn,
+               "sam2");
+
+    AdServer adServer = new AdServer("localhost", ldapContext);
+    adServer.initialize();
+
+    // first, do a full crawl
+    groupCatalog.readFrom(adServer, /*doIncrementalPushIfPossible=*/ false,
+        "ds_service_name", "0x0123456789abc", 12345678L);
+
+    // now do an incremental crawl
+    groupCatalog.readFrom(adServer, /*doIncrementalPushIfPossible=*/ true,
+        "ds_service_name", "0x0123456789abc", 12345677L);
+
+    // extract incrementally-added user as one golden entity
+    Set<AdEntity> incrementalUserSet = adServer.search(incrementalFilter, false,
+        new String[] { "member", "objectSid;binary", "objectGUID;binary",
+            "primaryGroupId", "sAMAccountName" });
+    assertEquals(1, incrementalUserSet.size());
+    for (AdEntity ae : incrementalUserSet) {
+      assertFalse(ae.isGroup());
+    }
+    AdEntity[] searchedEntity = incrementalUserSet.toArray(new AdEntity[0]);
+    final AdEntity goldenUserEntity = searchedEntity[0];
+
+    // extract group from full crawl as the other golden entity
+    Set<AdEntity> fullyCrawledGroupSet = adServer.search(filter, false,
+        new String[] { "member", "objectSid;binary", "objectGUID;binary",
+            "primaryGroupId", "sAMAccountName" });
+    assertEquals(1, fullyCrawledGroupSet.size());
+    for (AdEntity ae : fullyCrawledGroupSet) {
+      assertTrue(ae.isGroup());
+    }
+    AdEntity[] groupEntity = fullyCrawledGroupSet.toArray(new AdEntity[0]);
+    final AdEntity goldenGroupEntity = new AdEntity("S-1-0-0",
+        "cn=name\\ under,DN_for_default_naming_context");
+    goldenGroupEntity.getMembers().addAll(members);
+
+    final Map<AdEntity, Set<String>> goldenMembers =
+        new HashMap<AdEntity, Set<String>>();
+    goldenMembers.put(goldenGroupEntity, goldenGroupEntity.getMembers());
+    final Map<String, AdEntity> goldenSid =
+        new HashMap<String, AdEntity>();
+    goldenSid.put(goldenGroupEntity.getSid(), goldenGroupEntity);
+    goldenSid.put(goldenUserEntity.getSid(), goldenUserEntity);
+    final Map<String, AdEntity> goldenDn =
+        new HashMap<String, AdEntity>();
+    goldenDn.put(goldenUserEntity.getDn(), goldenUserEntity);
+    final Map<AdEntity, String> goldenDomain = new HashMap<AdEntity, String>();
+    goldenDomain.put(goldenUserEntity, "BUILTIN");
+    goldenDomain.put(goldenGroupEntity, "GSA-CONNECTORS");
+
+    final AdAdaptor.GroupCatalog golden = new AdAdaptor.GroupCatalog(
+      strings, "example.com", /*feedBuiltinGroups=*/ true,
+      /*entities*/ Sets.newHashSet(goldenUserEntity, goldenGroupEntity),
+      /*members*/ goldenMembers,
+      /*bySid*/ goldenSid,
+      /*byDn*/ goldenDn,
+      /*domain*/ goldenDomain);
+
     assertTrue(golden.equals(groupCatalog));
   }
 
@@ -365,7 +549,8 @@
     AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
 
-    groupCatalog.readFrom(adServer);
+    groupCatalog.readFrom(adServer, /*doIncrementalPushIfPossible=*/ false,
+        "ds_service_name", "0x0123456789abc", 12345678L);
 
     // add two additional entities to test all branches of our method.
     // first -- a user
@@ -430,7 +615,7 @@
 
     assertTrue(golden.equals(groupCatalog));
 
-    // make sure readFrom call is idempotent
+    // make sure resolveForeignSecurityPrincipals call is idempotent
     groupCatalog.resolveForeignSecurityPrincipals();
     assertTrue(golden.equals(groupCatalog));
   }
@@ -447,7 +632,8 @@
     AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
 
-    groupCatalog.readFrom(adServer);
+    groupCatalog.readFrom(adServer, /*doIncrementalPushIfPossible=*/ false,
+        "ds_service_name", "0x0123456789abc", 12345678L);
 
     tweakGroupCatalogForMakeDefs(groupCatalog, adServer, false);
 
@@ -476,7 +662,8 @@
     AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
 
-    groupCatalog.readFrom(adServer);
+    groupCatalog.readFrom(adServer, /*doIncrementalPushIfPossible=*/ false,
+        "ds_service_name", "0x0123456789abc", 12345678L);
 
     tweakGroupCatalogForMakeDefs(groupCatalog, adServer, true);
 
@@ -515,7 +702,8 @@
     AdServer adServer = new AdServer("localhost", ldapContext);
     adServer.initialize();
 
-    groupCatalog.readFrom(adServer);
+    groupCatalog.readFrom(adServer, /*doIncrementalPushIfPossible=*/ false,
+        "ds_service_name", "0x0123456789abc", 12345678L);
 
     tweakGroupCatalogForMakeDefs(groupCatalog, adServer, false);
     // now replace the parent group with a well-known one
@@ -976,6 +1164,7 @@
 
   /** A version of AdAdaptor that uses only mock AdServers */
   public class FakeAdaptor extends AdAdaptor {
+    private AdServer fakeServer = null;
     @Override
     AdServer newAdServer(Method method, String host, int port,
         String principal, String passwd) {
@@ -985,7 +1174,41 @@
       } catch (Exception e) {
         fail("Could not create LdapContext:" + e);
       }
-      return new AdServer(host, ldapContext);
+      fakeServer = new AdServer(host, ldapContext);
+      return fakeServer;
+    }
+
+    AdServer getServer() {
+      return fakeServer;
+    }
+  };
+
+  /** Simple Fake of GroupCatalog that tracks calls to full/incremental crawl */
+  public class FakeCatalog extends AdAdaptor.GroupCatalog {
+    boolean ranFullCrawl;
+    boolean ranIncrementalCrawl;
+
+    public FakeCatalog(Map<String, String> localizedStrings, String namespace,
+        boolean feedBuiltinGroups) {
+      super(localizedStrings, namespace, feedBuiltinGroups);
+    }
+
+    @Override
+    void fullCrawl(AdServer server) throws InterruptedNamingException {
+      ranIncrementalCrawl = false;
+      ranFullCrawl = true;
+    }
+
+    @Override
+    void incrementalCrawl(AdServer server, long previousHighestUSN,
+        long currentHighestUSN) throws InterruptedNamingException {
+      ranFullCrawl = false;
+      ranIncrementalCrawl = true;
+    }
+
+    void resetCrawlFlags() {
+      ranFullCrawl = false;
+      ranIncrementalCrawl = false;
     }
   }
 }