Merge CONNECTORS-1740 branch.

git-svn-id: https://svn.apache.org/repos/asf/manifoldcf/trunk@1910246 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/build.xml b/build.xml
index 691f356..35a0c44 100644
--- a/build.xml
+++ b/build.xml
@@ -31,12 +31,12 @@
 
     <property name="javax-activation.version" value="1.2.0"/>
     <property name="hsqldb.version" value="2.3.2"/>
-    <property name="jetty.version" value="9.4.25.v20191220"/>
+    <property name="jetty.version" value="9.4.48.v20220622"/>
     <property name="httpclient.version" value="4.5.13"/>
     <property name="httpmime.version" value="4.5.13"/>
     <property name="httpcore.version" value="4.4.15"/>
     <property name="xerces.version" value="2.12.2"/>
-    <property name="zookeeper.version" value="3.4.10"/>
+    <property name="zookeeper.version" value="3.5.6"/>
     <property name="mongodb.version" value="2.14.3"/>
     <property name="postgresql.version" value="42.1.3"/>
     <property name="axis.version" value="1.4"/>
@@ -84,7 +84,7 @@
     <property name="jakarta.annotation-api.version" value="1.3.4"/>
     <property name="jakarta.jws-api.version" value="1.1.1"/>
     <property name="ha-api.version" value="3.1.12"/>
-    <property name="solrj.version" value="8.3.1"/>
+    <property name="solrj.version" value="9.1.0"/>
     <property name="noggit.version" value="0.8"/>
     <property name="commons-math3.version" value="3.6.1"/>
     <property name="chemistry.version" value="1.1.0"/>
@@ -1421,10 +1421,35 @@
     <target name="download-jetty">
         <mkdir dir="lib"/>
         <property name="jetty-package" value="org/eclipse/jetty"/>
+        <property name="jetty-http2-package" value="org/eclipse/jetty/http2"/>
         <antcall target="download-via-maven"><param name="project-path" value="${jetty-package}"/><param name="artifact-version" value="9.2.30.v20200428"/><param name="target" value="lib"/>
             <param name="artifact-name" value="jetty-jsp"/>
             <param name="artifact-type" value="jar"/>
         </antcall>
+        <antcall target="download-via-maven"><param name="project-path" value="${jetty-http2-package}"/><param name="artifact-version" value="${jetty.version}"/><param name="target" value="lib"/>
+            <param name="artifact-name" value="http2-client"/>
+            <param name="artifact-type" value="jar"/>
+        </antcall>
+    	  <antcall target="download-via-maven"><param name="project-path" value="${jetty-http2-package}"/><param name="artifact-version" value="${jetty.version}"/><param name="target" value="lib"/>
+            <param name="artifact-name" value="http2-common"/>
+            <param name="artifact-type" value="jar"/>
+        </antcall>
+    	  <antcall target="download-via-maven"><param name="project-path" value="${jetty-http2-package}"/><param name="artifact-version" value="${jetty.version}"/><param name="target" value="lib"/>
+            <param name="artifact-name" value="http2-hpack"/>
+            <param name="artifact-type" value="jar"/>
+        </antcall>
+    	  <antcall target="download-via-maven"><param name="project-path" value="${jetty-http2-package}"/><param name="artifact-version" value="${jetty.version}"/><param name="target" value="lib"/>
+            <param name="artifact-name" value="http2-http-client-transport"/>
+            <param name="artifact-type" value="jar"/>
+        </antcall>
+    	 <antcall target="download-via-maven"><param name="project-path" value="${jetty-package}"/><param name="artifact-version" value="${jetty.version}"/><param name="target" value="lib"/>
+            <param name="artifact-name" value="jetty-alpn-client"/>
+            <param name="artifact-type" value="jar"/>
+        </antcall>
+    	  <antcall target="download-via-maven"><param name="project-path" value="${jetty-package}"/><param name="artifact-version" value="${jetty.version}"/><param name="target" value="lib"/>
+            <param name="artifact-name" value="jetty-alpn-java-client"/>
+            <param name="artifact-type" value="jar"/>
+        </antcall>
         <antcall target="download-via-maven"><param name="project-path" value="${jetty-package}"/><param name="artifact-version" value="${jetty.version}"/><param name="target" value="lib"/>
             <param name="artifact-name" value="jetty-client"/>
             <param name="artifact-type" value="jar"/>
@@ -2442,6 +2467,13 @@
         </antcall>
         <antcall target="download-via-maven">
             <param name="target" value="lib"/>
+            <param name="project-path" value="org/apache/solr"/>
+            <param name="artifact-version" value="${solrj.version}"/>
+            <param name="artifact-name" value="solr-solrj-zookeeper"/>
+            <param name="artifact-type" value="jar"/>
+        </antcall>
+        <antcall target="download-via-maven">
+            <param name="target" value="lib"/>
             <param name="project-path" value="org/noggit"/>
             <param name="artifact-version" value="${noggit.version}"/>
             <param name="artifact-name" value="noggit"/>
@@ -2465,6 +2497,13 @@
             <param name="artifact-name" value="zookeeper"/>
             <param name="artifact-type" value="jar"/>
         </antcall>
+        <antcall target="download-via-maven">
+    	    <param name="target" value="lib"/>
+            <param name="project-path" value="org/apache/zookeeper"/>
+            <param name="artifact-version" value="${zookeeper.version}"/>
+            <param name="artifact-name" value="zookeeper-jute"/>
+            <param name="artifact-type" value="jar"/>
+        </antcall>
     </target>
     
     <target name="download-commons-exec">
diff --git a/connectors/solr/build.xml b/connectors/solr/build.xml
index 18efc7c..f44d178 100644
--- a/connectors/solr/build.xml
+++ b/connectors/solr/build.xml
@@ -34,11 +34,12 @@
         <path refid="mcf-connector-build.connector-classpath"/>
         <fileset dir="../../lib">
             <include name="solr-solrj*.jar"/>
+        	  <include name="http2*.jar"/>
+        	  <include name="httpmime*.jar"/>
+        	  <include name="jetty-client*.jar"/>
+            <include name="jetty-http*.jar"/>
+            <include name="jetty-util*.jar"/>
             <include name="commons-math3*.jar"/>
-            <include name="noggit*.jar"/>
-            <include name="httpmime*.jar"/>
-            <include name="woodstox-core-asl*.jar"/>
-            <include name="stax2-api*.jar"/>
             <include name="jcl-over-slf4j*.jar"/>
         </fileset>
     </path>
@@ -49,10 +50,6 @@
             <fileset dir="../../lib">
                 <include name="solr-solrj*.jar"/>
                 <include name="commons-math3*.jar"/>
-                <include name="noggit*.jar"/>
-                <include name="httpmime*.jar"/>
-                <include name="woodstox-core-asl*.jar"/>
-                <include name="stax2-api*.jar"/>
                 <include name="jcl-over-slf4j*.jar"/>
             </fileset>
         </copy>
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/HttpPoster.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/HttpPoster.java
index 59bc233..8ca4cd4 100644
--- a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/HttpPoster.java
+++ b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/HttpPoster.java
@@ -18,66 +18,61 @@
 */
 package org.apache.manifoldcf.agents.output.solr;
 
-import org.apache.http.client.CredentialsProvider;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.config.SocketConfig;
-import org.apache.http.config.RegistryBuilder;
-import org.apache.http.conn.socket.ConnectionSocketFactory;
-import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
-import org.apache.http.conn.socket.PlainConnectionSocketFactory;
-import org.apache.http.conn.ssl.NoopHostnameVerifier;
-import org.apache.http.impl.client.*;
-import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
-import org.apache.http.protocol.HttpRequestExecutor;
-import org.apache.manifoldcf.core.interfaces.*;
-import org.apache.manifoldcf.connectorcommon.interfaces.*;
-import org.apache.manifoldcf.core.common.DateParser;
-import org.apache.manifoldcf.agents.interfaces.*;
-import org.apache.manifoldcf.agents.system.*;
-
-import java.io.*;
-import java.net.MalformedURLException;
-import java.util.*;
-import java.util.regex.*;
-
-import org.apache.http.Consts;
-import org.apache.http.auth.AuthScope;
-import org.apache.http.auth.Credentials;
-import org.apache.http.auth.UsernamePasswordCredentials;
-
-import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.SolrServerException;
-import org.apache.solr.client.solrj.impl.CloudSolrClient;
-import org.apache.solr.client.solrj.impl.HttpSolrClient;
-import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
-import org.apache.solr.client.solrj.request.UpdateRequest;
-import org.apache.solr.client.solrj.impl.XMLResponseParser;
-import org.apache.solr.common.util.ContentStreamBase;
-import org.apache.solr.client.solrj.SolrResponse;
-import org.apache.solr.client.solrj.response.SolrPingResponse;
-import org.apache.solr.client.solrj.response.UpdateResponse;
-import org.apache.solr.client.solrj.SolrRequest;
-import org.apache.solr.common.params.CommonParams;
-import org.apache.solr.common.params.ModifiableSolrParams;
-import org.apache.solr.common.util.ContentStream;
-import org.apache.solr.common.SolrException;
-import org.apache.solr.client.solrj.impl.HttpClientUtil;
-import org.apache.solr.common.SolrInputDocument;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.InterruptedIOException;
+import java.io.Reader;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 
 import org.apache.commons.lang.StringUtils;
-
+import org.apache.http.Consts;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.manifoldcf.agents.interfaces.IOutputAddActivity;
+import org.apache.manifoldcf.agents.interfaces.IOutputRemoveActivity;
+import org.apache.manifoldcf.agents.interfaces.RepositoryDocument;
+import org.apache.manifoldcf.agents.interfaces.ServiceInterruption;
+import org.apache.manifoldcf.agents.output.solr.ModifiedHttp2SolrClient.Builder;
+import org.apache.manifoldcf.agents.system.Logging;
+import org.apache.manifoldcf.connectorcommon.interfaces.IKeystoreManager;
+import org.apache.manifoldcf.core.common.DateParser;
+import org.apache.manifoldcf.core.interfaces.ManifoldCFException;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.impl.HttpClientUtil;
 import org.apache.solr.client.solrj.impl.Krb5HttpClientBuilder;
 import org.apache.solr.client.solrj.impl.SolrHttpClientBuilder;
+import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
+import org.apache.solr.client.solrj.request.UpdateRequest;
+import org.apache.solr.client.solrj.response.SolrPingResponse;
+import org.apache.solr.client.solrj.response.UpdateResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.util.ContentStream;
+import org.apache.solr.common.util.ContentStreamBase;
 
 /**
-* Posts an input stream to SOLR
-*
-* @author James Sablatura, modified by Karl Wright
-*/
-public class HttpPoster
-{
+ * Posts an input stream to SOLR
+ *
+ * @author James Sablatura, modified by Karl Wright
+ */
+public class HttpPoster {
   public static final String _rcsid = "@(#)$Id: HttpPoster.java 991295 2010-08-31 19:12:14Z kwright $";
 
   /** Ingestion buffer size property. */
@@ -120,7 +115,7 @@
 
   // Included and excluded mime types
   private final Set<String> includedMimeTypes;
-  private final Set<String>excludedMimeTypes;
+  private final Set<String> excludedMimeTypes;
 
   // Commit-within flag
   private final String commitWithin;
@@ -134,20 +129,14 @@
   /** How long to wait before retrying a failed ingestion */
   private static final long interruptionRetryTime = 60000L;
 
-  /** Initialize the SolrCloud http poster.
-  */
-  public HttpPoster(String zookeeperHosts, String collection,
-    int zkClientTimeout, int zkConnectTimeout,
-    String updatePath, String removePath, String statusPath,
-    String allowAttributeName, String denyAttributeName, String idAttributeName,
-    String originalSizeAttributeName, String modifiedDateAttributeName, String createdDateAttributeName, String indexedDateAttributeName,
-    String fileNameAttributeName, String mimeTypeAttributeName, String contentAttributeName,
-    Long maxDocumentLength,
-    String commitWithin, boolean useExtractUpdateHandler,
-    final Set<String> includedMimeTypes, final Set<String> excludedMimeTypes,
-    boolean allowCompression)
-    throws ManifoldCFException
-  {
+  /**
+   * Initialize the SolrCloud http poster.
+   */
+  public HttpPoster(final List<String> zookeeperHosts, final String znodePath, final String collection, final int socketTimeout, final int connectionTimeout, final String updatePath,
+      final String removePath, final String statusPath, final String allowAttributeName, final String denyAttributeName, final String idAttributeName, final String originalSizeAttributeName,
+      final String modifiedDateAttributeName, final String createdDateAttributeName, final String indexedDateAttributeName, final String fileNameAttributeName, final String mimeTypeAttributeName,
+      final String contentAttributeName, final Long maxDocumentLength, final String commitWithin, final boolean useExtractUpdateHandler, final Set<String> includedMimeTypes,
+      final Set<String> excludedMimeTypes, final boolean allowCompression) throws ManifoldCFException {
     // These are the paths to the handlers in Solr that deal with the actions we need to do
     this.postUpdateAction = updatePath;
     this.postRemoveAction = removePath;
@@ -173,39 +162,35 @@
 
     initializeKerberos();
 
-    try
-    {
-      CloudSolrClient cloudSolrServer = new CloudSolrClient.Builder()
-        .withZkHost(zookeeperHosts)
-        .withLBHttpSolrClient(new ModifiedLBHttpSolrClient(HttpClientUtil.createClient(null), allowCompression))
-        .build();
-      cloudSolrServer.setZkClientTimeout(zkClientTimeout);
-      cloudSolrServer.setZkConnectTimeout(zkConnectTimeout);
+    Optional<String> chroot = Optional.empty();
+    if (znodePath != null && !znodePath.isEmpty()) {
+      chroot = Optional.of(znodePath);
+    }
+    final Builder http2SolrClientBuilder = new ModifiedHttp2SolrClient.Builder();
+    http2SolrClientBuilder.connectionTimeout(connectionTimeout);
+    http2SolrClientBuilder.idleTimeout(socketTimeout);
+    // CloudSolrClient does not have exceptions in its build method signature because it catches them and encapsulate them as runtime exceptions.
+    // Thus, we need to set a try catch here to properly raise the exceptions that may happen
+    try {
+      final ModifiedCloudSolrClient cloudSolrServer = new ModifiedCloudSolrClient.Builder(zookeeperHosts, chroot).withInternalClientBuilder(http2SolrClientBuilder).build();
       cloudSolrServer.setDefaultCollection(collection);
       // Set the solrj instance we want to use
       solrServer = cloudSolrServer;
+    } catch (final Exception e) {
+      throw new ManifoldCFException(e);
     }
-    catch (MalformedURLException e)
-    {
-      throw new ManifoldCFException(e.getMessage(),e);
-    }
+
   }
 
-  /** Initialize the standard http poster.
-  */
-  public HttpPoster(String protocol, String server, int port, String webapp, String core,
-    int connectionTimeout, int socketTimeout,
-    String updatePath, String removePath, String statusPath,
-    String realm, String userID, String password,
-    String allowAttributeName, String denyAttributeName, String idAttributeName,
-    String originalSizeAttributeName, String modifiedDateAttributeName, String createdDateAttributeName, String indexedDateAttributeName,
-    String fileNameAttributeName, String mimeTypeAttributeName, String contentAttributeName,
-    IKeystoreManager keystoreManager, Long maxDocumentLength,
-    String commitWithin, boolean useExtractUpdateHandler,
-    final Set<String> includedMimeTypes, final Set<String> excludedMimeTypes,
-    boolean allowCompression)
-    throws ManifoldCFException
-  {
+  /**
+   * Initialize the standard http poster.
+   */
+  public HttpPoster(final String protocol, final String server, final int port, final String webapp, final String core, final int connectionTimeout, final int socketTimeout, final String updatePath,
+      final String removePath, final String statusPath, final String realm, final String userID, final String password, final String allowAttributeName, final String denyAttributeName,
+      final String idAttributeName, final String originalSizeAttributeName, final String modifiedDateAttributeName, final String createdDateAttributeName, final String indexedDateAttributeName,
+      final String fileNameAttributeName, final String mimeTypeAttributeName, final String contentAttributeName, final IKeystoreManager keystoreManager, final Long maxDocumentLength,
+      final String commitWithin, final boolean useExtractUpdateHandler, final Set<String> includedMimeTypes, final Set<String> excludedMimeTypes, final boolean allowCompression)
+      throws ManifoldCFException {
     // These are the paths to the handlers in Solr that deal with the actions we need to do
     this.postUpdateAction = updatePath;
     this.postRemoveAction = removePath;
@@ -234,106 +219,48 @@
     String location = "";
     if (webapp != null)
       location = "/" + webapp;
-    if (core != null)
-    {
+    if (core != null) {
       if (webapp == null)
         throw new ManifoldCFException("Webapp must be specified if core is specified.");
       location += "/" + core;
     }
 
     // Initialize standard solr-j.
-
-    SSLConnectionSocketFactory myFactory;
-    if (keystoreManager != null)
-    {
-      myFactory = new SSLConnectionSocketFactory(keystoreManager.getSecureSocketFactory(), NoopHostnameVerifier.INSTANCE);
+    final String httpSolrServerUrl = protocol + "://" + server + ":" + port + location;
+    final Builder solrClientBuilder = new ModifiedHttp2SolrClient.Builder(httpSolrServerUrl);
+    solrClientBuilder.maxConnectionsPerHost(1);
+    // Set tiemouts
+    solrClientBuilder.connectionTimeout(connectionTimeout);
+    solrClientBuilder.idleTimeout(socketTimeout);
+    // Set auth credentials if provided
+    if (userID != null && userID.length() > 0 && password != null) {
+      solrClientBuilder.withBasicAuthCredentials(userID, password);
     }
-    else
-    {
-      // Use the "trust everything" one
-      myFactory = new SSLConnectionSocketFactory(KeystoreManagerFactory.getTrustingSecureSocketFactory(),NoopHostnameVerifier.INSTANCE);
-    }
-
-    // First, we need an HttpClient where basic auth is properly set up.
-    connectionManager = new PoolingHttpClientConnectionManager(RegistryBuilder.<ConnectionSocketFactory>create()
-      .register("http", PlainConnectionSocketFactory.getSocketFactory())
-      .register("https", myFactory)
-      .build());
-    connectionManager.setDefaultMaxPerRoute(1);
-    connectionManager.setValidateAfterInactivity(2000);
-    connectionManager.setDefaultSocketConfig(SocketConfig.custom()
-      .setTcpNoDelay(true)
-      .setSoTimeout(socketTimeout)
-      .build());
-
-    RequestConfig.Builder requestBuilder = RequestConfig.custom()
-      .setCircularRedirectsAllowed(true)
-      .setSocketTimeout(socketTimeout)
-      .setExpectContinueEnabled(true)
-      .setConnectTimeout(connectionTimeout)
-      .setConnectionRequestTimeout(socketTimeout);
-
-    HttpClientBuilder clientBuilder = HttpClients.custom()
-      .setConnectionManager(connectionManager)
-      .disableAutomaticRetries()
-      .setDefaultRequestConfig(requestBuilder.build())
-      .setRedirectStrategy(new LaxRedirectStrategy())
-      .setRequestExecutor(new HttpRequestExecutor(socketTimeout));
-
-
-    if (userID != null && userID.length() > 0 && password != null)
-    {
-      CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
-      Credentials credentials = new UsernamePasswordCredentials(userID, password);
-      if (realm != null && realm.trim().length() > 0)
-      {
-        final AuthScope scope = new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, realm);
-        credentialsProvider.setCredentials(scope, credentials);
-        clientBuilder.addInterceptorFirst(new PreemptiveBasicAuthInterceptor(scope));
-      }
-      else
-      {
-        credentialsProvider.setCredentials(AuthScope.ANY, credentials);
-        clientBuilder.addInterceptorFirst(new PreemptiveBasicAuthInterceptor(AuthScope.ANY));
-      }
-
-      clientBuilder.setDefaultCredentialsProvider(credentialsProvider);
-    }
-
-    HttpClient localClient = clientBuilder.build();
-
-
-    String httpSolrServerUrl = protocol + "://" + server + ":" + port + location;
-    solrServer = new ModifiedHttpSolrClient(httpSolrServerUrl, localClient, new XMLResponseParser(), allowCompression);
+    solrServer = new ModifiedHttp2SolrClient.Builder(httpSolrServerUrl).build();
   }
 
-  private static void initializeKerberos()
-  {
+  private static void initializeKerberos() {
 
     final String loginConfig = System.getProperty("java.security.auth.login.config");
     if (loginConfig != null && loginConfig.trim().length() > 0) {
       if (Logging.ingest.isInfoEnabled()) {
         Logging.ingest.info("Using Kerberos for Solr Authentication");
       }
-      Krb5HttpClientBuilder krbBuild = new Krb5HttpClientBuilder();
-      SolrHttpClientBuilder kb = krbBuild.getBuilder();
+      final Krb5HttpClientBuilder krbBuild = new Krb5HttpClientBuilder();
+      final SolrHttpClientBuilder kb = krbBuild.getBuilder();
       HttpClientUtil.setHttpClientBuilder(kb);
     }
 
   }
 
-  /** Shut down the poster.
-  */
-  public void shutdown()
-  {
-    if (solrServer != null)
-    {
-      try
-      {
+  /**
+   * Shut down the poster.
+   */
+  public void shutdown() {
+    if (solrServer != null) {
+      try {
         solrServer.close();
-      }
-      catch (IOException ioe)
-      {
+      } catch (final IOException ioe) {
         // Eat this exception
       }
       solrServer = null;
@@ -343,125 +270,90 @@
     connectionManager = null;
   }
 
-  /** Cause a commit to happen.
-  */
-  public void commitPost()
-    throws ManifoldCFException, ServiceInterruption
-  {
+  /**
+   * Cause a commit to happen.
+   */
+  public void commitPost() throws ManifoldCFException, ServiceInterruption {
     if (Logging.ingest.isDebugEnabled())
       Logging.ingest.debug("commitPost()");
 
     // Open a socket to ingest, and to the response stream to get the post result
-    try
-    {
-      CommitThread t = new CommitThread();
-      try
-      {
+    try {
+      final CommitThread t = new CommitThread();
+      try {
         t.start();
         t.finishUp();
         return;
-      }
-      catch (InterruptedException e)
-      {
+      } catch (final InterruptedException e) {
         t.interrupt();
-        throw new ManifoldCFException("Interrupted: "+e.getMessage(),ManifoldCFException.INTERRUPTED);
+        throw new ManifoldCFException("Interrupted: " + e.getMessage(), ManifoldCFException.INTERRUPTED);
       }
-    }
-    catch (SolrServerException e)
-    {
+    } catch (final SolrServerException e) {
       handleSolrServerException(e, "commit");
       return;
-    }
-    catch (SolrException e)
-    {
+    } catch (final SolrException e) {
       handleSolrException(e, "commit");
       return;
-    }
-    catch (RuntimeException e)
-    {
+    } catch (final RuntimeException e) {
       handleRuntimeException(e, "commit");
       return;
-    }
-    catch (IOException ioe)
-    {
+    } catch (final IOException ioe) {
       handleIOException(ioe, "commit");
       return;
     }
   }
 
-  /** Handle a RuntimeException.
-  * Unfortunately, SolrCloud 4.6.x throws RuntimeExceptions whenever ZooKeeper is not happy.
-  * We have to catch these too.  I've logged a ticket: SOLR-5678.
-  */
-  protected static void handleRuntimeException(RuntimeException e, String context)
-    throws ManifoldCFException, ServiceInterruption
-  {
-    Throwable childException = e.getCause();
-    if (childException != null && childException instanceof java.util.concurrent.TimeoutException)
-    {
-      Logging.ingest.warn("SolrJ runtime exception during "+context+": "+childException.getMessage(),childException);
-      long currentTime = System.currentTimeMillis();
-      throw new ServiceInterruption(childException.getMessage(),childException,
-        currentTime + interruptionRetryTime,
-        currentTime + 2L * 60L * 60000L,
-        -1,
-        true);
+  /**
+   * Handle a RuntimeException. Unfortunately, SolrCloud 4.6.x throws RuntimeExceptions whenever ZooKeeper is not happy. We have to catch these too. I've logged a ticket: SOLR-5678.
+   */
+  protected static void handleRuntimeException(final RuntimeException e, final String context) throws ManifoldCFException, ServiceInterruption {
+    final Throwable childException = e.getCause();
+    if (childException != null && childException instanceof java.util.concurrent.TimeoutException) {
+      Logging.ingest.warn("SolrJ runtime exception during " + context + ": " + childException.getMessage(), childException);
+      final long currentTime = System.currentTimeMillis();
+      throw new ServiceInterruption(childException.getMessage(), childException, currentTime + interruptionRetryTime, currentTime + 2L * 60L * 60000L, -1, true);
     }
     // Solr was not able to parse the request because it is malformed: skip the document
-    Logging.ingest.warn("Solr was unable to parse request during "+context+": "+e.getMessage(),e);
+    Logging.ingest.warn("Solr was unable to parse request during " + context + ": " + e.getMessage(), e);
     return;
   }
 
-  /** Handle a SolrServerException.
-  * These exceptions seem to be catch-all exceptions having to do with misconfiguration or
-  * underlying IO exceptions, or request parsing exceptions.
-  * If this method doesn't throw an exception, it means that the exception should be interpreted
-  * as meaning that the document or action is illegal and should not be repeated.
-  */
-  protected static void handleSolrServerException(SolrServerException e, String context)
-    throws ManifoldCFException, ServiceInterruption
-  {
-    Throwable childException = e.getCause();
-    if (childException instanceof IOException)
-    {
-      handleIOException((IOException)childException, context);
+  /**
+   * Handle a SolrServerException. These exceptions seem to be catch-all exceptions having to do with misconfiguration or underlying IO exceptions, or request parsing exceptions. If this method
+   * doesn't throw an exception, it means that the exception should be interpreted as meaning that the document or action is illegal and should not be repeated.
+   */
+  protected static void handleSolrServerException(final SolrServerException e, final String context) throws ManifoldCFException, ServiceInterruption {
+    final Throwable childException = e.getCause();
+    if (childException instanceof IOException) {
+      handleIOException((IOException) childException, context);
       return;
     } else if (childException instanceof RuntimeException) {
       handleRuntimeException((RuntimeException) childException, context);
       return;
     }
-    //  Unknown exception, but a "Solr down" related issue does not end up here: log the error and skip this document or action
-    String message = "SolrServerException exception during "+context+": "+e.getMessage();
-    Logging.ingest.warn(message,e);
+    // Unknown exception, but a "Solr down" related issue does not end up here: log the error and skip this document or action
+    final String message = "SolrServerException exception during " + context + ": " + e.getMessage();
+    Logging.ingest.warn(message, e);
     return;
   }
 
-  /** Handle a SolrException.
-  * These exceptions are mainly Http errors having to do with actual responses from Solr.
-  * If this method doesn't throw an exception, it means that the exception should be interpreted
-  * as meaning that the document or action is illegal and should not be repeated.
-  */
-  protected static void handleSolrException(SolrException e, String context)
-    throws ManifoldCFException, ServiceInterruption
-  {
+  /**
+   * Handle a SolrException. These exceptions are mainly Http errors having to do with actual responses from Solr. If this method doesn't throw an exception, it means that the exception should be
+   * interpreted as meaning that the document or action is illegal and should not be repeated.
+   */
+  protected static void handleSolrException(final SolrException e, final String context) throws ManifoldCFException, ServiceInterruption {
     int code = e.code();
-    if (code == 0)
-    {
-      try
-      {
-        // Solrj doesn't always set the code properly.  If it doesn't, we have to parse it out of the exception string.  Ugh.
-        Pattern p = Pattern.compile("non ok status:([0-9]*),");
-        Matcher m = p.matcher(e.getMessage());
+    if (code == 0) {
+      try {
+        // Solrj doesn't always set the code properly. If it doesn't, we have to parse it out of the exception string. Ugh.
+        final Pattern p = Pattern.compile("non ok status:([0-9]*),");
+        final Matcher m = p.matcher(e.getMessage());
         if (m.find())
           code = Integer.parseInt(m.group(1));
-      }
-      catch (PatternSyntaxException e2)
-      {
-        throw new ManifoldCFException("Unexpected error: "+e2.getMessage());
-      }
-      catch (NumberFormatException e2)
-      {
-        throw new ManifoldCFException("Unexpected error: "+e2.getMessage());
+      } catch (final PatternSyntaxException e2) {
+        throw new ManifoldCFException("Unexpected error: " + e2.getMessage());
+      } catch (final NumberFormatException e2) {
+        throw new ManifoldCFException("Unexpected error: " + e2.getMessage());
       }
     }
 
@@ -471,9 +363,8 @@
       return;
 
     // If code is 401, we should abort the job because security credentials are incorrect
-    if (code == 401)
-    {
-      String message = "Solr authorization failure, code "+code+": aborting job";
+    if (code == 401) {
+      final String message = "Solr authorization failure, code " + code + ": aborting job";
       Logging.ingest.error(message);
       throw new ManifoldCFException(message);
     }
@@ -489,346 +380,251 @@
       long currentTime = System.currentTimeMillis();
 
       // Log the error
-      String message = "Solr exception during "+context+" ("+e.code()+"): "+e.getMessage();
-      Logging.ingest.warn(message,e);
-      throw new ServiceInterruption(message,
-        e,
-        currentTime + interruptionRetryTime,
-        currentTime + 2L * 60L * 60000L,
-        -1,
-        true);
+      final String message = "Solr exception during " + context + " (" + e.code() + "): " + e.getMessage();
+      Logging.ingest.warn(message, e);
+      throw new ServiceInterruption(message, e, currentTime + interruptionRetryTime, currentTime + 2L * 60L * 60000L, -1, true);
     }
 
     // Unknown code: end the job.
-    throw new ManifoldCFException("Unhandled Solr exception during "+context+" ("+e.code()+"): "+e.getMessage());
+    throw new ManifoldCFException("Unhandled Solr exception during " + context + " (" + e.code() + "): " + e.getMessage());
   }
 
-  /** Handle an IOException.
-  * I'm not actually sure where these exceptions come from in SolrJ, but we handle them
-  * as real I/O errors, meaning they should be retried.
-  */
-  protected static void handleIOException(IOException e, String context)
-    throws ManifoldCFException, ServiceInterruption
-  {
+  /**
+   * Handle an IOException. I'm not actually sure where these exceptions come from in SolrJ, but we handle them as real I/O errors, meaning they should be retried.
+   */
+  protected static void handleIOException(final IOException e, final String context) throws ManifoldCFException, ServiceInterruption {
     if ((e instanceof InterruptedIOException) && (!(e instanceof java.net.SocketTimeoutException)))
       throw new ManifoldCFException(e.getMessage(), ManifoldCFException.INTERRUPTED);
 
-    long currentTime = System.currentTimeMillis();
+    final long currentTime = System.currentTimeMillis();
 
-    if (e instanceof java.net.ConnectException)
-    {
-      // Server isn't up at all.  Try for a brief time then give up.
-      String message = "Server could not be contacted during "+context+": "+e.getMessage();
-      Logging.ingest.warn(message,e);
-      throw new ServiceInterruption(message,
-        e,
-        currentTime + interruptionRetryTime,
-        -1L,
-        3,
-        true);
+    if (e instanceof java.net.ConnectException) {
+      // Server isn't up at all. Try for a brief time then give up.
+      final String message = "Server could not be contacted during " + context + ": " + e.getMessage();
+      Logging.ingest.warn(message, e);
+      throw new ServiceInterruption(message, e, currentTime + interruptionRetryTime, -1L, 3, true);
     }
 
-    if (e instanceof java.net.SocketTimeoutException)
-    {
-      String message2 = "Socket timeout exception during "+context+": "+e.getMessage();
-      Logging.ingest.warn(message2,e);
-      throw new ServiceInterruption(message2,
-        e,
-        currentTime + interruptionRetryTime,
-        currentTime + 20L * 60000L,
-        -1,
-        false);
+    if (e instanceof java.net.SocketTimeoutException) {
+      final String message2 = "Socket timeout exception during " + context + ": " + e.getMessage();
+      Logging.ingest.warn(message2, e);
+      throw new ServiceInterruption(message2, e, currentTime + interruptionRetryTime, currentTime + 20L * 60000L, -1, false);
     }
 
-    if (e.getClass().getName().equals("java.net.SocketException"))
-    {
+    if (e.getClass().getName().equals("java.net.SocketException")) {
       // In the past we would have treated this as a straight document rejection, and
-      // treated it in the same manner as a 400.  The reasoning is that the server can
+      // treated it in the same manner as a 400. The reasoning is that the server can
       // perfectly legally send out a 400 and drop the connection immediately thereafter,
       // this a race condition.
       // However, Solr 4.0 (or the Jetty version that the example runs on) seems
       // to have a bug where it drops the connection when two simultaneous documents come in
-      // at the same time.  This is the final version of Solr 4.0 so we need to deal with
+      // at the same time. This is the final version of Solr 4.0 so we need to deal with
       // this.
-      if (e.getMessage().toLowerCase(Locale.ROOT).indexOf("broken pipe") != -1 ||
-        e.getMessage().toLowerCase(Locale.ROOT).indexOf("connection reset") != -1 ||
-        e.getMessage().toLowerCase(Locale.ROOT).indexOf("target server failed to respond") != -1)
-      {
+      if (e.getMessage().toLowerCase(Locale.ROOT).indexOf("broken pipe") != -1 || e.getMessage().toLowerCase(Locale.ROOT).indexOf("connection reset") != -1
+          || e.getMessage().toLowerCase(Locale.ROOT).indexOf("target server failed to respond") != -1) {
         // Treat it as a service interruption, but with a limited number of retries.
         // In that way we won't burden the user with a huge retry interval; it should
         // give up fairly quickly, and yet NOT give up if the error was merely transient
-        String message = "Server dropped connection during "+context+": "+e.getMessage();
-        Logging.ingest.warn(message,e);
-        throw new ServiceInterruption(message,
-          e,
-          currentTime + interruptionRetryTime,
-          -1L,
-          3,
-          false);
+        final String message = "Server dropped connection during " + context + ": " + e.getMessage();
+        Logging.ingest.warn(message, e);
+        throw new ServiceInterruption(message, e, currentTime + interruptionRetryTime, -1L, 3, false);
       }
 
       // Other socket exceptions are service interruptions - but if we keep getting them, it means
-      // that a socket timeout is probably set too low to accept this particular document.  So
+      // that a socket timeout is probably set too low to accept this particular document. So
       // we retry for a while, then skip the document.
-      String message2 = "Socket exception during "+context+": "+e.getMessage();
-      Logging.ingest.warn(message2,e);
-      throw new ServiceInterruption(message2,
-        e,
-        currentTime + interruptionRetryTime,
-        currentTime + 20L * 60000L,
-        -1,
-        false);
+      final String message2 = "Socket exception during " + context + ": " + e.getMessage();
+      Logging.ingest.warn(message2, e);
+      throw new ServiceInterruption(message2, e, currentTime + interruptionRetryTime, currentTime + 20L * 60000L, -1, false);
     }
 
     // Otherwise, no idea what the trouble is, so presume that retries might fix it, skip the document/action otherwise
-    String message3 = "IO exception during "+context+": "+e.getMessage();
-    Logging.ingest.warn(message3,e);
-    throw new ServiceInterruption(message3,
-      e,
-      currentTime + interruptionRetryTime,
-      -1L,
-      3,
-      false);
+    final String message3 = "IO exception during " + context + ": " + e.getMessage();
+    Logging.ingest.warn(message3, e);
+    throw new ServiceInterruption(message3, e, currentTime + interruptionRetryTime, -1L, 3, false);
   }
 
   /**
-  * Post the input stream to ingest
-  *
-   * @param documentURI is the document's uri.
-   * @param document is the document structure to ingest.
-   * @param arguments are the configuration arguments to pass in the post.  Key is argument name, value is a list of the argument values.
+   * Post the input stream to ingest
+   *
+   * @param documentURI         is the document's uri.
+   * @param document            is the document structure to ingest.
+   * @param arguments           are the configuration arguments to pass in the post. Key is argument name, value is a list of the argument values.
    * @param authorityNameString is the name of the governing authority for this document's acls, or null if none.
-   * @param activities is the activities object, so we can report what's happening.   @return true if the ingestion was successful, or false if the ingestion is illegal.
-  * @throws ManifoldCFException
-  * @throws ServiceInterruption
-  */
-  public boolean indexPost(String documentURI,
-    RepositoryDocument document, Map<String,List<String>> arguments,
-    String authorityNameString, IOutputAddActivity activities)
-    throws ManifoldCFException, ServiceInterruption
-  {
+   * @param activities          is the activities object, so we can report what's happening. @return true if the ingestion was successful, or false if the ingestion is illegal.
+   * @throws ManifoldCFException
+   * @throws ServiceInterruption
+   */
+  public boolean indexPost(final String documentURI, final RepositoryDocument document, final Map<String, List<String>> arguments, final String authorityNameString,
+      final IOutputAddActivity activities) throws ManifoldCFException, ServiceInterruption {
     if (Logging.ingest.isDebugEnabled())
       Logging.ingest.debug("indexPost(): '" + documentURI + "'");
 
     // If the document is too long, reject it.
-    if (maxDocumentLength != null && document.getBinaryLength() > maxDocumentLength.longValue()){
-      activities.recordActivity(null,SolrConnector.INGEST_ACTIVITY,null,documentURI,activities.EXCLUDED_LENGTH,"Solr connector rejected document due to its big size: ('"+document.getBinaryLength()+"')");
+    if (maxDocumentLength != null && document.getBinaryLength() > maxDocumentLength.longValue()) {
+      activities.recordActivity(null, SolrConnector.INGEST_ACTIVITY, null, documentURI, activities.EXCLUDED_LENGTH,
+          "Solr connector rejected document due to its big size: ('" + document.getBinaryLength() + "')");
       return false;
     }
 
     // If not the right mime type, reject it.
-    // Note: this code added as part of CONNECTORS-1482 was incorrect!  Document filtering specified in the solr connector is always against the
-    // ORIGINAL mime type (which is what's in the document).  This why the checkMimeTypeIndexable second argument is always "true".
-    if ((includedMimeTypes !=null || excludedMimeTypes != null) && !checkMimeTypeIndexable(document.getMimeType(), true, includedMimeTypes, excludedMimeTypes)) {
-      activities.recordActivity(null,SolrConnector.INGEST_ACTIVITY,null,documentURI,activities.EXCLUDED_MIMETYPE,"Solr connector rejected document due to mime type restrictions: ("+document.getMimeType()+")");
+    // Note: this code added as part of CONNECTORS-1482 was incorrect! Document filtering specified in the solr connector is always against the
+    // ORIGINAL mime type (which is what's in the document). This why the checkMimeTypeIndexable second argument is always "true".
+    if ((includedMimeTypes != null || excludedMimeTypes != null) && !checkMimeTypeIndexable(document.getMimeType(), true, includedMimeTypes, excludedMimeTypes)) {
+      activities.recordActivity(null, SolrConnector.INGEST_ACTIVITY, null, documentURI, activities.EXCLUDED_MIMETYPE,
+          "Solr connector rejected document due to mime type restrictions: (" + document.getMimeType() + ")");
       return false;
     }
 
     // Convert the incoming acls that we know about to qualified forms, and reject the document if
     // we don't know how to deal with its acls
-    Map<String,String[]> aclsMap = new HashMap<String,String[]>();
-    Map<String,String[]> denyAclsMap = new HashMap<String,String[]>();
+    final Map<String, String[]> aclsMap = new HashMap<String, String[]>();
+    final Map<String, String[]> denyAclsMap = new HashMap<String, String[]>();
 
-    Iterator<String> aclTypes = document.securityTypesIterator();
-    while (aclTypes.hasNext())
-    {
-      String aclType = aclTypes.next();
-      aclsMap.put(aclType,convertACL(document.getSecurityACL(aclType),authorityNameString,activities));
-      denyAclsMap.put(aclType,convertACL(document.getSecurityDenyACL(aclType),authorityNameString,activities));
+    final Iterator<String> aclTypes = document.securityTypesIterator();
+    while (aclTypes.hasNext()) {
+      final String aclType = aclTypes.next();
+      aclsMap.put(aclType, convertACL(document.getSecurityACL(aclType), authorityNameString, activities));
+      denyAclsMap.put(aclType, convertACL(document.getSecurityDenyACL(aclType), authorityNameString, activities));
 
-      // Reject documents that have security we don't know how to deal with in the Solr plugin!!  Only safe thing to do.
-      if (!aclType.equals(RepositoryDocument.SECURITY_TYPE_DOCUMENT) &&
-        !aclType.equals(RepositoryDocument.SECURITY_TYPE_SHARE) &&
-        !aclType.startsWith(RepositoryDocument.SECURITY_TYPE_PARENT)){
-          activities.recordActivity(null,SolrConnector.INGEST_ACTIVITY,null,documentURI,activities.UNKNOWN_SECURITY,"Solr connector rejected document that has security info which Solr does not recognize: '"+aclType + "'");
-          return false;
+      // Reject documents that have security we don't know how to deal with in the Solr plugin!! Only safe thing to do.
+      if (!aclType.equals(RepositoryDocument.SECURITY_TYPE_DOCUMENT) && !aclType.equals(RepositoryDocument.SECURITY_TYPE_SHARE) && !aclType.startsWith(RepositoryDocument.SECURITY_TYPE_PARENT)) {
+        activities.recordActivity(null, SolrConnector.INGEST_ACTIVITY, null, documentURI, activities.UNKNOWN_SECURITY,
+            "Solr connector rejected document that has security info which Solr does not recognize: '" + aclType + "'");
+        return false;
       }
 
     }
 
-    try
-    {
-      IngestThread t = new IngestThread(documentURI,document,arguments,
-                                        aclsMap,denyAclsMap);
-      try
-      {
+    try {
+      final IngestThread t = new IngestThread(documentURI, document, arguments, aclsMap, denyAclsMap);
+      try {
         t.start();
         t.finishUp();
 
         if (t.getActivityCode() != null)
-          activities.recordActivity(t.getActivityStart(),SolrConnector.INGEST_ACTIVITY,t.getActivityBytes(),documentURI,t.getActivityCode(),t.getActivityDetails());
+          activities.recordActivity(t.getActivityStart(), SolrConnector.INGEST_ACTIVITY, t.getActivityBytes(), documentURI, t.getActivityCode(), t.getActivityDetails());
 
         return t.getRval();
-      }
-      catch (InterruptedException e)
-      {
+      } catch (final InterruptedException e) {
         t.interrupt();
-        throw new ManifoldCFException("Interrupted: "+e.getMessage(),ManifoldCFException.INTERRUPTED);
-      }
-      catch (SolrServerException e)
-      {
+        throw new ManifoldCFException("Interrupted: " + e.getMessage(), ManifoldCFException.INTERRUPTED);
+      } catch (final SolrServerException e) {
         if (t.getActivityCode() != null)
-          activities.recordActivity(t.getActivityStart(),SolrConnector.INGEST_ACTIVITY,t.getActivityBytes(),documentURI,t.getActivityCode(),t.getActivityDetails());
+          activities.recordActivity(t.getActivityStart(), SolrConnector.INGEST_ACTIVITY, t.getActivityBytes(), documentURI, t.getActivityCode(), t.getActivityDetails());
+        throw e;
+      } catch (final SolrException e) {
+        if (t.getActivityCode() != null)
+          activities.recordActivity(t.getActivityStart(), SolrConnector.INGEST_ACTIVITY, t.getActivityBytes(), documentURI, t.getActivityCode(), t.getActivityDetails());
+        throw e;
+      } catch (final RuntimeException e) {
+        if (t.getActivityCode() != null)
+          activities.recordActivity(t.getActivityStart(), SolrConnector.INGEST_ACTIVITY, t.getActivityBytes(), documentURI, t.getActivityCode(), t.getActivityDetails());
+        throw e;
+      } catch (final IOException e) {
+        if (t.getActivityCode() != null)
+          activities.recordActivity(t.getActivityStart(), SolrConnector.INGEST_ACTIVITY, t.getActivityBytes(), documentURI, t.getActivityCode(), t.getActivityDetails());
         throw e;
       }
-      catch (SolrException e)
-      {
-        if (t.getActivityCode() != null)
-          activities.recordActivity(t.getActivityStart(),SolrConnector.INGEST_ACTIVITY,t.getActivityBytes(),documentURI,t.getActivityCode(),t.getActivityDetails());
-        throw e;
-      }
-      catch (RuntimeException e)
-      {
-        if (t.getActivityCode() != null)
-          activities.recordActivity(t.getActivityStart(),SolrConnector.INGEST_ACTIVITY,t.getActivityBytes(),documentURI,t.getActivityCode(),t.getActivityDetails());
-        throw e;
-      }
-      catch (IOException e)
-      {
-        if (t.getActivityCode() != null)
-          activities.recordActivity(t.getActivityStart(),SolrConnector.INGEST_ACTIVITY,t.getActivityBytes(),documentURI,t.getActivityCode(),t.getActivityDetails());
-        throw e;
-      }
-    }
-    catch (SolrServerException e)
-    {
-      handleSolrServerException(e, "indexing "+documentURI);
+    } catch (final SolrServerException e) {
+      handleSolrServerException(e, "indexing " + documentURI);
       return false;
-    }
-    catch (SolrException e)
-    {
-      handleSolrException(e, "indexing "+documentURI);
+    } catch (final SolrException e) {
+      handleSolrException(e, "indexing " + documentURI);
       return false;
-    }
-    catch (RuntimeException e)
-    {
-      handleRuntimeException(e, "indexing "+documentURI);
+    } catch (final RuntimeException e) {
+      handleRuntimeException(e, "indexing " + documentURI);
       return false;
-    }
-    catch (IOException ioe)
-    {
-      handleIOException(ioe, "indexing "+documentURI);
+    } catch (final IOException ioe) {
+      handleIOException(ioe, "indexing " + documentURI);
       return false;
     }
 
   }
 
-  /** Post a check request.
-  */
-  public void checkPost()
-    throws ManifoldCFException, ServiceInterruption
-  {
+  /**
+   * Post a check request.
+   */
+  public void checkPost() throws ManifoldCFException, ServiceInterruption {
     if (Logging.ingest.isDebugEnabled())
       Logging.ingest.debug("checkPost()");
 
     // Open a socket to ingest, and to the response stream to get the post result
-    try
-    {
-      StatusThread t = new StatusThread();
-      try
-      {
+    try {
+      final StatusThread t = new StatusThread();
+      try {
         t.start();
         t.finishUp();
         return;
-      }
-      catch (InterruptedException e)
-      {
+      } catch (final InterruptedException e) {
         t.interrupt();
-        throw new ManifoldCFException("Interrupted: "+e.getMessage(),ManifoldCFException.INTERRUPTED);
+        throw new ManifoldCFException("Interrupted: " + e.getMessage(), ManifoldCFException.INTERRUPTED);
       }
-    }
-    catch (SolrServerException e)
-    {
+    } catch (final SolrServerException e) {
       handleSolrServerException(e, "check");
       return;
-    }
-    catch (SolrException e)
-    {
+    } catch (final SolrException e) {
       handleSolrException(e, "check");
       return;
-    }
-    catch (RuntimeException e)
-    {
+    } catch (final RuntimeException e) {
       handleRuntimeException(e, "check");
       return;
-    }
-    catch (IOException ioe)
-    {
+    } catch (final IOException ioe) {
       handleIOException(ioe, "check");
       return;
     }
 
   }
 
-  /** Post a delete request.
-  *@param documentURI is the document's URI.
-  */
-  public void deletePost(String documentURI, IOutputRemoveActivity activities)
-    throws ManifoldCFException, ServiceInterruption
-  {
+  /**
+   * Post a delete request.
+   *
+   * @param documentURI is the document's URI.
+   */
+  public void deletePost(final String documentURI, final IOutputRemoveActivity activities) throws ManifoldCFException, ServiceInterruption {
     if (Logging.ingest.isDebugEnabled())
       Logging.ingest.debug("deletePost(): '" + documentURI + "'");
 
-    try
-    {
-      DeleteThread t = new DeleteThread(documentURI);
-      try
-      {
+    try {
+      final DeleteThread t = new DeleteThread(documentURI);
+      try {
         t.start();
         t.finishUp();
 
         if (t.getActivityCode() != null)
-          activities.recordActivity(t.getActivityStart(),SolrConnector.REMOVE_ACTIVITY,null,documentURI,t.getActivityCode(),t.getActivityDetails());
+          activities.recordActivity(t.getActivityStart(), SolrConnector.REMOVE_ACTIVITY, null, documentURI, t.getActivityCode(), t.getActivityDetails());
 
         return;
-      }
-      catch (InterruptedException e)
-      {
+      } catch (final InterruptedException e) {
         t.interrupt();
-        throw new ManifoldCFException("Interrupted: "+e.getMessage(),ManifoldCFException.INTERRUPTED);
-      }
-      catch (SolrServerException e)
-      {
+        throw new ManifoldCFException("Interrupted: " + e.getMessage(), ManifoldCFException.INTERRUPTED);
+      } catch (final SolrServerException e) {
         if (t.getActivityCode() != null)
-          activities.recordActivity(t.getActivityStart(),SolrConnector.REMOVE_ACTIVITY,null,documentURI,t.getActivityCode(),t.getActivityDetails());
+          activities.recordActivity(t.getActivityStart(), SolrConnector.REMOVE_ACTIVITY, null, documentURI, t.getActivityCode(), t.getActivityDetails());
+        throw e;
+      } catch (final SolrException e) {
+        if (t.getActivityCode() != null)
+          activities.recordActivity(t.getActivityStart(), SolrConnector.REMOVE_ACTIVITY, null, documentURI, t.getActivityCode(), t.getActivityDetails());
+        throw e;
+      } catch (final RuntimeException e) {
+        if (t.getActivityCode() != null)
+          activities.recordActivity(t.getActivityStart(), SolrConnector.REMOVE_ACTIVITY, null, documentURI, t.getActivityCode(), t.getActivityDetails());
+        throw e;
+      } catch (final IOException e) {
+        if (t.getActivityCode() != null)
+          activities.recordActivity(t.getActivityStart(), SolrConnector.REMOVE_ACTIVITY, null, documentURI, t.getActivityCode(), t.getActivityDetails());
         throw e;
       }
-      catch (SolrException e)
-      {
-        if (t.getActivityCode() != null)
-          activities.recordActivity(t.getActivityStart(),SolrConnector.REMOVE_ACTIVITY,null,documentURI,t.getActivityCode(),t.getActivityDetails());
-        throw e;
-      }
-      catch (RuntimeException e)
-      {
-        if (t.getActivityCode() != null)
-          activities.recordActivity(t.getActivityStart(),SolrConnector.REMOVE_ACTIVITY,null,documentURI,t.getActivityCode(),t.getActivityDetails());
-        throw e;
-      }
-      catch (IOException e)
-      {
-        if (t.getActivityCode() != null)
-          activities.recordActivity(t.getActivityStart(),SolrConnector.REMOVE_ACTIVITY,null,documentURI,t.getActivityCode(),t.getActivityDetails());
-        throw e;
-      }
-    }
-    catch (SolrServerException e)
-    {
+    } catch (final SolrServerException e) {
       handleSolrServerException(e, "delete");
       return;
-    }
-    catch (SolrException e)
-    {
+    } catch (final SolrException e) {
       handleSolrException(e, "delete");
       return;
-    }
-    catch (RuntimeException e)
-    {
+    } catch (final RuntimeException e) {
       handleRuntimeException(e, "delete");
       return;
-    }
-    catch (IOException ioe)
-    {
+    } catch (final IOException ioe) {
       handleIOException(ioe, "delete");
       return;
     }
@@ -836,8 +632,7 @@
   }
 
   private final static Set<String> acceptableMimeTypes = new HashSet<String>();
-  static
-  {
+  static {
     acceptableMimeTypes.add("text/plain;charset=utf-8");
     acceptableMimeTypes.add("text/plain;charset=ascii");
     acceptableMimeTypes.add("text/plain;charset=us-ascii");
@@ -847,17 +642,14 @@
     acceptableMimeTypes.add("text/plain");
   }
 
-  public static boolean checkMimeTypeIndexable(final String mimeType, final boolean useExtractUpdateHandler,
-    final Set<String> includedMimeTypes, final Set<String> excludedMimeTypes)
-  {
-    final String lowerMimeType = (mimeType==null)?null:mimeType.toLowerCase(Locale.ROOT);
-    if (useExtractUpdateHandler)
-    {
+  public static boolean checkMimeTypeIndexable(final String mimeType, final boolean useExtractUpdateHandler, final Set<String> includedMimeTypes, final Set<String> excludedMimeTypes) {
+    final String lowerMimeType = (mimeType == null) ? null : mimeType.toLowerCase(Locale.ROOT);
+    if (useExtractUpdateHandler) {
       // Strip the charset off for this check
-      int index = lowerMimeType == null ? -1 : lowerMimeType.indexOf(";");
+      final int index = lowerMimeType == null ? -1 : lowerMimeType.indexOf(";");
       final String checkMimeType;
       if (index != -1) {
-        checkMimeType = lowerMimeType.substring(0,index);
+        checkMimeType = lowerMimeType.substring(0, index);
       } else {
         checkMimeType = lowerMimeType;
       }
@@ -870,22 +662,20 @@
     return acceptableMimeTypes.contains(lowerMimeType);
   }
 
-  /** Convert an unqualified ACL to qualified form.
-  * @param acl is the initial, unqualified ACL.
-  * @param authorityNameString is the name of the governing authority for this document's acls, or null if none.
-  * @param activities is the activities object, so we can report what's happening.
-  * @return the modified ACL.
-  */
-  protected static String[] convertACL(String[] acl, String authorityNameString, IOutputAddActivity activities)
-    throws ManifoldCFException
-  {
-    if (acl != null)
-    {
-      String[] rval = new String[acl.length];
+  /**
+   * Convert an unqualified ACL to qualified form.
+   *
+   * @param acl                 is the initial, unqualified ACL.
+   * @param authorityNameString is the name of the governing authority for this document's acls, or null if none.
+   * @param activities          is the activities object, so we can report what's happening.
+   * @return the modified ACL.
+   */
+  protected static String[] convertACL(final String[] acl, final String authorityNameString, final IOutputAddActivity activities) throws ManifoldCFException {
+    if (acl != null) {
+      final String[] rval = new String[acl.length];
       int i = 0;
-      while (i < rval.length)
-      {
-        rval[i] = activities.qualifyAccessToken(authorityNameString,acl[i]);
+      while (i < rval.length) {
+        rval[i] = activities.qualifyAccessToken(authorityNameString, acl[i]);
         i++;
       }
       return rval;
@@ -894,68 +684,60 @@
   }
 
   /** Write a field */
-  protected static void writeField(ModifiableSolrParams out, String fieldName, String[] fieldValues)
-  {
+  protected static void writeField(final ModifiableSolrParams out, final String fieldName, final String[] fieldValues) {
     out.add(fieldName, fieldValues);
   }
 
   /** Write a field */
-  protected static void writeField(ModifiableSolrParams out, String fieldName, List<String> fieldValues)
-  {
-    String[] values = new String[fieldValues.size()];
+  protected static void writeField(final ModifiableSolrParams out, final String fieldName, final List<String> fieldValues) {
+    final String[] values = new String[fieldValues.size()];
     int i = 0;
-    for (String fieldValue : fieldValues) {
+    for (final String fieldValue : fieldValues) {
       values[i++] = fieldValue;
     }
     writeField(out, fieldName, values);
   }
 
   /** Write a field */
-  protected static void writeField(ModifiableSolrParams out, String fieldName, String fieldValue)
-  {
+  protected static void writeField(final ModifiableSolrParams out, final String fieldName, final String fieldValue) {
     out.add(fieldName, fieldValue);
   }
 
   /** Output an acl level */
-  protected void writeACLs(ModifiableSolrParams out, String aclType, String[] acl, String[] denyAcl)
-  {
-    String metadataACLName = LITERAL + allowAttributeName + aclType;
-    for (int i = 0; i < acl.length; i++)
-    {
-      writeField(out,metadataACLName,acl[i]);
+  protected void writeACLs(final ModifiableSolrParams out, final String aclType, final String[] acl, final String[] denyAcl) {
+    final String metadataACLName = LITERAL + allowAttributeName + aclType;
+    for (int i = 0; i < acl.length; i++) {
+      writeField(out, metadataACLName, acl[i]);
     }
-    String metadataDenyACLName = LITERAL + denyAttributeName + aclType;
-    for (int i = 0; i < denyAcl.length; i++)
-    {
-      writeField(out,metadataDenyACLName,denyAcl[i]);
+    final String metadataDenyACLName = LITERAL + denyAttributeName + aclType;
+    for (int i = 0; i < denyAcl.length; i++) {
+      writeField(out, metadataDenyACLName, denyAcl[i]);
     }
   }
 
   /**
-    * Output an acl level in a SolrInputDocument
-    */
-  protected void writeACLsInSolrDoc( SolrInputDocument inputDoc, String aclType, String[] acl, String[] denyAcl )
-  {
-    String metadataACLName = allowAttributeName + aclType;
-    inputDoc.addField( metadataACLName, acl );
+   * Output an acl level in a SolrInputDocument
+   */
+  protected void writeACLsInSolrDoc(final SolrInputDocument inputDoc, final String aclType, final String[] acl, final String[] denyAcl) {
+    final String metadataACLName = allowAttributeName + aclType;
+    inputDoc.addField(metadataACLName, acl);
 
-    String metadataDenyACLName = denyAttributeName + aclType;
-    inputDoc.addField( metadataDenyACLName, denyAcl );
+    final String metadataDenyACLName = denyAttributeName + aclType;
+    inputDoc.addField(metadataDenyACLName, denyAcl);
   }
 
-  /** Killable thread that does ingestions.
-  * Java 1.5 stopped permitting thread interruptions to abort socket waits.  As a result, it is impossible to get threads to shutdown cleanly that are doing
-  * such waits.  So, the places where this happens are segregated in their own threads so that they can be just abandoned.
-  *
-  * This thread does a single document ingestion.
-  */
-  protected class IngestThread extends java.lang.Thread
-  {
+  /**
+   * Killable thread that does ingestions. Java 1.5 stopped permitting thread interruptions to abort socket waits. As a result, it is impossible to get threads to shutdown cleanly that are doing such
+   * waits. So, the places where this happens are segregated in their own threads so that they can be just abandoned.
+   *
+   * This thread does a single document ingestion.
+   */
+  protected class IngestThread extends java.lang.Thread {
     protected final String documentURI;
     protected final RepositoryDocument document;
-    protected final Map<String,List<String>> arguments;
-    protected final Map<String,String[]> aclsMap;
-    protected final Map<String,String[]> denyAclsMap;
+    protected final Map<String, List<String>> arguments;
+    protected final Map<String, String[]> aclsMap;
+    protected final Map<String, String[]> denyAclsMap;
 
     protected Long activityStart = null;
     protected Long activityBytes = null;
@@ -965,10 +747,8 @@
     protected boolean readFromDocumentStreamYet = false;
     protected boolean rval = false;
 
-    public IngestThread(String documentURI, RepositoryDocument document,
-      Map<String, List<String>> arguments,
-      Map<String,String[]> aclsMap, Map<String,String[]> denyAclsMap)
-    {
+    public IngestThread(final String documentURI, final RepositoryDocument document, final Map<String, List<String>> arguments, final Map<String, String[]> aclsMap,
+        final Map<String, String[]> denyAclsMap) {
       super();
       setDaemon(true);
       this.documentURI = documentURI;
@@ -978,50 +758,39 @@
       this.denyAclsMap = denyAclsMap;
     }
 
-    public void run()
-    {
-      long length = document.getBinaryLength();
-      InputStream is = document.getBinaryStream();
-      String contentType = document.getMimeType();
-      String contentName = document.getFileName();
+    @Override
+    public void run() {
+      final long length = document.getBinaryLength();
+      final InputStream is = document.getBinaryStream();
+      final String contentType = document.getMimeType();
+      final String contentName = document.getFileName();
 
-      try
-      {
+      try {
         // Do the operation!
-        long fullStartTime = System.currentTimeMillis();
+        final long fullStartTime = System.currentTimeMillis();
 
         // Open a socket to ingest, and to the response stream to get the post result
-        try
-        {
+        try {
           SolrInputDocument currentSolrDoc = new SolrInputDocument();
-          ContentStreamUpdateRequest contentStreamUpdateRequest = new ContentStreamUpdateRequest(postUpdateAction);
-          if ( useExtractUpdateHandler )
-          {
-            buildExtractUpdateHandlerRequest( length, is, contentType, (contentName==null || contentName.length()==0)?"docname":contentName,
-              contentStreamUpdateRequest );
-          }
-          else
-          {
-            currentSolrDoc = buildSolrDocument( length, is );
+          final ContentStreamUpdateRequest contentStreamUpdateRequest = new ContentStreamUpdateRequest(postUpdateAction);
+          if (useExtractUpdateHandler) {
+            buildExtractUpdateHandlerRequest(length, is, contentType, (contentName == null || contentName.length() == 0) ? "docname" : contentName, contentStreamUpdateRequest);
+          } else {
+            currentSolrDoc = buildSolrDocument(length, is);
           }
 
           // Fire off the request.
           // Note: I need to know whether the document has been permanently rejected or not, but we currently have
-          // no means to determine that.  Analysis of SolrServerExceptions that have been thrown is likely needed.
-          try
-          {
+          // no means to determine that. Analysis of SolrServerExceptions that have been thrown is likely needed.
+          try {
             readFromDocumentStreamYet = true;
             UpdateResponse response;
-            if ( useExtractUpdateHandler )
-            {
+            if (useExtractUpdateHandler) {
               response = contentStreamUpdateRequest.process(solrServer);
-            }
-            else
-            {
+            } else {
               final ModifiableSolrParams params = new ModifiableSolrParams();
               // Write the arguments
-              for (final String name : arguments.keySet())
-              {
+              for (final String name : arguments.keySet()) {
                 final List<String> values = arguments.get(name);
                 writeField(params, name, values);
               }
@@ -1031,7 +800,7 @@
               if (commitWithin != null) {
                 req.setCommitWithin(Integer.parseInt(commitWithin));
               }
-              response =  req.process(solrServer);
+              response = req.process(solrServer);
             }
 
             // Successful completion
@@ -1042,43 +811,33 @@
 
             rval = true;
             return;
-          }
-          catch (SolrServerException e)
-          {
+          } catch (final SolrServerException e) {
             // Log what happened to us
             activityStart = new Long(fullStartTime);
             activityBytes = new Long(length);
-            activityDetails = e.getMessage() +
-              ((e.getCause() != null)?": "+e.getCause().getMessage():"");
+            activityDetails = e.getMessage() + ((e.getCause() != null) ? ": " + e.getCause().getMessage() : "");
 
             // Broken pipe exceptions we log specially because they usually mean
             // Solr has rejected the document, and the user will want to know that.
-            if (e.getCause() != null && e.getCause().getClass().getName().equals("java.net.SocketException") &&
-              (activityDetails.toLowerCase(Locale.ROOT).indexOf("broken pipe") != -1 ||
-                activityDetails.toLowerCase(Locale.ROOT).indexOf("connection reset") != -1 ||
-                activityDetails.toLowerCase(Locale.ROOT).indexOf("target server failed to respond") != -1))
+            if (e.getCause() != null && e.getCause().getClass().getName().equals("java.net.SocketException") && (activityDetails.toLowerCase(Locale.ROOT).indexOf("broken pipe") != -1
+                || activityDetails.toLowerCase(Locale.ROOT).indexOf("connection reset") != -1 || activityDetails.toLowerCase(Locale.ROOT).indexOf("target server failed to respond") != -1))
               activityCode = "SOLRREJECT";
             else
               activityCode = e.getClass().getSimpleName().toUpperCase(Locale.ROOT);
 
             // Rethrow; will interpret at a higher level
             throw e;
-          }
-          catch (SolrException e)
-          {
+          } catch (final SolrException e) {
             // Log what happened to us
             activityStart = new Long(fullStartTime);
             activityBytes = new Long(length);
             activityCode = Integer.toString(e.code());
-            activityDetails = e.getMessage() +
-              ((e.getCause() != null)?": "+e.getCause().getMessage():"");
+            activityDetails = e.getMessage() + ((e.getCause() != null) ? ": " + e.getCause().getMessage() : "");
 
             // Rethrow; we'll interpret at the next level
             throw e;
           }
-        }
-        catch (IOException ioe)
-        {
+        } catch (final IOException ioe) {
           if ((ioe instanceof InterruptedIOException) && (!(ioe instanceof java.net.SocketTimeoutException)))
             return;
 
@@ -1087,307 +846,262 @@
           activityDetails = ioe.getMessage();
 
           // Log the error
-          Logging.ingest.warn("Error indexing into Solr: "+ioe.getMessage(),ioe);
+          Logging.ingest.warn("Error indexing into Solr: " + ioe.getMessage(), ioe);
 
           throw ioe;
         }
-      }
-      catch (Throwable e)
-      {
+      } catch (final Throwable e) {
         this.exception = e;
       }
     }
 
-    private SolrInputDocument buildSolrDocument( long length, InputStream is )
-      throws IOException
-    {
-      SolrInputDocument outputDoc = new SolrInputDocument();
+    private SolrInputDocument buildSolrDocument(final long length, final InputStream is) throws IOException {
+      final SolrInputDocument outputDoc = new SolrInputDocument();
 
       // Write the id field
-      outputDoc.addField( idAttributeName, documentURI );
+      outputDoc.addField(idAttributeName, documentURI);
 
-      if (contentAttributeName != null)
-      {
-        // Copy the content into a string.  This is a bad thing to do, but we have no choice given SolrJ architecture at this time.
+      if (contentAttributeName != null) {
+        // Copy the content into a string. This is a bad thing to do, but we have no choice given SolrJ architecture at this time.
         // We enforce a size limit upstream.
-        Reader r = new InputStreamReader(is, Consts.UTF_8);
-        StringBuilder sb = new StringBuilder((int)length);
-        char[] buffer = new char[65536];
-        while (true)
-        {
-          int amt = r.read(buffer,0,buffer.length);
+        final Reader r = new InputStreamReader(is, Consts.UTF_8);
+        final StringBuilder sb = new StringBuilder((int) length);
+        final char[] buffer = new char[65536];
+        while (true) {
+          final int amt = r.read(buffer, 0, buffer.length);
           if (amt == -1)
             break;
-          sb.append(buffer,0,amt);
+          sb.append(buffer, 0, amt);
         }
-        outputDoc.addField( contentAttributeName, sb.toString() );
+        outputDoc.addField(contentAttributeName, sb.toString());
       }
 
       // Write the rest of the attributes
-      if ( originalSizeAttributeName != null )
-      {
-        Long size = document.getOriginalSize();
-        if ( size != null )
-        {
-          outputDoc.addField( originalSizeAttributeName, size.toString() );
+      if (originalSizeAttributeName != null) {
+        final Long size = document.getOriginalSize();
+        if (size != null) {
+          outputDoc.addField(originalSizeAttributeName, size.toString());
         }
       }
-      if ( modifiedDateAttributeName != null )
-      {
-        Date date = document.getModifiedDate();
-        if ( date != null )
-        {
-          outputDoc.addField( modifiedDateAttributeName, DateParser.formatISO8601Date( date ) );
+      if (modifiedDateAttributeName != null) {
+        final Date date = document.getModifiedDate();
+        if (date != null) {
+          outputDoc.addField(modifiedDateAttributeName, DateParser.formatISO8601Date(date));
         }
       }
-      if ( createdDateAttributeName != null )
-      {
-        Date date = document.getCreatedDate();
-        if ( date != null )
-        {
-          outputDoc.addField( createdDateAttributeName, DateParser.formatISO8601Date( date ) );
+      if (createdDateAttributeName != null) {
+        final Date date = document.getCreatedDate();
+        if (date != null) {
+          outputDoc.addField(createdDateAttributeName, DateParser.formatISO8601Date(date));
         }
 
       }
-      if ( indexedDateAttributeName != null )
-      {
-        Date date = document.getIndexingDate();
-        if ( date != null )
-        {
-          outputDoc.addField( indexedDateAttributeName, DateParser.formatISO8601Date( date ) );
+      if (indexedDateAttributeName != null) {
+        final Date date = document.getIndexingDate();
+        if (date != null) {
+          outputDoc.addField(indexedDateAttributeName, DateParser.formatISO8601Date(date));
         }
       }
-      if ( fileNameAttributeName != null )
-      {
-        String fileName = document.getFileName();
-        if ( !StringUtils.isBlank(fileName) )
-        {
-          outputDoc.addField( fileNameAttributeName, fileName );
+      if (fileNameAttributeName != null) {
+        final String fileName = document.getFileName();
+        if (!StringUtils.isBlank(fileName)) {
+          outputDoc.addField(fileNameAttributeName, fileName);
         }
       }
-      if ( mimeTypeAttributeName != null )
-      {
-        String mimeType = document.getMimeType();
-        if ( !StringUtils.isBlank(mimeType) )
-        {
-          outputDoc.addField( mimeTypeAttributeName, mimeType );
+      if (mimeTypeAttributeName != null) {
+        final String mimeType = document.getMimeType();
+        if (!StringUtils.isBlank(mimeType)) {
+          outputDoc.addField(mimeTypeAttributeName, mimeType);
         }
       }
 
-      Iterator<String> typeIterator = aclsMap.keySet().iterator();
-      while (typeIterator.hasNext())
-      {
-        String aclType = typeIterator.next();
-        writeACLsInSolrDoc(outputDoc,aclType,aclsMap.get(aclType),denyAclsMap.get(aclType));
+      final Iterator<String> typeIterator = aclsMap.keySet().iterator();
+      while (typeIterator.hasNext()) {
+        final String aclType = typeIterator.next();
+        writeACLsInSolrDoc(outputDoc, aclType, aclsMap.get(aclType), denyAclsMap.get(aclType));
       }
 
       // Write the metadata, each in a field by itself
-      buildSolrParamsFromMetadata( outputDoc );
+      buildSolrParamsFromMetadata(outputDoc);
 
       return outputDoc;
     }
 
-    private void buildExtractUpdateHandlerRequest( long length, InputStream is, String contentType,
-      String contentName,
-      ContentStreamUpdateRequest contentStreamUpdateRequest )
-      throws IOException
-    {
-      ModifiableSolrParams out = new ModifiableSolrParams();
-      Logging.ingest.debug("Solr: Writing document '"+documentURI);
+    private void buildExtractUpdateHandlerRequest(final long length, final InputStream is, final String contentType, final String contentName,
+        final ContentStreamUpdateRequest contentStreamUpdateRequest) throws IOException {
+      final ModifiableSolrParams out = new ModifiableSolrParams();
+      Logging.ingest.debug("Solr: Writing document '" + documentURI);
 
       // Write the id field
-      writeField(out,LITERAL+idAttributeName,documentURI);
+      writeField(out, LITERAL + idAttributeName, documentURI);
       // Write the rest of the attributes
-      if (originalSizeAttributeName != null)
-      {
-        Long size = document.getOriginalSize();
+      if (originalSizeAttributeName != null) {
+        final Long size = document.getOriginalSize();
         if (size != null)
           // Write value
-          writeField(out,LITERAL+originalSizeAttributeName,size.toString());
+          writeField(out, LITERAL + originalSizeAttributeName, size.toString());
       }
-      if (modifiedDateAttributeName != null)
-      {
-        Date date = document.getModifiedDate();
+      if (modifiedDateAttributeName != null) {
+        final Date date = document.getModifiedDate();
         if (date != null)
           // Write value
-          writeField(out,LITERAL+modifiedDateAttributeName,DateParser.formatISO8601Date(date));
+          writeField(out, LITERAL + modifiedDateAttributeName, DateParser.formatISO8601Date(date));
       }
-      if (createdDateAttributeName != null)
-      {
-        Date date = document.getCreatedDate();
+      if (createdDateAttributeName != null) {
+        final Date date = document.getCreatedDate();
         if (date != null)
           // Write value
-          writeField(out,LITERAL+createdDateAttributeName,DateParser.formatISO8601Date(date));
+          writeField(out, LITERAL + createdDateAttributeName, DateParser.formatISO8601Date(date));
       }
-      if (indexedDateAttributeName != null)
-      {
-        Date date = document.getIndexingDate();
+      if (indexedDateAttributeName != null) {
+        final Date date = document.getIndexingDate();
         if (date != null)
           // Write value
-          writeField(out,LITERAL+indexedDateAttributeName,DateParser.formatISO8601Date(date));
+          writeField(out, LITERAL + indexedDateAttributeName, DateParser.formatISO8601Date(date));
       }
-      if (fileNameAttributeName != null)
-      {
-        String fileName = document.getFileName();
+      if (fileNameAttributeName != null) {
+        final String fileName = document.getFileName();
         if (!StringUtils.isBlank(fileName))
-          writeField(out,LITERAL+fileNameAttributeName,fileName);
+          writeField(out, LITERAL + fileNameAttributeName, fileName);
       }
-      if (mimeTypeAttributeName != null)
-      {
-        String mimeType = document.getMimeType();
+      if (mimeTypeAttributeName != null) {
+        final String mimeType = document.getMimeType();
         if (!StringUtils.isBlank(mimeType))
-          writeField(out,LITERAL+mimeTypeAttributeName,mimeType);
+          writeField(out, LITERAL + mimeTypeAttributeName, mimeType);
       }
 
       // Write the access token information
       // Both maps have the same keys.
-      Iterator<String> typeIterator = aclsMap.keySet().iterator();
-      while (typeIterator.hasNext())
-      {
-        String aclType = typeIterator.next();
-        writeACLs(out,aclType,aclsMap.get(aclType),denyAclsMap.get(aclType));
+      final Iterator<String> typeIterator = aclsMap.keySet().iterator();
+      while (typeIterator.hasNext()) {
+        final String aclType = typeIterator.next();
+        writeACLs(out, aclType, aclsMap.get(aclType), denyAclsMap.get(aclType));
       }
 
       // Write the arguments
-      for (String name : arguments.keySet())
-      {
-        List<String> values = arguments.get(name);
-        writeField(out,name,values);
+      for (final String name : arguments.keySet()) {
+        final List<String> values = arguments.get(name);
+        writeField(out, name, values);
       }
 
       // Write the metadata, each in a field by itself
       buildSolrParamsFromMetadata(out);
 
       // These are unnecessary now in the case of non-solrcloud setups, because we overrode the SolrJ posting method to use multipart.
-      //writeField(out,LITERAL+"stream_size",String.valueOf(length));
-      //writeField(out,LITERAL+"stream_name",document.getFileName());
+      // writeField(out,LITERAL+"stream_size",String.valueOf(length));
+      // writeField(out,LITERAL+"stream_name",document.getFileName());
 
       // General hint for Tika
       if (!StringUtils.isBlank(document.getFileName()))
-        writeField(out,"resource.name",document.getFileName());
+        writeField(out, "resource.name", document.getFileName());
 
       // Write the commitWithin parameter
       if (commitWithin != null)
-        writeField(out,COMMITWITHIN_METADATA,commitWithin);
+        writeField(out, COMMITWITHIN_METADATA, commitWithin);
 
       contentStreamUpdateRequest.setParams(out);
 
-      contentStreamUpdateRequest.addContentStream(new RepositoryDocumentStream(is,length,contentType,contentName));
+      contentStreamUpdateRequest.addContentStream(new RepositoryDocumentStream(is, length, contentType, contentName));
 
-      Logging.ingest.debug("Solr: Done writing '"+documentURI+"'");
+      Logging.ingest.debug("Solr: Done writing '" + documentURI + "'");
     }
 
     /**
-      * builds the solr parameter maps for the update request.
-      * For each mapping expressed is applied the renaming for the metadata field name.
-      * If we set to keep all the metadata, the metadata non present in the mapping will be kept with their original names.
-      * In the other case ignored
-      * @param out
-      * @throws IOException
-      */
-    private void buildSolrParamsFromMetadata(ModifiableSolrParams out) throws IOException
-    {
-      Iterator<String> iter = document.getFields();
-      while (iter.hasNext())
-      {
-        String originalFieldName = iter.next();
-        String fieldName = makeSafeLuceneField(originalFieldName);
-        Logging.ingest.debug("Solr: Saw field '"+originalFieldName+"'; converted to '"+fieldName+"'");
+     * builds the solr parameter maps for the update request. For each mapping expressed is applied the renaming for the metadata field name. If we set to keep all the metadata, the metadata non
+     * present in the mapping will be kept with their original names. In the other case ignored
+     *
+     * @param out
+     * @throws IOException
+     */
+    private void buildSolrParamsFromMetadata(final ModifiableSolrParams out) throws IOException {
+      final Iterator<String> iter = document.getFields();
+      while (iter.hasNext()) {
+        final String originalFieldName = iter.next();
+        final String fieldName = makeSafeLuceneField(originalFieldName);
+        Logging.ingest.debug("Solr: Saw field '" + originalFieldName + "'; converted to '" + fieldName + "'");
         applySingleMapping(originalFieldName, out, fieldName);
       }
     }
 
-    private void buildSolrParamsFromMetadata(SolrInputDocument outputDocument) throws IOException
-    {
-      Iterator<String> iter = document.getFields();
-      while (iter.hasNext())
-      {
-        String originalFieldName = iter.next();
-        String fieldName = makeSafeLuceneField(originalFieldName);
+    private void buildSolrParamsFromMetadata(final SolrInputDocument outputDocument) throws IOException {
+      final Iterator<String> iter = document.getFields();
+      while (iter.hasNext()) {
+        final String originalFieldName = iter.next();
+        final String fieldName = makeSafeLuceneField(originalFieldName);
         applySingleMapping(originalFieldName, outputDocument, fieldName);
       }
     }
 
-    private void applySingleMapping(String originalFieldName, ModifiableSolrParams out, String newFieldName) throws IOException {
-      if(newFieldName != null && !newFieldName.isEmpty()) {
+    private void applySingleMapping(final String originalFieldName, final ModifiableSolrParams out, String newFieldName) throws IOException {
+      if (newFieldName != null && !newFieldName.isEmpty()) {
         if (newFieldName.toLowerCase(Locale.ROOT).equals(idAttributeName.toLowerCase(Locale.ROOT))) {
           newFieldName = ID_METADATA;
         }
-        String[] values = document.getFieldAsStrings(originalFieldName);
-        writeField(out,LITERAL+newFieldName,values);
+        final String[] values = document.getFieldAsStrings(originalFieldName);
+        writeField(out, LITERAL + newFieldName, values);
       }
     }
 
-    private void applySingleMapping(String originalFieldName, SolrInputDocument outputDocument, String newFieldName) throws IOException {
-      if(newFieldName != null && !newFieldName.isEmpty()) {
+    private void applySingleMapping(final String originalFieldName, final SolrInputDocument outputDocument, String newFieldName) throws IOException {
+      if (newFieldName != null && !newFieldName.isEmpty()) {
         if (newFieldName.toLowerCase(Locale.ROOT).equals(idAttributeName.toLowerCase(Locale.ROOT))) {
           newFieldName = ID_METADATA;
         }
-        String[] values = document.getFieldAsStrings(originalFieldName);
-        outputDocument.addField( newFieldName, values );
+        final String[] values = document.getFieldAsStrings(originalFieldName);
+        outputDocument.addField(newFieldName, values);
       }
     }
 
-    public void finishUp()
-      throws InterruptedException, SolrServerException, IOException
-    {
+    public void finishUp() throws InterruptedException, SolrServerException, IOException {
       join();
 
-      Throwable thr = exception;
-      if (thr != null)
-      {
+      final Throwable thr = exception;
+      if (thr != null) {
         if (thr instanceof SolrServerException)
-          throw (SolrServerException)thr;
+          throw (SolrServerException) thr;
         if (thr instanceof IOException)
-          throw (IOException)thr;
+          throw (IOException) thr;
         if (thr instanceof RuntimeException)
-          throw (RuntimeException)thr;
+          throw (RuntimeException) thr;
         if (thr instanceof Error)
-          throw (Error)thr;
+          throw (Error) thr;
         else
-          throw new RuntimeException("Unexpected exception type: "+thr.getClass().getName()+": "+thr.getMessage(),thr);
+          throw new RuntimeException("Unexpected exception type: " + thr.getClass().getName() + ": " + thr.getMessage(), thr);
       }
     }
 
-    public Long getActivityStart()
-    {
+    public Long getActivityStart() {
       return activityStart;
     }
 
-    public Long getActivityBytes()
-    {
+    public Long getActivityBytes() {
       return activityBytes;
     }
 
-    public String getActivityCode()
-    {
+    public String getActivityCode() {
       return activityCode;
     }
 
-    public String getActivityDetails()
-    {
+    public String getActivityDetails() {
       return activityDetails;
     }
 
-    public boolean getReadFromDocumentStreamYet()
-    {
+    public boolean getReadFromDocumentStreamYet() {
       return readFromDocumentStreamYet;
     }
 
-    public boolean getRval()
-    {
+    public boolean getRval() {
       return rval;
     }
   }
 
-  /** Killable thread that does deletions.
-  * Java 1.5 stopped permitting thread interruptions to abort socket waits.  As a result, it is impossible to get threads to shutdown cleanly that are doing
-  * such waits.  So, the places where this happens are segregated in their own threads so that they can be just abandoned.
-  *
-  * This thread does a single document deletion.
-  */
-  protected class DeleteThread extends java.lang.Thread
-  {
+  /**
+   * Killable thread that does deletions. Java 1.5 stopped permitting thread interruptions to abort socket waits. As a result, it is impossible to get threads to shutdown cleanly that are doing such
+   * waits. So, the places where this happens are segregated in their own threads so that they can be just abandoned.
+   *
+   * This thread does a single document deletion.
+   */
+  protected class DeleteThread extends java.lang.Thread {
     protected String documentURI;
 
     protected Long activityStart = null;
@@ -1395,56 +1109,43 @@
     protected String activityDetails = null;
     protected Throwable exception = null;
 
-    public DeleteThread(String documentURI)
-    {
+    public DeleteThread(final String documentURI) {
       super();
       setDaemon(true);
       this.documentURI = documentURI;
     }
 
-    public void run()
-    {
-      try
-      {
+    @Override
+    public void run() {
+      try {
         // Do the operation!
-        long fullStartTime = System.currentTimeMillis();
+        final long fullStartTime = System.currentTimeMillis();
         // Open a socket to ingest, and to the response stream to get the post result
-        try
-        {
-          UpdateResponse response = new UpdateRequest(postRemoveAction).deleteById(documentURI).process(solrServer);
+        try {
+          final UpdateResponse response = new UpdateRequest(postRemoveAction).deleteById(documentURI).process(solrServer);
 
           // Success
           activityStart = new Long(fullStartTime);
           activityCode = "OK";
           activityDetails = null;
           return;
-        }
-        catch (InterruptedIOException ioe)
-        {
+        } catch (final InterruptedIOException ioe) {
           return;
-        }
-        catch (SolrServerException e)
-        {
+        } catch (final SolrServerException e) {
           activityStart = new Long(fullStartTime);
           activityCode = e.getClass().getSimpleName().toUpperCase(Locale.ROOT);
-          activityDetails = e.getMessage() +
-            ((e.getCause() != null)?": "+e.getCause().getMessage():"");
+          activityDetails = e.getMessage() + ((e.getCause() != null) ? ": " + e.getCause().getMessage() : "");
 
           throw e;
-        }
-        catch (SolrException e)
-        {
+        } catch (final SolrException e) {
           activityStart = new Long(fullStartTime);
           activityCode = e.getClass().getSimpleName().toUpperCase(Locale.ROOT);
-          activityDetails = e.getMessage() +
-            ((e.getCause() != null)?": "+e.getCause().getMessage():"");
+          activityDetails = e.getMessage() + ((e.getCause() != null) ? ": " + e.getCause().getMessage() : "");
 
           throw e;
-        }
-        catch (IOException ioe)
-        {
+        } catch (final IOException ioe) {
           // Log the error
-          Logging.ingest.warn("Error deleting document: "+ioe.getMessage(),ioe);
+          Logging.ingest.warn("Error deleting document: " + ioe.getMessage(), ioe);
 
           activityStart = new Long(fullStartTime);
           activityCode = ioe.getClass().getSimpleName().toUpperCase(Locale.ROOT);
@@ -1452,194 +1153,159 @@
 
           throw ioe;
         }
-      }
-      catch (Throwable e)
-      {
+      } catch (final Throwable e) {
         this.exception = e;
       }
     }
 
-    public void finishUp()
-      throws InterruptedException, SolrServerException, IOException
-    {
+    public void finishUp() throws InterruptedException, SolrServerException, IOException {
       join();
 
-      Throwable thr = exception;
-      if (thr != null)
-      {
+      final Throwable thr = exception;
+      if (thr != null) {
         if (thr instanceof SolrServerException)
-          throw (SolrServerException)thr;
+          throw (SolrServerException) thr;
         if (thr instanceof IOException)
-          throw (IOException)thr;
+          throw (IOException) thr;
         if (thr instanceof RuntimeException)
-          throw (RuntimeException)thr;
+          throw (RuntimeException) thr;
         if (thr instanceof Error)
-          throw (Error)thr;
+          throw (Error) thr;
         else
-          throw new RuntimeException("Unexpected exception type: "+thr.getClass().getName()+": "+thr.getMessage(),thr);
+          throw new RuntimeException("Unexpected exception type: " + thr.getClass().getName() + ": " + thr.getMessage(), thr);
       }
     }
-    public Long getActivityStart()
-    {
+
+    public Long getActivityStart() {
       return activityStart;
     }
 
-    public String getActivityCode()
-    {
+    public String getActivityCode() {
       return activityCode;
     }
 
-    public String getActivityDetails()
-    {
+    public String getActivityDetails() {
       return activityDetails;
     }
   }
 
-  /** Killable thread that does a commit.
-  * Java 1.5 stopped permitting thread interruptions to abort socket waits.  As a result, it is impossible to get threads to shutdown cleanly that are doing
-  * such waits.  So, the places where this happens are segregated in their own threads so that they can be just abandoned.
-  *
-  * This thread does a commit.
-  */
-  protected class CommitThread extends java.lang.Thread
-  {
+  /**
+   * Killable thread that does a commit. Java 1.5 stopped permitting thread interruptions to abort socket waits. As a result, it is impossible to get threads to shutdown cleanly that are doing such
+   * waits. So, the places where this happens are segregated in their own threads so that they can be just abandoned.
+   *
+   * This thread does a commit.
+   */
+  protected class CommitThread extends java.lang.Thread {
     protected Throwable exception = null;
 
-    public CommitThread()
-    {
+    public CommitThread() {
       super();
       setDaemon(true);
     }
 
-    public void run()
-    {
-      try
-      {
-        try
-        {
+    @Override
+    public void run() {
+      try {
+        try {
           // Do the operation!
-          UpdateRequest updateRequest = new UpdateRequest(postUpdateAction + "?commit=true");
-          UpdateResponse response = updateRequest.process(solrServer);
-          //UpdateResponse response = solrServer.commit();
-        }
-        catch (InterruptedIOException ioe)
-        {
+          final UpdateRequest updateRequest = new UpdateRequest(postUpdateAction + "?commit=true");
+          final UpdateResponse response = updateRequest.process(solrServer);
+          // UpdateResponse response = solrServer.commit();
+        } catch (final InterruptedIOException ioe) {
           return;
-        }
-        catch (IOException ioe)
-        {
+        } catch (final IOException ioe) {
           // Log the error
-          Logging.ingest.warn("Error committing: "+ioe.getMessage(),ioe);
+          Logging.ingest.warn("Error committing: " + ioe.getMessage(), ioe);
           throw ioe;
         }
-      }
-      catch (Throwable e)
-      {
+      } catch (final Throwable e) {
         this.exception = e;
       }
     }
 
-    public void finishUp()
-      throws InterruptedException, SolrServerException, IOException
-    {
+    public void finishUp() throws InterruptedException, SolrServerException, IOException {
       join();
 
-      Throwable thr = exception;
-      if (thr != null)
-      {
+      final Throwable thr = exception;
+      if (thr != null) {
         if (thr instanceof SolrServerException)
-          throw (SolrServerException)thr;
+          throw (SolrServerException) thr;
         if (thr instanceof IOException)
-          throw (IOException)thr;
+          throw (IOException) thr;
         if (thr instanceof RuntimeException)
-          throw (RuntimeException)thr;
+          throw (RuntimeException) thr;
         if (thr instanceof Error)
-          throw (Error)thr;
+          throw (Error) thr;
         else
-          throw new RuntimeException("Unexpected exception type: "+thr.getClass().getName()+": "+thr.getMessage(),thr);
+          throw new RuntimeException("Unexpected exception type: " + thr.getClass().getName() + ": " + thr.getMessage(), thr);
       }
     }
 
   }
 
-
-  /** Killable thread that does a status check.
-  * Java 1.5 stopped permitting thread interruptions to abort socket waits.  As a result, it is impossible to get threads to shutdown cleanly that are doing
-  * such waits.  So, the places where this happens are segregated in their own threads so that they can be just abandoned.
-  *
-  * This thread does a status check.
-  */
-  protected class StatusThread extends java.lang.Thread
-  {
+  /**
+   * Killable thread that does a status check. Java 1.5 stopped permitting thread interruptions to abort socket waits. As a result, it is impossible to get threads to shutdown cleanly that are doing
+   * such waits. So, the places where this happens are segregated in their own threads so that they can be just abandoned.
+   *
+   * This thread does a status check.
+   */
+  protected class StatusThread extends java.lang.Thread {
     protected Throwable exception = null;
 
-    public StatusThread()
-    {
+    public StatusThread() {
       super();
       setDaemon(true);
     }
 
-    public void run()
-    {
-      try
-      {
+    @Override
+    public void run() {
+      try {
         // Do the operation!
-        try
-        {
-          SolrResponse response = new SolrPing(postStatusAction).process(solrServer);
-        }
-        catch (InterruptedIOException ioe)
-        {
+        try {
+          final SolrResponse response = new SolrPing(postStatusAction).process(solrServer);
+        } catch (final InterruptedIOException ioe) {
           // Exit the thread.
           return;
-        }
-        catch (IOException ioe)
-        {
+        } catch (final IOException ioe) {
           // Log the error
-          Logging.ingest.warn("Error checking status: "+ioe.getMessage(),ioe);
+          Logging.ingest.warn("Error checking status: " + ioe.getMessage(), ioe);
           throw ioe;
         }
-      }
-      catch (Throwable e)
-      {
+      } catch (final Throwable e) {
         this.exception = e;
       }
     }
 
-    public void finishUp()
-      throws InterruptedException, SolrServerException, IOException
-    {
+    public void finishUp() throws InterruptedException, SolrServerException, IOException {
       join();
 
-      Throwable thr = exception;
-      if (thr != null)
-      {
+      final Throwable thr = exception;
+      if (thr != null) {
         if (thr instanceof SolrServerException)
-          throw (SolrServerException)thr;
+          throw (SolrServerException) thr;
         if (thr instanceof IOException)
-          throw (IOException)thr;
+          throw (IOException) thr;
         if (thr instanceof RuntimeException)
-          throw (RuntimeException)thr;
+          throw (RuntimeException) thr;
         if (thr instanceof Error)
-          throw (Error)thr;
+          throw (Error) thr;
         else
-          throw new RuntimeException("Unexpected exception type: "+thr.getClass().getName()+": "+thr.getMessage(),thr);
+          throw new RuntimeException("Unexpected exception type: " + thr.getClass().getName() + ": " + thr.getMessage(), thr);
       }
     }
 
   }
 
-  /** Class for importing documents into Solr via SolrJ
-  */
-  protected static class RepositoryDocumentStream extends ContentStreamBase
-  {
+  /**
+   * Class for importing documents into Solr via SolrJ
+   */
+  protected static class RepositoryDocumentStream extends ContentStreamBase {
     protected final InputStream is;
     protected final long length;
     protected final String contentType;
     protected final String contentName;
 
-    public RepositoryDocumentStream(InputStream is, long length, String contentType, String contentName)
-    {
+    public RepositoryDocumentStream(final InputStream is, final long length, final String contentType, final String contentName) {
       this.is = is;
       this.length = length;
       this.contentType = contentType;
@@ -1647,54 +1313,48 @@
     }
 
     @Override
-    public Long getSize()
-    {
+    public Long getSize() {
       return new Long(length);
     }
 
     @Override
-    public InputStream getStream() throws IOException
-    {
+    public InputStream getStream() throws IOException {
       return is;
     }
 
     @Override
-    public Reader getReader() throws IOException
-    {
+    public Reader getReader() throws IOException {
       return null;
     }
 
     @Override
-    public String getContentType()
-    {
+    public String getContentType() {
       return contentType;
     }
 
     @Override
-    public String getName()
-    {
+    public String getName() {
       return contentName;
     }
   }
 
-  /** Special version of ping class where we can control the URL
-  */
-  protected static class SolrPing extends SolrRequest
-  {
+  /**
+   * Special version of ping class where we can control the URL
+   */
+  protected static class SolrPing extends SolrRequest {
     /** Request parameters. */
-    private ModifiableSolrParams params;
+    private final ModifiableSolrParams params;
 
     /**
      * Create a new SolrPing object.
      */
     public SolrPing() {
-      super(METHOD.GET, "/admin/ping");
+      super(METHOD.GET, CommonParams.PING_HANDLER);
       params = new ModifiableSolrParams();
     }
 
-    public SolrPing(String url)
-    {
-      super( METHOD.GET, url );
+    public SolrPing(final String url) {
+      super(METHOD.GET, url);
       params = new ModifiableSolrParams();
     }
 
@@ -1704,7 +1364,7 @@
     }
 
     @Override
-    protected SolrPingResponse createResponse(SolrClient client) {
+    protected SolrPingResponse createResponse(final SolrClient client) {
       return new SolrPingResponse();
     }
 
@@ -1714,9 +1374,7 @@
     }
 
     /**
-     * Remove the action parameter from this request. This will result in the same
-     * behavior as {@code SolrPing#setActionPing()}. For Solr server version 4.0
-     * and later.
+     * Remove the action parameter from this request. This will result in the same behavior as {@code SolrPing#setActionPing()}. For Solr server version 4.0 and later.
      *
      * @return this
      */
@@ -1726,8 +1384,7 @@
     }
 
     /**
-     * Set the action parameter on this request to enable. This will delete the
-     * health-check file for the Solr core. For Solr server version 4.0 and later.
+     * Set the action parameter on this request to enable. This will delete the health-check file for the Solr core. For Solr server version 4.0 and later.
      *
      * @return this
      */
@@ -1737,8 +1394,7 @@
     }
 
     /**
-     * Set the action parameter on this request to enable. This will create the
-     * health-check file for the Solr core. For Solr server version 4.0 and later.
+     * Set the action parameter on this request to enable. This will create the health-check file for the Solr core. For Solr server version 4.0 and later.
      *
      * @return this
      */
@@ -1748,8 +1404,7 @@
     }
 
     /**
-     * Set the action parameter on this request to ping. This is the same as not
-     * including the action at all. For Solr server version 4.0 and later.
+     * Set the action parameter on this request to ping. This is the same as not including the action at all. For Solr server version 4.0 and later.
      *
      * @return this
      */
@@ -1758,28 +1413,28 @@
       return this;
     }
 
+    @Override
+    public String getRequestType() {
+      return SolrRequestType.ADMIN.toString();
+    }
+
   }
 
-  /** See CONNECTORS-956.  Make a safe lucene field name from a possibly
-  * unsafe input field name from a repository connector.
-  */
-  protected static String makeSafeLuceneField(String inputField)
-  {
-    StringBuilder sb = new StringBuilder();
+  /**
+   * See CONNECTORS-956. Make a safe lucene field name from a possibly unsafe input field name from a repository connector.
+   */
+  protected static String makeSafeLuceneField(final String inputField) {
+    final StringBuilder sb = new StringBuilder();
     boolean isFirst = true;
-    for (int i = 0; i < inputField.length(); i++)
-    {
-      char x = inputField.charAt(i);
-      if (isFirst && !Character.isJavaIdentifierStart(x) || !isFirst && !Character.isJavaIdentifierPart(x))
-      {
+    for (int i = 0; i < inputField.length(); i++) {
+      final char x = inputField.charAt(i);
+      if (isFirst && !Character.isJavaIdentifierStart(x) || !isFirst && !Character.isJavaIdentifierPart(x)) {
         // Check for exceptions for Lucene
         if (!isFirst && (x == '.' || x == '-'))
           sb.append(x);
         else
           sb.append('_');
-      }
-      else
-      {
+      } else {
         // Check for exceptions for Lucene
         if (isFirst && x == '$')
           sb.append('_');
@@ -1792,4 +1447,3 @@
   }
 
 }
-
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedCloudHttp2SolrClient.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedCloudHttp2SolrClient.java
new file mode 100644
index 0000000..26230c1
--- /dev/null
+++ b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedCloudHttp2SolrClient.java
@@ -0,0 +1,239 @@
+package org.apache.manifoldcf.agents.output.solr;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.solr.client.solrj.impl.CloudHttp2SolrClient;
+import org.apache.solr.client.solrj.impl.ClusterStateProvider;
+import org.apache.solr.client.solrj.request.UpdateRequest;
+import org.apache.solr.common.SolrException;
+
+public class ModifiedCloudHttp2SolrClient extends ModifiedCloudSolrClient {
+
+  private static final long serialVersionUID = -7543846119917075693L;
+  private final ClusterStateProvider stateProvider;
+  private final ModifiedLBHttp2SolrClient lbClient;
+  private final ModifiedHttp2SolrClient myClient;
+  private final boolean clientIsInternal;
+
+  /**
+   * Create a new client object that connects to Zookeeper and is always aware of the SolrCloud state. If there is a fully redundant Zookeeper quorum and SolrCloud has enough replicas for every shard
+   * in a collection, there is no single point of failure. Updates will be sent to shard leaders by default.
+   *
+   * @param builder a {@link ModifiedHttp2SolrClient.Builder} with the options used to create the client.
+   */
+  protected ModifiedCloudHttp2SolrClient(final Builder builder) {
+    super(builder.shardLeadersOnly, builder.parallelUpdates, builder.directUpdatesToLeadersOnly);
+    if (builder.httpClient == null) {
+      this.clientIsInternal = true;
+      if (builder.internalClientBuilder == null) {
+        this.myClient = new ModifiedHttp2SolrClient.Builder().build();
+      } else {
+        this.myClient = builder.internalClientBuilder.build();
+      }
+    } else {
+      this.clientIsInternal = false;
+      this.myClient = builder.httpClient;
+    }
+    if (builder.stateProvider == null) {
+      if (builder.zkHosts != null && builder.solrUrls != null) {
+        throw new IllegalArgumentException("Both zkHost(s) & solrUrl(s) have been specified. Only specify one.");
+      }
+      if (builder.zkHosts != null) {
+        this.stateProvider = ClusterStateProvider.newZkClusterStateProvider(builder.zkHosts, builder.zkChroot);
+      } else if (builder.solrUrls != null && !builder.solrUrls.isEmpty()) {
+        try {
+          this.stateProvider = new ModifiedHttp2ClusterStateProvider(builder.solrUrls, builder.httpClient);
+        } catch (final Exception e) {
+          throw new RuntimeException("Couldn't initialize a HttpClusterStateProvider (is/are the " + "Solr server(s), " + builder.solrUrls + ", down?)", e);
+        }
+      } else {
+        throw new IllegalArgumentException("Both zkHosts and solrUrl cannot be null.");
+      }
+    } else {
+      this.stateProvider = builder.stateProvider;
+    }
+    this.lbClient = new ModifiedLBHttp2SolrClient(myClient);
+  }
+
+  @Override
+  public void close() throws IOException {
+    stateProvider.close();
+    lbClient.close();
+
+    if (clientIsInternal && myClient != null) {
+      myClient.close();
+    }
+
+    super.close();
+  }
+
+  @Override
+  public ModifiedLBHttp2SolrClient getLbClient() {
+    return lbClient;
+  }
+
+  @Override
+  public ClusterStateProvider getClusterStateProvider() {
+    return stateProvider;
+  }
+
+  public ModifiedHttp2SolrClient getHttpClient() {
+    return myClient;
+  }
+
+  @Override
+  protected boolean wasCommError(final Throwable rootCause) {
+    return false;
+  }
+
+  /** Constructs {@link CloudHttp2SolrClient} instances from provided configuration. */
+  public static class Builder {
+    protected Collection<String> zkHosts = new ArrayList<>();
+    protected List<String> solrUrls = new ArrayList<>();
+    protected String zkChroot;
+    protected ModifiedHttp2SolrClient httpClient;
+    protected boolean shardLeadersOnly = true;
+    protected boolean directUpdatesToLeadersOnly = false;
+    protected boolean parallelUpdates = true;
+    protected ClusterStateProvider stateProvider;
+    protected ModifiedHttp2SolrClient.Builder internalClientBuilder;
+
+    /**
+     * Provide a series of Solr URLs to be used when configuring {@link CloudHttp2SolrClient} instances. The solr client will use these urls to understand the cluster topology, which solr nodes are
+     * active etc.
+     *
+     * <p>
+     * Provided Solr URLs are expected to point to the root Solr path ("http://hostname:8983/solr"); they should not include any collections, cores, or other path components.
+     *
+     * <p>
+     * Usage example:
+     *
+     * <pre>
+     * final List&lt;String&gt; solrBaseUrls = new ArrayList&lt;String&gt;();
+     * solrBaseUrls.add("http://solr1:8983/solr");
+     * solrBaseUrls.add("http://solr2:8983/solr");
+     * solrBaseUrls.add("http://solr3:8983/solr");
+     * final SolrClient client = new CloudHttp2SolrClient.Builder(solrBaseUrls).build();
+     * </pre>
+     */
+    public Builder(final List<String> solrUrls) {
+      this.solrUrls = solrUrls;
+    }
+
+    /**
+     * Provide a series of ZK hosts which will be used when configuring {@link CloudHttp2SolrClient} instances.
+     *
+     * <p>
+     * Usage example when Solr stores data at the ZooKeeper root ('/'):
+     *
+     * <pre>
+     * final List&lt;String&gt; zkServers = new ArrayList&lt;String&gt;();
+     * zkServers.add("zookeeper1:2181");
+     * zkServers.add("zookeeper2:2181");
+     * zkServers.add("zookeeper3:2181");
+     * final SolrClient client = new CloudHttp2SolrClient.Builder(zkServers, Optional.empty()).build();
+     * </pre>
+     *
+     * Usage example when Solr data is stored in a ZooKeeper chroot:
+     *
+     * <pre>
+     * final List&lt;String&gt; zkServers = new ArrayList&lt;String&gt;();
+     * zkServers.add("zookeeper1:2181");
+     * zkServers.add("zookeeper2:2181");
+     * zkServers.add("zookeeper3:2181");
+     * final SolrClient client = new CloudHttp2SolrClient.Builder(zkServers, Optional.of("/solr")).build();
+     * </pre>
+     *
+     * @param zkHosts  a List of at least one ZooKeeper host and port (e.g. "zookeeper1:2181")
+     * @param zkChroot the path to the root ZooKeeper node containing Solr data. Provide {@code
+     *     java.util.Optional.empty()} if no ZK chroot is used.
+     */
+    public Builder(final List<String> zkHosts, final Optional<String> zkChroot) {
+      this.zkHosts = zkHosts;
+      if (zkChroot.isPresent())
+        this.zkChroot = zkChroot.get();
+    }
+
+    /**
+     * Tells {@link CloudHttp2SolrClient.Builder} that created clients should send direct updates to shard leaders only.
+     *
+     * <p>
+     * UpdateRequests whose leaders cannot be found will "fail fast" on the client side with a {@link SolrException}
+     */
+    public Builder sendDirectUpdatesToShardLeadersOnly() {
+      directUpdatesToLeadersOnly = true;
+      return this;
+    }
+
+    /**
+     * Tells {@link CloudHttp2SolrClient.Builder} that created clients can send updates to any shard replica (shard leaders and non-leaders).
+     *
+     * <p>
+     * Shard leaders are still preferred, but the created clients will fallback to using other replicas if a leader cannot be found.
+     */
+    public Builder sendDirectUpdatesToAnyShardReplica() {
+      directUpdatesToLeadersOnly = false;
+      return this;
+    }
+
+    /**
+     * Tells {@link CloudHttp2SolrClient.Builder} whether created clients should send shard updates serially or in parallel
+     *
+     * <p>
+     * When an {@link UpdateRequest} affects multiple shards, {@link CloudHttp2SolrClient} splits it up and sends a request to each affected shard. This setting chooses whether those sub-requests are
+     * sent serially or in parallel.
+     *
+     * <p>
+     * If not set, this defaults to 'true' and sends sub-requests in parallel.
+     */
+    public Builder withParallelUpdates(final boolean parallelUpdates) {
+      this.parallelUpdates = parallelUpdates;
+      return this;
+    }
+
+    public Builder withHttpClient(final ModifiedHttp2SolrClient httpClient) {
+      if (this.internalClientBuilder != null) {
+        throw new IllegalStateException("The builder can't accept an httpClient AND an internalClientBuilder, only one of those can be provided");
+      }
+      this.httpClient = httpClient;
+      return this;
+    }
+
+    /**
+     * If provided, the CloudHttp2SolrClient will build it's internal ModifiedHttp2SolrClient using this builder (instead of the empty default one). Providing this builder allows users to configure
+     * the internal clients (authentication, timeouts, etc).
+     *
+     * @param internalClientBuilder the builder to use for creating the internal http client.
+     * @return this
+     */
+    public Builder withInternalClientBuilder(final ModifiedHttp2SolrClient.Builder internalClientBuilder) {
+      if (this.httpClient != null) {
+        throw new IllegalStateException("The builder can't accept an httpClient AND an internalClientBuilder, only one of those can be provided");
+      }
+      this.internalClientBuilder = internalClientBuilder;
+      return this;
+    }
+
+    /** Create a {@link CloudHttp2SolrClient} based on the provided configuration. */
+    public ModifiedCloudHttp2SolrClient build() {
+      if (stateProvider == null) {
+        if (!zkHosts.isEmpty()) {
+          stateProvider = ClusterStateProvider.newZkClusterStateProvider(zkHosts, Builder.this.zkChroot);
+        } else if (!this.solrUrls.isEmpty()) {
+          try {
+            stateProvider = new ModifiedHttp2ClusterStateProvider(solrUrls, httpClient);
+          } catch (final Exception e) {
+            throw new RuntimeException("Couldn't initialize a HttpClusterStateProvider (is/are the " + "Solr server(s), " + solrUrls + ", down?)", e);
+          }
+        } else {
+          throw new IllegalArgumentException("Both zkHosts and solrUrl cannot be null.");
+        }
+      }
+      return new ModifiedCloudHttp2SolrClient(this);
+    }
+  }
+}
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedCloudSolrClient.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedCloudSolrClient.java
new file mode 100644
index 0000000..f76f6a2
--- /dev/null
+++ b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedCloudSolrClient.java
@@ -0,0 +1,1229 @@
+package org.apache.manifoldcf.agents.output.solr;
+
+import static org.apache.solr.common.params.CommonParams.ADMIN_PATHS;
+import static org.apache.solr.common.params.CommonParams.ID;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.net.ConnectException;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import org.apache.solr.client.solrj.ResponseParser;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.V2RequestSupport;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.impl.ClusterStateProvider;
+import org.apache.solr.client.solrj.request.AbstractUpdateRequest;
+import org.apache.solr.client.solrj.request.IsUpdateRequest;
+import org.apache.solr.client.solrj.request.RequestWriter;
+import org.apache.solr.client.solrj.request.V2Request;
+import org.apache.solr.client.solrj.routing.ReplicaListTransformer;
+import org.apache.solr.client.solrj.routing.RequestReplicaListTransformerGenerator;
+import org.apache.solr.client.solrj.util.ClientUtils;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.ToleratedUpdateError;
+import org.apache.solr.common.cloud.ClusterState;
+import org.apache.solr.common.cloud.DocCollection;
+import org.apache.solr.common.cloud.DocRouter;
+import org.apache.solr.common.cloud.ImplicitDocRouter;
+import org.apache.solr.common.cloud.Replica;
+import org.apache.solr.common.cloud.Slice;
+import org.apache.solr.common.cloud.ZkCoreNodeProps;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.params.UpdateParams;
+import org.apache.solr.common.util.ExecutorUtil;
+import org.apache.solr.common.util.Hash;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.common.util.SolrNamedThreadFactory;
+import org.apache.solr.common.util.StrUtils;
+import org.apache.solr.common.util.Utils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+
+public abstract class ModifiedCloudSolrClient extends SolrClient {
+
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private volatile String defaultCollection;
+  // no of times collection state to be reloaded if stale state error is received
+  private static final int MAX_STALE_RETRIES = Integer.parseInt(System.getProperty("cloudSolrClientMaxStaleRetries", "5"));
+  private final Random rand = new Random();
+
+  private final boolean updatesToLeaders;
+  private final boolean directUpdatesToLeadersOnly;
+  private final RequestReplicaListTransformerGenerator requestRLTGenerator;
+  boolean parallelUpdates; // TODO final
+  private ExecutorService threadPool = ExecutorUtil.newMDCAwareCachedThreadPool(new SolrNamedThreadFactory("CloudSolrClient ThreadPool"));
+
+  public static final String STATE_VERSION = "_stateVer_";
+  private long retryExpiryTime = TimeUnit.NANOSECONDS.convert(3, TimeUnit.SECONDS); // 3 seconds or 3 million nanos
+  private final Set<String> NON_ROUTABLE_PARAMS;
+
+  {
+    NON_ROUTABLE_PARAMS = new HashSet<>();
+    NON_ROUTABLE_PARAMS.add(UpdateParams.EXPUNGE_DELETES);
+    NON_ROUTABLE_PARAMS.add(UpdateParams.MAX_OPTIMIZE_SEGMENTS);
+    NON_ROUTABLE_PARAMS.add(UpdateParams.COMMIT);
+    NON_ROUTABLE_PARAMS.add(UpdateParams.WAIT_SEARCHER);
+    NON_ROUTABLE_PARAMS.add(UpdateParams.OPEN_SEARCHER);
+
+    NON_ROUTABLE_PARAMS.add(UpdateParams.SOFT_COMMIT);
+    NON_ROUTABLE_PARAMS.add(UpdateParams.PREPARE_COMMIT);
+    NON_ROUTABLE_PARAMS.add(UpdateParams.OPTIMIZE);
+
+    // Not supported via SolrCloud
+    // NON_ROUTABLE_PARAMS.add(UpdateParams.ROLLBACK);
+
+  }
+
+  private volatile List<Object> locks = objectList(3);
+
+  /** Constructs {@link CloudSolrClient} instances from provided configuration. */
+  public static class Builder extends ModifiedCloudHttp2SolrClient.Builder {
+
+    /**
+     * Provide a series of Solr URLs to be used when configuring {@link CloudSolrClient} instances. The solr client will use these urls to understand the cluster topology, which solr nodes are active
+     * etc.
+     *
+     * <p>
+     * Provided Solr URLs are expected to point to the root Solr path ("http://hostname:8983/solr"); they should not include any collections, cores, or other path components.
+     *
+     * <p>
+     * Usage example:
+     *
+     * <pre>
+     * final List&lt;String&gt; solrBaseUrls = new ArrayList&lt;String&gt;();
+     * solrBaseUrls.add("http://solr1:8983/solr");
+     * solrBaseUrls.add("http://solr2:8983/solr");
+     * solrBaseUrls.add("http://solr3:8983/solr");
+     * final SolrClient client = new CloudSolrClient.Builder(solrBaseUrls).build();
+     * </pre>
+     */
+    public Builder(final List<String> solrUrls) {
+      super(solrUrls);
+    }
+
+    /**
+     * Provide a series of ZK hosts which will be used when configuring {@link CloudSolrClient} instances. This requires a dependency on {@code solr-solrj-zookeeper} which transitively depends on more
+     * JARs. The ZooKeeper based connection is the most reliable and performant means for CloudSolrClient to work. On the other hand, it means exposing ZooKeeper more broadly than to Solr nodes, which
+     * is a security risk.
+     *
+     * <p>
+     * Usage example when Solr stores data at the ZooKeeper root ('/'):
+     *
+     * <pre>
+     * final List&lt;String&gt; zkServers = new ArrayList&lt;String&gt;();
+     * zkServers.add("zookeeper1:2181");
+     * zkServers.add("zookeeper2:2181");
+     * zkServers.add("zookeeper3:2181");
+     * final SolrClient client = new CloudSolrClient.Builder(zkServers, Optional.empty()).build();
+     * </pre>
+     *
+     * Usage example when Solr data is stored in a ZooKeeper chroot:
+     *
+     * <pre>
+     * final List&lt;String&gt; zkServers = new ArrayList&lt;String&gt;();
+     * zkServers.add("zookeeper1:2181");
+     * zkServers.add("zookeeper2:2181");
+     * zkServers.add("zookeeper3:2181");
+     * final SolrClient client = new CloudSolrClient.Builder(zkServers, Optional.of("/solr")).build();
+     * </pre>
+     *
+     * @param zkHosts  a List of at least one ZooKeeper host and port (e.g. "zookeeper1:2181")
+     * @param zkChroot the path to the root ZooKeeper node containing Solr data. Provide {@code
+     *     java.util.Optional.empty()} if no ZK chroot is used.
+     */
+    public Builder(final List<String> zkHosts, final Optional<String> zkChroot) {
+      super(zkHosts, zkChroot);
+    }
+  }
+
+  static class StateCache extends ConcurrentHashMap<String, ExpiringCachedDocCollection> {
+    final AtomicLong puts = new AtomicLong();
+    final AtomicLong hits = new AtomicLong();
+    final Lock evictLock = new ReentrantLock(true);
+    protected volatile long timeToLive = 60 * 1000L;
+
+    @Override
+    public ExpiringCachedDocCollection get(final Object key) {
+      final ExpiringCachedDocCollection val = super.get(key);
+      if (val == null) {
+        // a new collection is likely to be added now.
+        // check if there are stale items and remove them
+        evictStale();
+        return null;
+      }
+      if (val.isExpired(timeToLive)) {
+        super.remove(key);
+        return null;
+      }
+      hits.incrementAndGet();
+      return val;
+    }
+
+    @Override
+    public ExpiringCachedDocCollection put(final String key, final ExpiringCachedDocCollection value) {
+      puts.incrementAndGet();
+      return super.put(key, value);
+    }
+
+    void evictStale() {
+      if (!evictLock.tryLock())
+        return;
+      try {
+        for (final Entry<String, ExpiringCachedDocCollection> e : entrySet()) {
+          if (e.getValue().isExpired(timeToLive)) {
+            super.remove(e.getKey());
+          }
+        }
+      } finally {
+        evictLock.unlock();
+      }
+    }
+  }
+
+  /**
+   * This is the time to wait to refetch the state after getting the same state version from ZK
+   *
+   * <p>
+   * secs
+   */
+  public void setRetryExpiryTime(final int secs) {
+    this.retryExpiryTime = TimeUnit.NANOSECONDS.convert(secs, TimeUnit.SECONDS);
+  }
+
+  protected final StateCache collectionStateCache = new StateCache();
+
+  class ExpiringCachedDocCollection {
+    final DocCollection cached;
+    final long cachedAt;
+    // This is the time at which the collection is retried and got the same old version
+    volatile long retriedAt = -1;
+    // flag that suggests that this is potentially to be rechecked
+    volatile boolean maybeStale = false;
+
+    ExpiringCachedDocCollection(final DocCollection cached) {
+      this.cached = cached;
+      this.cachedAt = System.nanoTime();
+    }
+
+    boolean isExpired(final long timeToLiveMs) {
+      return (System.nanoTime() - cachedAt) > TimeUnit.NANOSECONDS.convert(timeToLiveMs, TimeUnit.MILLISECONDS);
+    }
+
+    boolean shouldRetry() {
+      if (maybeStale) { // we are not sure if it is stale so check with retry time
+        if ((retriedAt == -1 || (System.nanoTime() - retriedAt) > retryExpiryTime)) {
+          return true; // we retried a while back. and we could not get anything new.
+          // it's likely that it is not going to be available now also.
+        }
+      }
+      return false;
+    }
+
+    void setRetriedAt() {
+      retriedAt = System.nanoTime();
+    }
+  }
+
+  protected ModifiedCloudSolrClient(final boolean updatesToLeaders, final boolean parallelUpdates, final boolean directUpdatesToLeadersOnly) {
+    this.updatesToLeaders = updatesToLeaders;
+    this.parallelUpdates = parallelUpdates;
+    this.directUpdatesToLeadersOnly = directUpdatesToLeadersOnly;
+    this.requestRLTGenerator = new RequestReplicaListTransformerGenerator();
+  }
+
+  /**
+   * Sets the cache ttl for DocCollection Objects cached.
+   *
+   * @param seconds ttl value in seconds
+   */
+  public void setCollectionCacheTTl(final int seconds) {
+    assert seconds > 0;
+    this.collectionStateCache.timeToLive = seconds * 1000L;
+  }
+
+  protected abstract ModifiedLBSolrClient getLbClient();
+
+  public abstract ClusterStateProvider getClusterStateProvider();
+
+  public ClusterState getClusterState() {
+    return getClusterStateProvider().getClusterState();
+  }
+
+  protected abstract boolean wasCommError(Throwable t);
+
+  @Override
+  public void close() throws IOException {
+    if (this.threadPool != null && !this.threadPool.isShutdown()) {
+      ExecutorUtil.shutdownAndAwaitTermination(this.threadPool);
+      this.threadPool = null;
+    }
+  }
+
+  public ResponseParser getParser() {
+    return getLbClient().getParser();
+  }
+
+  /**
+   * Note: This setter method is <b>not thread-safe</b>.
+   *
+   * @param processor Default Response Parser chosen to parse the response if the parser were not specified as part of the request.
+   * @see org.apache.solr.client.solrj.SolrRequest#getResponseParser()
+   */
+  public void setParser(final ResponseParser processor) {
+    getLbClient().setParser(processor);
+  }
+
+  public RequestWriter getRequestWriter() {
+    return getLbClient().getRequestWriter();
+  }
+
+  public void setRequestWriter(final RequestWriter requestWriter) {
+    getLbClient().setRequestWriter(requestWriter);
+  }
+
+  /** Sets the default collection for request */
+  public void setDefaultCollection(final String collection) {
+    this.defaultCollection = collection;
+  }
+
+  /** Gets the default collection for request */
+  public String getDefaultCollection() {
+    return defaultCollection;
+  }
+
+  /** Gets whether direct updates are sent in parallel */
+  public boolean isParallelUpdates() {
+    return parallelUpdates;
+  }
+
+  /**
+   * Connect to the zookeeper ensemble. This is an optional method that may be used to force a connect before any other requests are sent.
+   */
+  public void connect() {
+    getClusterStateProvider().connect();
+  }
+
+  /**
+   * Connect to a cluster. If the cluster is not ready, retry connection up to a given timeout.
+   *
+   * @param duration the timeout
+   * @param timeUnit the units of the timeout
+   * @throws TimeoutException     if the cluster is not ready after the timeout
+   * @throws InterruptedException if the wait is interrupted
+   */
+  public void connect(final long duration, final TimeUnit timeUnit) throws TimeoutException, InterruptedException {
+    if (log.isInfoEnabled()) {
+      log.info("Waiting for {} {} for cluster at {} to be ready", duration, timeUnit, getClusterStateProvider());
+    }
+    final long timeout = System.nanoTime() + timeUnit.toNanos(duration);
+    while (System.nanoTime() < timeout) {
+      try {
+        connect();
+        if (log.isInfoEnabled()) {
+          log.info("Cluster at {} ready", getClusterStateProvider());
+        }
+        return;
+      } catch (final RuntimeException e) {
+        // not ready yet, then...
+      }
+      TimeUnit.MILLISECONDS.sleep(250);
+    }
+    throw new TimeoutException("Timed out waiting for cluster");
+  }
+
+  @SuppressWarnings({ "unchecked" })
+  private NamedList<Object> directUpdate(final AbstractUpdateRequest request, String collection) throws SolrServerException {
+    final ModifiedUpdateRequest updateRequest = (ModifiedUpdateRequest) request;
+    SolrParams params = request.getParams();
+    final ModifiableSolrParams routableParams = new ModifiableSolrParams();
+    final ModifiableSolrParams nonRoutableParams = new ModifiableSolrParams();
+
+    if (params != null) {
+      nonRoutableParams.add(params);
+      routableParams.add(params);
+      for (final String param : NON_ROUTABLE_PARAMS) {
+        routableParams.remove(param);
+      }
+    } else {
+      params = new ModifiableSolrParams();
+    }
+
+    if (collection == null) {
+      throw new SolrServerException("No collection param specified on request and no default collection has been set.");
+    }
+
+    // Check to see if the collection is an alias. Updates to multi-collection aliases are ok as
+    // long as they are routed aliases
+    final List<String> aliasedCollections = getClusterStateProvider().resolveAlias(collection);
+    if (getClusterStateProvider().isRoutedAlias(collection) || aliasedCollections.size() == 1) {
+      collection = aliasedCollections.get(0); // pick 1st (consistent with HttpSolrCall behavior)
+    } else {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Update request to non-routed multi-collection alias not supported: " + collection + " -> " + aliasedCollections);
+    }
+
+    final DocCollection col = getDocCollection(collection, null);
+
+    final DocRouter router = col.getRouter();
+
+    if (router instanceof ImplicitDocRouter) {
+      // short circuit as optimization
+      return null;
+    }
+
+    final ReplicaListTransformer replicaListTransformer = requestRLTGenerator.getReplicaListTransformer(params);
+
+    // Create the URL map, which is keyed on slice name.
+    // The value is a list of URLs for each replica in the slice.
+    // The first value in the list is the leader for the slice.
+    final Map<String, List<String>> urlMap = buildUrlMap(col, replicaListTransformer);
+    final String routeField = (col.getRouter().getRouteField(col) == null) ? ID : col.getRouter().getRouteField(col);
+    final Map<String, ? extends ModifiedLBSolrClient.Req> routes = createRoutes(updateRequest, routableParams, col, router, urlMap, routeField);
+    if (routes == null) {
+      if (directUpdatesToLeadersOnly && hasInfoToFindLeaders(updateRequest, routeField)) {
+        // we have info (documents with ids and/or ids to delete) with
+        // which to find the leaders but we could not find (all of) them
+        throw new SolrException(SolrException.ErrorCode.SERVICE_UNAVAILABLE, "directUpdatesToLeadersOnly==true but could not find leader(s)");
+      } else {
+        // we could not find a leader or routes yet - use unoptimized general path
+        return null;
+      }
+    }
+
+    final NamedList<Throwable> exceptions = new NamedList<>();
+    final NamedList<NamedList<?>> shardResponses = new NamedList<>(routes.size() + 1); // +1 for deleteQuery
+
+    final long start = System.nanoTime();
+
+    if (parallelUpdates) {
+      final Map<String, Future<NamedList<?>>> responseFutures = new HashMap<>(routes.size());
+      for (final Map.Entry<String, ? extends ModifiedLBSolrClient.Req> entry : routes.entrySet()) {
+        final String url = entry.getKey();
+        final ModifiedLBSolrClient.Req lbRequest = entry.getValue();
+        try {
+          MDC.put("CloudSolrClient.url", url);
+          responseFutures.put(url, threadPool.submit(() -> {
+            return getLbClient().request(lbRequest).getResponse();
+          }));
+        } finally {
+          MDC.remove("CloudSolrClient.url");
+        }
+      }
+
+      for (final Map.Entry<String, Future<NamedList<?>>> entry : responseFutures.entrySet()) {
+        final String url = entry.getKey();
+        final Future<NamedList<?>> responseFuture = entry.getValue();
+        try {
+          shardResponses.add(url, responseFuture.get());
+        } catch (final InterruptedException e) {
+          Thread.currentThread().interrupt();
+          throw new RuntimeException(e);
+        } catch (final ExecutionException e) {
+          exceptions.add(url, e.getCause());
+        }
+      }
+
+      if (exceptions.size() > 0) {
+        final Throwable firstException = exceptions.getVal(0);
+        if (firstException instanceof SolrException) {
+          final SolrException e = (SolrException) firstException;
+          throw getRouteException(SolrException.ErrorCode.getErrorCode(e.code()), exceptions, routes);
+        } else {
+          throw getRouteException(SolrException.ErrorCode.SERVER_ERROR, exceptions, routes);
+        }
+      }
+    } else {
+      for (final Map.Entry<String, ? extends ModifiedLBSolrClient.Req> entry : routes.entrySet()) {
+        final String url = entry.getKey();
+        final ModifiedLBSolrClient.Req lbRequest = entry.getValue();
+        try {
+          final NamedList<Object> rsp = getLbClient().request(lbRequest).getResponse();
+          shardResponses.add(url, rsp);
+        } catch (final Exception e) {
+          if (e instanceof SolrException) {
+            throw (SolrException) e;
+          } else {
+            throw new SolrServerException(e);
+          }
+        }
+      }
+    }
+
+    ModifiedUpdateRequest nonRoutableRequest = null;
+    final List<String> deleteQuery = updateRequest.getDeleteQuery();
+    if (deleteQuery != null && deleteQuery.size() > 0) {
+      final ModifiedUpdateRequest deleteQueryRequest = new ModifiedUpdateRequest();
+      deleteQueryRequest.setDeleteQuery(deleteQuery);
+      nonRoutableRequest = deleteQueryRequest;
+    }
+
+    final Set<String> paramNames = nonRoutableParams.getParameterNames();
+
+    final Set<String> intersection = new HashSet<>(paramNames);
+    intersection.retainAll(NON_ROUTABLE_PARAMS);
+
+    if (nonRoutableRequest != null || intersection.size() > 0) {
+      if (nonRoutableRequest == null) {
+        nonRoutableRequest = new ModifiedUpdateRequest();
+      }
+      nonRoutableRequest.setParams(nonRoutableParams);
+      nonRoutableRequest.setBasicAuthCredentials(request.getBasicAuthUser(), request.getBasicAuthPassword());
+      final List<String> urlList = new ArrayList<>(routes.keySet());
+      Collections.shuffle(urlList, rand);
+      final ModifiedLBSolrClient.Req req = new ModifiedLBSolrClient.Req(nonRoutableRequest, urlList);
+      try {
+        final ModifiedLBSolrClient.Rsp rsp = getLbClient().request(req);
+        shardResponses.add(urlList.get(0), rsp.getResponse());
+      } catch (final Exception e) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, urlList.get(0), e);
+      }
+    }
+
+    final long end = System.nanoTime();
+
+    @SuppressWarnings({ "rawtypes" })
+    final RouteResponse rr = condenseResponse(shardResponses, (int) TimeUnit.MILLISECONDS.convert(end - start, TimeUnit.NANOSECONDS));
+    rr.setRouteResponses(shardResponses);
+    rr.setRoutes(routes);
+    return rr;
+  }
+
+  protected RouteException getRouteException(final SolrException.ErrorCode serverError, final NamedList<Throwable> exceptions, final Map<String, ? extends ModifiedLBSolrClient.Req> routes) {
+    return new RouteException(serverError, exceptions, routes);
+  }
+
+  protected Map<String, ? extends ModifiedLBSolrClient.Req> createRoutes(final ModifiedUpdateRequest updateRequest, final ModifiableSolrParams routableParams, final DocCollection col,
+      final DocRouter router, final Map<String, List<String>> urlMap, final String routeField) {
+    return urlMap == null ? null : updateRequest.getRoutesToCollection(router, col, urlMap, routableParams, routeField);
+  }
+
+  private Map<String, List<String>> buildUrlMap(final DocCollection col, final ReplicaListTransformer replicaListTransformer) {
+    final Map<String, List<String>> urlMap = new HashMap<>();
+    final Slice[] slices = col.getActiveSlicesArr();
+    for (final Slice slice : slices) {
+      final String name = slice.getName();
+      final List<Replica> sortedReplicas = new ArrayList<>();
+      Replica leader = slice.getLeader();
+      if (directUpdatesToLeadersOnly && leader == null) {
+        for (final Replica replica : slice.getReplicas(replica -> replica.isActive(getClusterStateProvider().getLiveNodes()) && replica.getType() == Replica.Type.NRT)) {
+          leader = replica;
+          break;
+        }
+      }
+      if (leader == null) {
+        if (directUpdatesToLeadersOnly) {
+          continue;
+        }
+        // take unoptimized general path - we cannot find a leader yet
+        return null;
+      }
+
+      if (!directUpdatesToLeadersOnly) {
+        for (final Replica replica : slice.getReplicas()) {
+          if (!replica.equals(leader)) {
+            sortedReplicas.add(replica);
+          }
+        }
+      }
+
+      // Sort the non-leader replicas according to the request parameters
+      replicaListTransformer.transform(sortedReplicas);
+
+      // put the leaderUrl first.
+      sortedReplicas.add(0, leader);
+
+      urlMap.put(name, sortedReplicas.stream().map(Replica::getCoreUrl).collect(Collectors.toList()));
+    }
+    return urlMap;
+  }
+
+  protected <T extends RouteResponse<?>> T condenseResponse(final NamedList<?> response, final int timeMillis, final Supplier<T> supplier) {
+    final T condensed = supplier.get();
+    int status = 0;
+    Integer rf = null;
+
+    // TolerantUpdateProcessor
+    List<SimpleOrderedMap<String>> toleratedErrors = null;
+    int maxToleratedErrors = Integer.MAX_VALUE;
+
+    // For "adds", "deletes", "deleteByQuery" etc.
+    final Map<String, NamedList<Object>> versions = new HashMap<>();
+
+    for (int i = 0; i < response.size(); i++) {
+      final NamedList<?> shardResponse = (NamedList<?>) response.getVal(i);
+      final NamedList<?> header = (NamedList<?>) shardResponse.get("responseHeader");
+      final Integer shardStatus = (Integer) header.get("status");
+      final int s = shardStatus.intValue();
+      if (s > 0) {
+        status = s;
+      }
+      final Object rfObj = header.get(ModifiedUpdateRequest.REPFACT);
+      if (rfObj != null && rfObj instanceof Integer) {
+        final Integer routeRf = (Integer) rfObj;
+        if (rf == null || routeRf < rf)
+          rf = routeRf;
+      }
+
+      @SuppressWarnings("unchecked")
+      final List<SimpleOrderedMap<String>> shardTolerantErrors = (List<SimpleOrderedMap<String>>) header.get("errors");
+      if (null != shardTolerantErrors) {
+        final Integer shardMaxToleratedErrors = (Integer) header.get("maxErrors");
+        assert null != shardMaxToleratedErrors : "TolerantUpdateProcessor reported errors but not maxErrors";
+        // if we get into some weird state where the nodes disagree about the effective maxErrors,
+        // assume the min value seen to decide if we should fail.
+        maxToleratedErrors = Math.min(maxToleratedErrors, ToleratedUpdateError.getEffectiveMaxErrors(shardMaxToleratedErrors.intValue()));
+
+        if (null == toleratedErrors) {
+          toleratedErrors = new ArrayList<SimpleOrderedMap<String>>(shardTolerantErrors.size());
+        }
+        for (final SimpleOrderedMap<String> err : shardTolerantErrors) {
+          toleratedErrors.add(err);
+        }
+      }
+      for (final String updateType : Arrays.asList("adds", "deletes", "deleteByQuery")) {
+        final Object obj = shardResponse.get(updateType);
+        if (obj instanceof NamedList) {
+          final NamedList<Object> versionsList = versions.containsKey(updateType) ? versions.get(updateType) : new NamedList<>();
+          final NamedList<?> nl = (NamedList<?>) obj;
+          versionsList.addAll(nl);
+          versions.put(updateType, versionsList);
+        }
+      }
+    }
+
+    final NamedList<Object> cheader = new NamedList<>();
+    cheader.add("status", status);
+    cheader.add("QTime", timeMillis);
+    if (rf != null)
+      cheader.add(ModifiedUpdateRequest.REPFACT, rf);
+    if (null != toleratedErrors) {
+      cheader.add("maxErrors", ToleratedUpdateError.getUserFriendlyMaxErrors(maxToleratedErrors));
+      cheader.add("errors", toleratedErrors);
+      if (maxToleratedErrors < toleratedErrors.size()) {
+        // cumulative errors are too high, we need to throw a client exception w/correct metadata
+
+        // NOTE: it shouldn't be possible for 1 == toleratedErrors.size(), because if that were the
+        // case then at least one shard should have thrown a real error before this, so we don't
+        // worry about having a more "singular" exception msg for that situation
+        final StringBuilder msgBuf = new StringBuilder().append(toleratedErrors.size()).append(" Async failures during distributed update: ");
+
+        final NamedList<String> metadata = new NamedList<>();
+        for (final SimpleOrderedMap<String> err : toleratedErrors) {
+          final ToleratedUpdateError te = ToleratedUpdateError.parseMap(err);
+          metadata.add(te.getMetadataKey(), te.getMetadataValue());
+
+          msgBuf.append("\n").append(te.getMessage());
+        }
+
+        final SolrException toThrow = new SolrException(SolrException.ErrorCode.BAD_REQUEST, msgBuf.toString());
+        toThrow.setMetadata(metadata);
+        throw toThrow;
+      }
+    }
+    for (final Map.Entry<String, NamedList<Object>> entry : versions.entrySet()) {
+      condensed.add(entry.getKey(), entry.getValue());
+    }
+    condensed.add("responseHeader", cheader);
+    return condensed;
+  }
+
+  @SuppressWarnings({ "rawtypes" })
+  public RouteResponse condenseResponse(final NamedList<?> response, final int timeMillis) {
+    return condenseResponse(response, timeMillis, RouteResponse::new);
+  }
+
+  @SuppressWarnings({ "rawtypes" })
+  public static class RouteResponse<T extends ModifiedLBSolrClient.Req> extends NamedList<Object> {
+    private NamedList<NamedList<?>> routeResponses;
+    private Map<String, T> routes;
+
+    public void setRouteResponses(final NamedList<NamedList<?>> routeResponses) {
+      this.routeResponses = routeResponses;
+    }
+
+    public NamedList<NamedList<?>> getRouteResponses() {
+      return routeResponses;
+    }
+
+    public void setRoutes(final Map<String, T> routes) {
+      this.routes = routes;
+    }
+
+    public Map<String, T> getRoutes() {
+      return routes;
+    }
+  }
+
+  public static class RouteException extends SolrException {
+
+    private final NamedList<Throwable> throwables;
+    private final Map<String, ? extends ModifiedLBSolrClient.Req> routes;
+
+    public RouteException(final ErrorCode errorCode, final NamedList<Throwable> throwables, final Map<String, ? extends ModifiedLBSolrClient.Req> routes) {
+      super(errorCode, throwables.getVal(0).getMessage(), throwables.getVal(0));
+      this.throwables = throwables;
+      this.routes = routes;
+
+      // create a merged copy of the metadata from all wrapped exceptions
+      final NamedList<String> metadata = new NamedList<String>();
+      for (int i = 0; i < throwables.size(); i++) {
+        final Throwable t = throwables.getVal(i);
+        if (t instanceof SolrException) {
+          final SolrException e = (SolrException) t;
+          final NamedList<String> eMeta = e.getMetadata();
+          if (null != eMeta) {
+            metadata.addAll(eMeta);
+          }
+        }
+      }
+      if (0 < metadata.size()) {
+        this.setMetadata(metadata);
+      }
+    }
+
+    public NamedList<Throwable> getThrowables() {
+      return throwables;
+    }
+
+    public Map<String, ? extends ModifiedLBSolrClient.Req> getRoutes() {
+      return this.routes;
+    }
+  }
+
+  @Override
+  public NamedList<Object> request(final SolrRequest<?> request, String collection) throws SolrServerException, IOException {
+    // the collection parameter of the request overrides that of the parameter to this method
+    final String requestCollection = request.getCollection();
+    if (requestCollection != null) {
+      collection = requestCollection;
+    } else if (collection == null) {
+      collection = defaultCollection;
+    }
+    final List<String> inputCollections = collection == null ? Collections.emptyList() : StrUtils.splitSmart(collection, ",", true);
+    return requestWithRetryOnStaleState(request, 0, inputCollections);
+  }
+
+  /**
+   * As this class doesn't watch external collections on the client side, there's a chance that the request will fail due to cached stale state, which means the state must be refreshed from ZK and
+   * retried.
+   */
+  protected NamedList<Object> requestWithRetryOnStaleState(SolrRequest<?> request, final int retryCount, final List<String> inputCollections) throws SolrServerException, IOException {
+    connect(); // important to call this before you start working with the ZkStateReader
+
+    // build up a _stateVer_ param to pass to the server containing all of the
+    // external collection state versions involved in this request, which allows
+    // the server to notify us that our cached state for one or more of the external
+    // collections is stale and needs to be refreshed ... this code has no impact on internal
+    // collections
+    String stateVerParam = null;
+    List<DocCollection> requestedCollections = null;
+    boolean isCollectionRequestOfV2 = false;
+    if (request instanceof V2RequestSupport) {
+      request = ((V2RequestSupport) request).getV2Request();
+    }
+    if (request instanceof V2Request) {
+      isCollectionRequestOfV2 = ((V2Request) request).isPerCollectionRequest();
+    }
+    final boolean isAdmin = ADMIN_PATHS.contains(request.getPath());
+    final boolean isUpdate = (request instanceof IsUpdateRequest) && (request instanceof ModifiedUpdateRequest);
+    if (!inputCollections.isEmpty() && !isAdmin && !isCollectionRequestOfV2) { // don't do _stateVer_ checking for admin, v2 api requests
+      final Set<String> requestedCollectionNames = resolveAliases(inputCollections, isUpdate);
+
+      StringBuilder stateVerParamBuilder = null;
+      for (final String requestedCollection : requestedCollectionNames) {
+        // track the version of state we're using on the client side using the _stateVer_ param
+        final DocCollection coll = getDocCollection(requestedCollection, null);
+        if (coll == null) {
+          throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Collection not found: " + requestedCollection);
+        }
+        final int collVer = coll.getZNodeVersion();
+        if (requestedCollections == null)
+          requestedCollections = new ArrayList<>(requestedCollectionNames.size());
+        requestedCollections.add(coll);
+
+        if (stateVerParamBuilder == null) {
+          stateVerParamBuilder = new StringBuilder();
+        } else {
+          stateVerParamBuilder.append("|"); // hopefully pipe is not an allowed char in a collection name
+        }
+
+        stateVerParamBuilder.append(coll.getName()).append(":").append(collVer);
+      }
+
+      if (stateVerParamBuilder != null) {
+        stateVerParam = stateVerParamBuilder.toString();
+      }
+    }
+
+    if (request.getParams() instanceof ModifiableSolrParams) {
+      final ModifiableSolrParams params = (ModifiableSolrParams) request.getParams();
+      if (stateVerParam != null) {
+        params.set(STATE_VERSION, stateVerParam);
+      } else {
+        params.remove(STATE_VERSION);
+      }
+    } // else: ??? how to set this ???
+
+    NamedList<Object> resp = null;
+    try {
+      resp = sendRequest(request, inputCollections);
+      // to avoid an O(n) operation we always add STATE_VERSION to the last and try to read it from
+      // there
+      final Object o = resp == null || resp.size() == 0 ? null : resp.get(STATE_VERSION, resp.size() - 1);
+      if (o != null && o instanceof Map) {
+        // remove this because no one else needs this and tests would fail if they are comparing
+        // responses
+        resp.remove(resp.size() - 1);
+        final Map<?, ?> invalidStates = (Map<?, ?>) o;
+        for (final Map.Entry<?, ?> e : invalidStates.entrySet()) {
+          getDocCollection((String) e.getKey(), (Integer) e.getValue());
+        }
+      }
+    } catch (final Exception exc) {
+
+      final Throwable rootCause = SolrException.getRootCause(exc);
+      // don't do retry support for admin requests
+      // or if the request doesn't have a collection specified
+      // or request is v2 api and its method is not GET
+      if (inputCollections.isEmpty() || isAdmin || (request instanceof V2Request && request.getMethod() != SolrRequest.METHOD.GET)) {
+        if (exc instanceof SolrServerException) {
+          throw (SolrServerException) exc;
+        } else if (exc instanceof IOException) {
+          throw (IOException) exc;
+        } else if (exc instanceof RuntimeException) {
+          throw (RuntimeException) exc;
+        } else {
+          throw new SolrServerException(rootCause);
+        }
+      }
+
+      final int errorCode = (rootCause instanceof SolrException) ? ((SolrException) rootCause).code() : SolrException.ErrorCode.UNKNOWN.code;
+
+      final boolean wasCommError = (rootCause instanceof ConnectException || rootCause instanceof SocketException || wasCommError(rootCause));
+
+      if (wasCommError || (exc instanceof RouteException && (errorCode == 503)) // 404 because the core does not exist 503 service unavailable
+      // TODO there are other reasons for 404. We need to change the solr response format from HTML
+      // to structured data to know that
+      ) {
+        // it was a communication error. it is likely that
+        // the node to which the request to be sent is down . So , expire the state
+        // so that the next attempt would fetch the fresh state
+        // just re-read state for all of them, if it has not been retried
+        // in retryExpiryTime time
+        if (requestedCollections != null) {
+          for (final DocCollection ext : requestedCollections) {
+            final ExpiringCachedDocCollection cacheEntry = collectionStateCache.get(ext.getName());
+            if (cacheEntry == null)
+              continue;
+            cacheEntry.maybeStale = true;
+          }
+        }
+        if (retryCount < MAX_STALE_RETRIES) { // if it is a communication error , we must try again
+          // may be, we have a stale version of the collection state
+          // and we could not get any information from the server
+          // it is probably not worth trying again and again because
+          // the state would not have been updated
+          log.info("Request to collection {} failed due to ({}) {}, retry={} maxRetries={} commError={} errorCode={} - retrying", inputCollections, errorCode, rootCause, retryCount, MAX_STALE_RETRIES,
+              wasCommError, errorCode);
+          return requestWithRetryOnStaleState(request, retryCount + 1, inputCollections);
+        }
+      } else {
+        log.info("request was not communication error it seems");
+      }
+      log.info("Request to collection {} failed due to ({}) {}, retry={} maxRetries={} commError={} errorCode={} ", inputCollections, errorCode, rootCause, retryCount, MAX_STALE_RETRIES, wasCommError,
+          errorCode);
+
+      boolean stateWasStale = false;
+      if (retryCount < MAX_STALE_RETRIES && requestedCollections != null && !requestedCollections.isEmpty()
+          && (SolrException.ErrorCode.getErrorCode(errorCode) == SolrException.ErrorCode.INVALID_STATE || errorCode == 404)) {
+        // cached state for one or more external collections was stale
+        // re-issue request using updated state
+        stateWasStale = true;
+
+        // just re-read state for all of them, which is a little heavy handed but hopefully a rare
+        // occurrence
+        for (final DocCollection ext : requestedCollections) {
+          collectionStateCache.remove(ext.getName());
+        }
+      }
+
+      // if we experienced a communication error, it's worth checking the state
+      // with ZK just to make sure the node we're trying to hit is still part of the collection
+      if (retryCount < MAX_STALE_RETRIES && !stateWasStale && requestedCollections != null && !requestedCollections.isEmpty() && wasCommError) {
+        for (final DocCollection ext : requestedCollections) {
+          final DocCollection latestStateFromZk = getDocCollection(ext.getName(), null);
+          if (latestStateFromZk.getZNodeVersion() != ext.getZNodeVersion()) {
+            // looks like we couldn't reach the server because the state was stale == retry
+            stateWasStale = true;
+            // we just pulled state from ZK, so update the cache so that the retry uses it
+            collectionStateCache.put(ext.getName(), new ExpiringCachedDocCollection(latestStateFromZk));
+          }
+        }
+      }
+
+      if (requestedCollections != null) {
+        requestedCollections.clear(); // done with this
+      }
+
+      // if the state was stale, then we retry the request once with new state pulled from Zk
+      if (stateWasStale) {
+        log.warn("Re-trying request to collection(s) {} after stale state error from server.", inputCollections);
+        resp = requestWithRetryOnStaleState(request, retryCount + 1, inputCollections);
+      } else {
+        if (exc instanceof SolrException || exc instanceof SolrServerException || exc instanceof IOException) {
+          throw exc;
+        } else {
+          throw new SolrServerException(rootCause);
+        }
+      }
+    }
+
+    return resp;
+  }
+
+  protected NamedList<Object> sendRequest(final SolrRequest<?> request, final List<String> inputCollections) throws SolrServerException, IOException {
+    connect();
+
+    boolean sendToLeaders = false;
+    boolean isUpdate = false;
+
+    if (request instanceof IsUpdateRequest) {
+      if (request instanceof ModifiedUpdateRequest) {
+        isUpdate = true;
+        if (inputCollections.size() > 1) {
+          throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Update request must be sent to a single collection " + "or an alias: " + inputCollections);
+        }
+        final String collection = inputCollections.isEmpty() ? null : inputCollections.get(0); // getting first mimics HttpSolrCall
+        final NamedList<Object> response = directUpdate((AbstractUpdateRequest) request, collection);
+        if (response != null) {
+          return response;
+        }
+      }
+      sendToLeaders = true;
+    }
+
+    SolrParams reqParams = request.getParams();
+    if (reqParams == null) { // TODO fix getParams to never return null!
+      reqParams = new ModifiableSolrParams();
+    }
+
+    final ReplicaListTransformer replicaListTransformer = requestRLTGenerator.getReplicaListTransformer(reqParams);
+
+    final ClusterStateProvider provider = getClusterStateProvider();
+    final String urlScheme = provider.getClusterProperty(ClusterState.URL_SCHEME, "http");
+    final Set<String> liveNodes = provider.getLiveNodes();
+
+    final List<String> theUrlList = new ArrayList<>(); // we populate this as follows...
+
+    if (request instanceof V2Request) {
+      if (!liveNodes.isEmpty()) {
+        final List<String> liveNodesList = new ArrayList<>(liveNodes);
+        Collections.shuffle(liveNodesList, rand);
+        theUrlList.add(Utils.getBaseUrlForNodeName(liveNodesList.get(0), urlScheme));
+      }
+
+    } else if (ADMIN_PATHS.contains(request.getPath())) {
+      for (final String liveNode : liveNodes) {
+        theUrlList.add(Utils.getBaseUrlForNodeName(liveNode, urlScheme));
+      }
+
+    } else { // Typical...
+      final Set<String> collectionNames = resolveAliases(inputCollections, isUpdate);
+      if (collectionNames.isEmpty()) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "No collection param specified on request and no default collection has been set: " + inputCollections);
+      }
+
+      final List<String> preferredNodes = request.getPreferredNodes();
+      if (preferredNodes != null && !preferredNodes.isEmpty()) {
+        final String joinedInputCollections = StrUtils.join(inputCollections, ',');
+        final List<String> urlList = new ArrayList<>(preferredNodes.size());
+        for (final String nodeName : preferredNodes) {
+          urlList.add(Utils.getBaseUrlForNodeName(nodeName, urlScheme) + "/" + joinedInputCollections);
+        }
+        if (!urlList.isEmpty()) {
+          final ModifiedLBSolrClient.Req req = new ModifiedLBSolrClient.Req(request, urlList);
+          final ModifiedLBSolrClient.Rsp rsp = getLbClient().request(req);
+          return rsp.getResponse();
+        }
+      }
+
+      // TODO: not a big deal because of the caching, but we could avoid looking
+      // at every shard when getting leaders if we tweaked some things
+
+      // Retrieve slices from the cloud state and, for each collection specified, add it to the Map
+      // of slices.
+      final Map<String, Slice> slices = new HashMap<>();
+      final String shardKeys = reqParams.get(ShardParams._ROUTE_);
+      for (final String collectionName : collectionNames) {
+        final DocCollection col = getDocCollection(collectionName, null);
+        if (col == null) {
+          throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Collection not found: " + collectionName);
+        }
+        final Collection<Slice> routeSlices = col.getRouter().getSearchSlices(shardKeys, reqParams, col);
+        ClientUtils.addSlices(slices, collectionName, routeSlices, true);
+      }
+
+      // Gather URLs, grouped by leader or replica
+      final List<Replica> sortedReplicas = new ArrayList<>();
+      final List<Replica> replicas = new ArrayList<>();
+      for (final Slice slice : slices.values()) {
+        final Replica leader = slice.getLeader();
+        for (final Replica replica : slice.getReplicas()) {
+          final String node = replica.getNodeName();
+          if (!liveNodes.contains(node) // Must be a live node to continue
+              || replica.getState() != Replica.State.ACTIVE) // Must be an ACTIVE replica to continue
+            continue;
+          if (sendToLeaders && replica.equals(leader)) {
+            sortedReplicas.add(replica); // put leaders here eagerly (if sendToLeader mode)
+          } else {
+            replicas.add(replica); // replicas here
+          }
+        }
+      }
+
+      // Sort the leader replicas, if any, according to the request preferences (none if
+      // !sendToLeaders)
+      replicaListTransformer.transform(sortedReplicas);
+
+      // Sort the replicas, if any, according to the request preferences and append to our list
+      replicaListTransformer.transform(replicas);
+
+      sortedReplicas.addAll(replicas);
+
+      final String joinedInputCollections = StrUtils.join(inputCollections, ',');
+      final Set<String> seenNodes = new HashSet<>();
+      sortedReplicas.forEach(replica -> {
+        if (seenNodes.add(replica.getNodeName())) {
+          theUrlList.add(ZkCoreNodeProps.getCoreUrl(replica.getBaseUrl(), joinedInputCollections));
+        }
+      });
+
+      if (theUrlList.isEmpty()) {
+        collectionStateCache.keySet().removeAll(collectionNames);
+        throw new SolrException(SolrException.ErrorCode.INVALID_STATE, "Could not find a healthy node to handle the request.");
+      }
+    }
+
+    final ModifiedLBSolrClient.Req req = new ModifiedLBSolrClient.Req(request, theUrlList);
+    final ModifiedLBSolrClient.Rsp rsp = getLbClient().request(req);
+    return rsp.getResponse();
+  }
+
+  /**
+   * Resolves the input collections to their possible aliased collections. Doesn't validate collection existence.
+   */
+  private Set<String> resolveAliases(final List<String> inputCollections, final boolean isUpdate) {
+    if (inputCollections.isEmpty()) {
+      return Collections.emptySet();
+    }
+    final LinkedHashSet<String> uniqueNames = new LinkedHashSet<>(); // consistent ordering
+    for (final String collectionName : inputCollections) {
+      if (getClusterStateProvider().getState(collectionName) == null) {
+        // perhaps it's an alias
+        uniqueNames.addAll(getClusterStateProvider().resolveAlias(collectionName));
+      } else {
+        uniqueNames.add(collectionName); // it's a collection
+      }
+    }
+    return uniqueNames;
+  }
+
+  public boolean isUpdatesToLeaders() {
+    return updatesToLeaders;
+  }
+
+  /**
+   * @return true if direct updates are sent to shard leaders only
+   */
+  public boolean isDirectUpdatesToLeadersOnly() {
+    return directUpdatesToLeadersOnly;
+  }
+
+  /**
+   * If caches are expired they are refreshed after acquiring a lock. use this to set the number of locks
+   */
+  public void setParallelCacheRefreshes(final int n) {
+    locks = objectList(n);
+  }
+
+  protected static ArrayList<Object> objectList(final int n) {
+    final ArrayList<Object> l = new ArrayList<>(n);
+    for (int i = 0; i < n; i++)
+      l.add(new Object());
+    return l;
+  }
+
+  protected DocCollection getDocCollection(final String collection, Integer expectedVersion) throws SolrException {
+    if (expectedVersion == null)
+      expectedVersion = -1;
+    if (collection == null)
+      return null;
+    ExpiringCachedDocCollection cacheEntry = collectionStateCache.get(collection);
+    DocCollection col = cacheEntry == null ? null : cacheEntry.cached;
+    if (col != null) {
+      if (expectedVersion <= col.getZNodeVersion() && !cacheEntry.shouldRetry())
+        return col;
+    }
+
+    final ClusterState.CollectionRef ref = getCollectionRef(collection);
+    if (ref == null) {
+      // no such collection exists
+      return null;
+    }
+    if (!ref.isLazilyLoaded()) {
+      // it is readily available just return it
+      return ref.get();
+    }
+    final List<Object> locks = this.locks;
+    final Object lock = locks.get(Math.abs(Hash.murmurhash3_x86_32(collection, 0, collection.length(), 0) % locks.size()));
+    DocCollection fetchedCol = null;
+    synchronized (lock) {
+      /* we have waited for sometime just check once again */
+      cacheEntry = collectionStateCache.get(collection);
+      col = cacheEntry == null ? null : cacheEntry.cached;
+      if (col != null) {
+        if (expectedVersion <= col.getZNodeVersion() && !cacheEntry.shouldRetry())
+          return col;
+      }
+      // We are going to fetch a new version
+      // we MUST try to get a new version
+      fetchedCol = ref.get(); // this is a call to ZK
+      if (fetchedCol == null)
+        return null; // this collection no more exists
+      if (col != null && fetchedCol.getZNodeVersion() == col.getZNodeVersion()) {
+        cacheEntry.setRetriedAt(); // we retried and found that it is the same version
+        cacheEntry.maybeStale = false;
+      } else {
+        collectionStateCache.put(collection, new ExpiringCachedDocCollection(fetchedCol));
+      }
+      return fetchedCol;
+    }
+  }
+
+  ClusterState.CollectionRef getCollectionRef(final String collection) {
+    return getClusterStateProvider().getState(collection);
+  }
+
+  /**
+   * Useful for determining the minimum achieved replication factor across all shards involved in processing an update request, typically useful for gauging the replication factor of a batch.
+   */
+  public int getMinAchievedReplicationFactor(final String collection, final NamedList<?> resp) {
+    // it's probably already on the top-level header set by condense
+    final NamedList<?> header = (NamedList<?>) resp.get("responseHeader");
+    Integer achRf = (Integer) header.get(ModifiedUpdateRequest.REPFACT);
+    if (achRf != null)
+      return achRf.intValue();
+
+    // not on the top-level header, walk the shard route tree
+    final Map<String, Integer> shardRf = getShardReplicationFactor(collection, resp);
+    for (final Integer rf : shardRf.values()) {
+      if (achRf == null || rf < achRf) {
+        achRf = rf;
+      }
+    }
+    return (achRf != null) ? achRf.intValue() : -1;
+  }
+
+  /**
+   * Walks the NamedList response after performing an update request looking for the replication factor that was achieved in each shard involved in the request. For single doc updates, there will be
+   * only one shard in the return value.
+   */
+  public Map<String, Integer> getShardReplicationFactor(final String collection, final NamedList<?> resp) {
+    connect();
+
+    final Map<String, Integer> results = new HashMap<>();
+    if (resp instanceof RouteResponse) {
+      final NamedList<NamedList<?>> routes = ((RouteResponse<?>) resp).getRouteResponses();
+      final DocCollection coll = getDocCollection(collection, null);
+      final Map<String, String> leaders = new HashMap<>();
+      for (final Slice slice : coll.getActiveSlicesArr()) {
+        final Replica leader = slice.getLeader();
+        if (leader != null) {
+          final ZkCoreNodeProps zkProps = new ZkCoreNodeProps(leader);
+          final String leaderUrl = zkProps.getBaseUrl() + "/" + zkProps.getCoreName();
+          leaders.put(leaderUrl, slice.getName());
+          final String altLeaderUrl = zkProps.getBaseUrl() + "/" + collection;
+          leaders.put(altLeaderUrl, slice.getName());
+        }
+      }
+
+      final Iterator<Map.Entry<String, NamedList<?>>> routeIter = routes.iterator();
+      while (routeIter.hasNext()) {
+        final Map.Entry<String, NamedList<?>> next = routeIter.next();
+        final String host = next.getKey();
+        final NamedList<?> hostResp = next.getValue();
+        final Integer rf = (Integer) ((NamedList<?>) hostResp.get("responseHeader")).get(ModifiedUpdateRequest.REPFACT);
+        if (rf != null) {
+          String shard = leaders.get(host);
+          if (shard == null) {
+            if (host.endsWith("/"))
+              shard = leaders.get(host.substring(0, host.length() - 1));
+            if (shard == null) {
+              shard = host;
+            }
+          }
+          results.put(shard, rf);
+        }
+      }
+    }
+    return results;
+  }
+
+  private static boolean hasInfoToFindLeaders(final ModifiedUpdateRequest updateRequest, final String idField) {
+    final Map<SolrInputDocument, Map<String, Object>> documents = updateRequest.getDocumentsMap();
+    final Map<String, Map<String, Object>> deleteById = updateRequest.getDeleteByIdMap();
+
+    final boolean hasNoDocuments = (documents == null || documents.isEmpty());
+    final boolean hasNoDeleteById = (deleteById == null || deleteById.isEmpty());
+    if (hasNoDocuments && hasNoDeleteById) {
+      // no documents and no delete-by-id, so no info to find leader(s)
+      return false;
+    }
+
+    if (documents != null) {
+      for (final Map.Entry<SolrInputDocument, Map<String, Object>> entry : documents.entrySet()) {
+        final SolrInputDocument doc = entry.getKey();
+        final Object fieldValue = doc.getFieldValue(idField);
+        if (fieldValue == null) {
+          // a document with no id field value, so can't find leader for it
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+
+}
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedHttp2ClusterStateProvider.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedHttp2ClusterStateProvider.java
new file mode 100644
index 0000000..411be46
--- /dev/null
+++ b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedHttp2ClusterStateProvider.java
@@ -0,0 +1,30 @@
+package org.apache.manifoldcf.agents.output.solr;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.impl.BaseHttpClusterStateProvider;
+
+public class ModifiedHttp2ClusterStateProvider extends BaseHttpClusterStateProvider {
+  final ModifiedHttp2SolrClient httpClient;
+  final boolean closeClient;
+
+  public ModifiedHttp2ClusterStateProvider(final List<String> solrUrls, final ModifiedHttp2SolrClient httpClient) throws Exception {
+    this.httpClient = httpClient == null ? new ModifiedHttp2SolrClient.Builder().build() : httpClient;
+    this.closeClient = httpClient == null;
+    init(solrUrls);
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (this.closeClient && this.httpClient != null) {
+      httpClient.close();
+    }
+  }
+
+  @Override
+  protected SolrClient getSolrClient(final String baseUrl) {
+    return new ModifiedHttp2SolrClient.Builder(baseUrl).withHttpClient(httpClient).build();
+  }
+}
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedHttp2SolrClient.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedHttp2SolrClient.java
new file mode 100644
index 0000000..b353670
--- /dev/null
+++ b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedHttp2SolrClient.java
@@ -0,0 +1,1062 @@
+package org.apache.manifoldcf.agents.output.solr;
+
+import static org.apache.solr.common.util.Utils.getObjectByPath;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.lang.invoke.MethodHandles;
+import java.net.ConnectException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Phaser;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.solr.client.solrj.ResponseParser;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.V2RequestSupport;
+import org.apache.solr.client.solrj.embedded.SSLConfig;
+import org.apache.solr.client.solrj.impl.BaseHttpSolrClient.RemoteExecutionException;
+import org.apache.solr.client.solrj.impl.BaseHttpSolrClient.RemoteSolrException;
+import org.apache.solr.client.solrj.impl.BinaryRequestWriter;
+import org.apache.solr.client.solrj.impl.BinaryResponseParser;
+import org.apache.solr.client.solrj.impl.Http2SolrClient;
+import org.apache.solr.client.solrj.impl.HttpClientUtil;
+import org.apache.solr.client.solrj.impl.HttpListenerFactory;
+import org.apache.solr.client.solrj.impl.InputStreamResponseParser;
+import org.apache.solr.client.solrj.request.RequestWriter;
+import org.apache.solr.client.solrj.request.UpdateRequest;
+import org.apache.solr.client.solrj.request.V2Request;
+import org.apache.solr.client.solrj.util.AsyncListener;
+import org.apache.solr.client.solrj.util.Cancellable;
+import org.apache.solr.client.solrj.util.ClientUtils;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.StringUtils;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.params.UpdateParams;
+import org.apache.solr.common.util.ContentStream;
+import org.apache.solr.common.util.ExecutorUtil;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.ObjectReleaseTracker;
+import org.apache.solr.common.util.SolrNamedThreadFactory;
+import org.apache.solr.common.util.Utils;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpClientTransport;
+import org.eclipse.jetty.client.ProtocolHandlers;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
+import org.eclipse.jetty.client.util.ByteBufferContentProvider;
+import org.eclipse.jetty.client.util.FormContentProvider;
+import org.eclipse.jetty.client.util.InputStreamContentProvider;
+import org.eclipse.jetty.client.util.InputStreamResponseListener;
+import org.eclipse.jetty.client.util.MultiPartContentProvider;
+import org.eclipse.jetty.client.util.OutputStreamContentProvider;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.http2.client.HTTP2Client;
+import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
+import org.eclipse.jetty.util.BlockingArrayQueue;
+import org.eclipse.jetty.util.Fields;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ModifiedHttp2SolrClient extends SolrClient {
+
+  private static final long serialVersionUID = -869785058825555540L;
+
+  public static final String REQ_PRINCIPAL_KEY = "solr-req-principal";
+  private final boolean useMultiPartPost = true;
+
+  private static volatile SSLConfig defaultSSLConfig;
+
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+  private static final String AGENT = "Solr[" + ModifiedHttp2SolrClient.class.getName() + "] 2.0";
+  private static final Charset FALLBACK_CHARSET = StandardCharsets.UTF_8;
+  private static final String DEFAULT_PATH = "/select";
+  private static final List<String> errPath = Arrays.asList("metadata", "error-class");
+
+  private HttpClient httpClient;
+  private volatile Set<String> queryParams = Collections.emptySet();
+  private int idleTimeout;
+
+  private ResponseParser parser = new BinaryResponseParser();
+  private volatile RequestWriter requestWriter = new BinaryRequestWriter();
+  private final List<HttpListenerFactory> listenerFactory = new LinkedList<>();
+  private final AsyncTracker asyncTracker = new AsyncTracker();
+  /** The URL of the Solr server. */
+  private String serverBaseUrl;
+
+  private boolean closeClient;
+  private ExecutorService executor;
+  private boolean shutdownExecutor;
+
+  private final String basicAuthAuthorizationStr;
+
+  protected ModifiedHttp2SolrClient(String serverBaseUrl, final Builder builder) {
+    if (serverBaseUrl != null) {
+      if (!serverBaseUrl.equals("/") && serverBaseUrl.endsWith("/")) {
+        serverBaseUrl = serverBaseUrl.substring(0, serverBaseUrl.length() - 1);
+      }
+
+      if (serverBaseUrl.startsWith("//")) {
+        serverBaseUrl = serverBaseUrl.substring(1, serverBaseUrl.length());
+      }
+      this.serverBaseUrl = serverBaseUrl;
+    }
+
+    if (builder.idleTimeout != null && builder.idleTimeout > 0)
+      idleTimeout = builder.idleTimeout;
+    else
+      idleTimeout = HttpClientUtil.DEFAULT_SO_TIMEOUT;
+
+    if (builder.http2SolrClient == null) {
+      httpClient = createHttpClient(builder);
+      closeClient = true;
+    } else {
+      httpClient = builder.http2SolrClient.httpClient;
+    }
+    if (builder.basicAuthUser != null && builder.basicAuthPassword != null) {
+      basicAuthAuthorizationStr = basicAuthCredentialsToAuthorizationString(builder.basicAuthUser, builder.basicAuthPassword);
+    } else {
+      basicAuthAuthorizationStr = null;
+    }
+    assert ObjectReleaseTracker.track(this);
+  }
+
+  public void addListenerFactory(final HttpListenerFactory factory) {
+    this.listenerFactory.add(factory);
+  }
+
+  // internal usage only
+  HttpClient getHttpClient() {
+    return httpClient;
+  }
+
+  // internal usage only
+  ProtocolHandlers getProtocolHandlers() {
+    return httpClient.getProtocolHandlers();
+  }
+
+  private HttpClient createHttpClient(final Builder builder) {
+    HttpClient httpClient;
+
+    executor = builder.executor;
+    if (executor == null) {
+      final BlockingArrayQueue<Runnable> queue = new BlockingArrayQueue<>(256, 256);
+      this.executor = new ExecutorUtil.MDCAwareThreadPoolExecutor(32, 256, 60, TimeUnit.SECONDS, queue, new SolrNamedThreadFactory("h2sc"));
+      shutdownExecutor = true;
+    } else {
+      shutdownExecutor = false;
+    }
+
+    SslContextFactory.Client sslContextFactory;
+    boolean sslEnabled;
+    if (builder.sslConfig == null) {
+      sslEnabled = System.getProperty("javax.net.ssl.keyStore") != null || System.getProperty("javax.net.ssl.trustStore") != null;
+      sslContextFactory = sslEnabled ? getDefaultSslContextFactory() : null;
+    } else {
+      sslContextFactory = builder.sslConfig.createClientContextFactory();
+      sslEnabled = true;
+    }
+
+    HttpClientTransport transport;
+    if (builder.useHttp1_1) {
+      if (log.isDebugEnabled()) {
+        log.debug("Create Http2SolrClient with HTTP/1.1 transport");
+      }
+      transport = new HttpClientTransportOverHTTP(2);
+      httpClient = sslEnabled ? new HttpClient(transport, sslContextFactory) : new HttpClient(transport, null);
+      if (builder.maxConnectionsPerHost != null)
+        httpClient.setMaxConnectionsPerDestination(builder.maxConnectionsPerHost);
+    } else {
+      log.debug("Create Http2SolrClient with HTTP/2 transport");
+      final HTTP2Client http2client = new HTTP2Client();
+      transport = new HttpClientTransportOverHTTP2(http2client);
+      httpClient = new HttpClient(transport, sslContextFactory);
+      httpClient.setMaxConnectionsPerDestination(4);
+    }
+
+    httpClient.setExecutor(this.executor);
+    httpClient.setStrictEventOrdering(false);
+    httpClient.setConnectBlocking(true);
+    httpClient.setFollowRedirects(false);
+    httpClient.setMaxRequestsQueuedPerDestination(asyncTracker.getMaxRequestsQueuedPerDestination());
+    httpClient.setUserAgentField(new HttpField(HttpHeader.USER_AGENT, AGENT));
+
+    httpClient.setIdleTimeout(idleTimeout);
+    if (builder.connectionTimeout != null)
+      httpClient.setConnectTimeout(builder.connectionTimeout);
+    try {
+      httpClient.start();
+    } catch (final Exception e) {
+      close(); // make sure we clean up
+      throw new RuntimeException(e);
+    }
+
+    return httpClient;
+  }
+
+  @Override
+  public void close() {
+    // we wait for async requests, so far devs don't want to give sugar for this
+    asyncTracker.waitForComplete();
+    try {
+      if (closeClient) {
+        httpClient.setStopTimeout(1000);
+        httpClient.stop();
+        httpClient.destroy();
+      }
+    } catch (final Exception e) {
+      throw new RuntimeException("Exception on closing client", e);
+    } finally {
+      if (shutdownExecutor) {
+        ExecutorUtil.shutdownAndAwaitTermination(executor);
+      }
+    }
+
+    assert ObjectReleaseTracker.release(this);
+  }
+
+  public boolean isV2ApiRequest(final SolrRequest<?> request) {
+    return request instanceof V2Request || request.getPath().contains("/____v2");
+  }
+
+  public long getIdleTimeout() {
+    return idleTimeout;
+  }
+
+  public static class OutStream implements Closeable {
+    private final String origCollection;
+    private final ModifiableSolrParams origParams;
+    private final OutputStreamContentProvider outProvider;
+    private final InputStreamResponseListener responseListener;
+    private final boolean isXml;
+
+    public OutStream(final String origCollection, final ModifiableSolrParams origParams, final OutputStreamContentProvider outProvider, final InputStreamResponseListener responseListener,
+        final boolean isXml) {
+      this.origCollection = origCollection;
+      this.origParams = origParams;
+      this.outProvider = outProvider;
+      this.responseListener = responseListener;
+      this.isXml = isXml;
+    }
+
+    boolean belongToThisStream(final SolrRequest<?> solrRequest, final String collection) {
+      final ModifiableSolrParams solrParams = new ModifiableSolrParams(solrRequest.getParams());
+      if (!origParams.toNamedList().equals(solrParams.toNamedList()) || !StringUtils.equals(origCollection, collection)) {
+        return false;
+      }
+      return true;
+    }
+
+    public void write(final byte b[]) throws IOException {
+      this.outProvider.getOutputStream().write(b);
+    }
+
+    public void flush() throws IOException {
+      this.outProvider.getOutputStream().flush();
+    }
+
+    @Override
+    public void close() throws IOException {
+      if (isXml) {
+        write("</stream>".getBytes(FALLBACK_CHARSET));
+      }
+      this.outProvider.getOutputStream().close();
+    }
+
+    // TODO this class should be hidden
+    public InputStreamResponseListener getResponseListener() {
+      return responseListener;
+    }
+  }
+
+  public OutStream initOutStream(final String baseUrl, final UpdateRequest updateRequest, final String collection) throws IOException {
+    final String contentType = requestWriter.getUpdateContentType();
+    final ModifiableSolrParams origParams = new ModifiableSolrParams(updateRequest.getParams());
+
+    // The parser 'wt=' and 'version=' params are used instead of the
+    // original params
+    final ModifiableSolrParams requestParams = new ModifiableSolrParams(origParams);
+    requestParams.set(CommonParams.WT, parser.getWriterType());
+    requestParams.set(CommonParams.VERSION, parser.getVersion());
+
+    String basePath = baseUrl;
+    if (collection != null)
+      basePath += "/" + collection;
+    if (!basePath.endsWith("/"))
+      basePath += "/";
+
+    final OutputStreamContentProvider provider = new OutputStreamContentProvider();
+    final Request postRequest = httpClient.newRequest(basePath + "update" + requestParams.toQueryString()).method(HttpMethod.POST).header(HttpHeader.CONTENT_TYPE, contentType).content(provider);
+    decorateRequest(postRequest, updateRequest);
+    final InputStreamResponseListener responseListener = new InputStreamResponseListener();
+    postRequest.send(responseListener);
+
+    final boolean isXml = ClientUtils.TEXT_XML.equals(requestWriter.getUpdateContentType());
+    final OutStream outStream = new OutStream(collection, origParams, provider, responseListener, isXml);
+    if (isXml) {
+      outStream.write("<stream>".getBytes(FALLBACK_CHARSET));
+    }
+    return outStream;
+  }
+
+  public void send(final OutStream outStream, final SolrRequest<?> req, final String collection) throws IOException {
+    assert outStream.belongToThisStream(req, collection);
+    this.requestWriter.write(req, outStream.outProvider.getOutputStream());
+    if (outStream.isXml) {
+      // check for commit or optimize
+      final SolrParams params = req.getParams();
+      if (params != null) {
+        String fmt = null;
+        if (params.getBool(UpdateParams.OPTIMIZE, false)) {
+          fmt = "<optimize waitSearcher=\"%s\" />";
+        } else if (params.getBool(UpdateParams.COMMIT, false)) {
+          fmt = "<commit waitSearcher=\"%s\" />";
+        }
+        if (fmt != null) {
+          final byte[] content = String.format(Locale.ROOT, fmt, params.getBool(UpdateParams.WAIT_SEARCHER, false) + "").getBytes(FALLBACK_CHARSET);
+          outStream.write(content);
+        }
+      }
+    }
+    outStream.flush();
+  }
+
+  @SuppressWarnings("StaticAssignmentOfThrowable")
+  private static final Exception CANCELLED_EXCEPTION = new Exception();
+
+  private static final Cancellable FAILED_MAKING_REQUEST_CANCELLABLE = () -> {
+  };
+
+  public Cancellable asyncRequest(final SolrRequest<?> solrRequest, final String collection, final AsyncListener<NamedList<Object>> asyncListener) {
+    Request req;
+    try {
+      req = makeRequest(solrRequest, collection);
+    } catch (SolrServerException | IOException e) {
+      asyncListener.onFailure(e);
+      return FAILED_MAKING_REQUEST_CANCELLABLE;
+    }
+    final ResponseParser parser = solrRequest.getResponseParser() == null ? this.parser : solrRequest.getResponseParser();
+    req.onRequestQueued(asyncTracker.queuedListener).onComplete(asyncTracker.completeListener).send(new InputStreamResponseListener() {
+      @Override
+      public void onHeaders(final Response response) {
+        super.onHeaders(response);
+        final InputStreamResponseListener listener = this;
+        executor.execute(() -> {
+          final InputStream is = listener.getInputStream();
+          assert ObjectReleaseTracker.track(is);
+          try {
+            final NamedList<Object> body = processErrorsAndResponse(solrRequest, parser, response, is);
+            asyncListener.onSuccess(body);
+          } catch (final RemoteSolrException e) {
+            if (SolrException.getRootCause(e) != CANCELLED_EXCEPTION) {
+              asyncListener.onFailure(e);
+            }
+          } catch (final SolrServerException e) {
+            asyncListener.onFailure(e);
+          }
+        });
+      }
+
+      @Override
+      public void onFailure(final Response response, final Throwable failure) {
+        super.onFailure(response, failure);
+        if (failure != CANCELLED_EXCEPTION) {
+          asyncListener.onFailure(new SolrServerException(failure.getMessage(), failure));
+        }
+      }
+    });
+    return () -> req.abort(CANCELLED_EXCEPTION);
+  }
+
+  @Override
+  public NamedList<Object> request(final SolrRequest<?> solrRequest, final String collection) throws SolrServerException, IOException {
+    final Request req = makeRequest(solrRequest, collection);
+    final ResponseParser parser = solrRequest.getResponseParser() == null ? this.parser : solrRequest.getResponseParser();
+
+    Throwable abortCause = null;
+    try {
+      final InputStreamResponseListener listener = new InputStreamResponseListener();
+      req.send(listener);
+      final Response response = listener.get(idleTimeout, TimeUnit.MILLISECONDS);
+      final InputStream is = listener.getInputStream();
+      assert ObjectReleaseTracker.track(is);
+      return processErrorsAndResponse(solrRequest, parser, response, is);
+    } catch (final InterruptedException e) {
+      Thread.currentThread().interrupt();
+      abortCause = e;
+      throw new RuntimeException(e);
+    } catch (final TimeoutException e) {
+      throw new SolrServerException("Timeout occured while waiting response from server at: " + req.getURI(), e);
+    } catch (final ExecutionException e) {
+      final Throwable cause = e.getCause();
+      abortCause = cause;
+      if (cause instanceof ConnectException) {
+        throw new SolrServerException("Server refused connection at: " + req.getURI(), cause);
+      }
+      if (cause instanceof SolrServerException) {
+        throw (SolrServerException) cause;
+      } else if (cause instanceof IOException) {
+        throw new SolrServerException("IOException occured when talking to server at: " + getBaseURL(), cause);
+      }
+      throw new SolrServerException(cause.getMessage(), cause);
+    } catch (SolrServerException | RuntimeException sse) {
+      abortCause = sse;
+      throw sse;
+    } finally {
+      if (abortCause != null) {
+        req.abort(abortCause);
+      }
+    }
+  }
+
+  private NamedList<Object> processErrorsAndResponse(final SolrRequest<?> solrRequest, final ResponseParser parser, final Response response, final InputStream is) throws SolrServerException {
+    final String contentType = response.getHeaders().get(HttpHeader.CONTENT_TYPE);
+    String mimeType = null;
+    String encoding = null;
+    if (contentType != null) {
+      mimeType = MimeTypes.getContentTypeWithoutCharset(contentType);
+      encoding = MimeTypes.getCharsetFromContentType(contentType);
+    }
+    return processErrorsAndResponse(response, parser, is, mimeType, encoding, isV2ApiRequest(solrRequest));
+  }
+
+  private void setBasicAuthHeader(final SolrRequest<?> solrRequest, final Request req) {
+    if (solrRequest.getBasicAuthUser() != null && solrRequest.getBasicAuthPassword() != null) {
+      final String encoded = basicAuthCredentialsToAuthorizationString(solrRequest.getBasicAuthUser(), solrRequest.getBasicAuthPassword());
+      req.header("Authorization", encoded);
+    } else if (basicAuthAuthorizationStr != null) {
+      req.header("Authorization", basicAuthAuthorizationStr);
+    }
+  }
+
+  private String basicAuthCredentialsToAuthorizationString(final String user, final String pass) {
+    final String userPass = user + ":" + pass;
+    return "Basic " + Base64.getEncoder().encodeToString(userPass.getBytes(FALLBACK_CHARSET));
+  }
+
+  private Request makeRequest(final SolrRequest<?> solrRequest, final String collection) throws SolrServerException, IOException {
+    final Request req = createRequest(solrRequest, collection);
+    decorateRequest(req, solrRequest);
+    return req;
+  }
+
+  private void decorateRequest(final Request req, final SolrRequest<?> solrRequest) {
+    req.header(HttpHeader.ACCEPT_ENCODING, null);
+    req.timeout(idleTimeout, TimeUnit.MILLISECONDS);
+    if (solrRequest.getUserPrincipal() != null) {
+      req.attribute(REQ_PRINCIPAL_KEY, solrRequest.getUserPrincipal());
+    }
+
+    setBasicAuthHeader(solrRequest, req);
+    for (final HttpListenerFactory factory : listenerFactory) {
+      final HttpListenerFactory.RequestResponseListener listener = factory.get();
+      listener.onQueued(req);
+      req.onRequestBegin(listener);
+      req.onComplete(listener);
+    }
+
+    final Map<String, String> headers = solrRequest.getHeaders();
+    if (headers != null) {
+      for (final Map.Entry<String, String> entry : headers.entrySet()) {
+        req.header(entry.getKey(), entry.getValue());
+      }
+    }
+  }
+
+  private String changeV2RequestEndpoint(final String basePath) throws MalformedURLException {
+    final URL oldURL = new URL(basePath);
+    final String newPath = oldURL.getPath().replaceFirst("/solr", "/api");
+    return new URL(oldURL.getProtocol(), oldURL.getHost(), oldURL.getPort(), newPath).toString();
+  }
+
+  private Request createRequest(SolrRequest<?> solrRequest, final String collection) throws IOException, SolrServerException {
+    if (solrRequest.getBasePath() == null && serverBaseUrl == null)
+      throw new IllegalArgumentException("Destination node is not provided!");
+
+    if (solrRequest instanceof V2RequestSupport) {
+      solrRequest = ((V2RequestSupport) solrRequest).getV2Request();
+    }
+    final SolrParams params = solrRequest.getParams();
+    final RequestWriter.ContentWriter contentWriter = requestWriter.getContentWriter(solrRequest);
+    Collection<ContentStream> streams = contentWriter == null ? requestWriter.getContentStreams(solrRequest) : null;
+    String path = requestWriter.getPath(solrRequest);
+    if (path == null || !path.startsWith("/")) {
+      path = DEFAULT_PATH;
+    }
+
+    ResponseParser parser = solrRequest.getResponseParser();
+    if (parser == null) {
+      parser = this.parser;
+    }
+
+    // The parser 'wt=' and 'version=' params are used instead of the original
+    // params
+    final ModifiableSolrParams wparams = new ModifiableSolrParams(params);
+    if (parser != null) {
+      wparams.set(CommonParams.WT, parser.getWriterType());
+      wparams.set(CommonParams.VERSION, parser.getVersion());
+    }
+
+    // TODO add invariantParams support
+
+    String basePath = solrRequest.getBasePath() == null ? serverBaseUrl : solrRequest.getBasePath();
+    if (collection != null)
+      basePath += "/" + collection;
+
+    if (solrRequest instanceof V2Request) {
+      if (System.getProperty("solr.v2RealPath") == null) {
+        basePath = changeV2RequestEndpoint(basePath);
+      } else {
+        basePath = serverBaseUrl + "/____v2";
+      }
+    }
+
+    if (SolrRequest.METHOD.GET == solrRequest.getMethod()) {
+      if (streams != null || contentWriter != null) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "GET can't send streams!");
+      }
+
+      return httpClient.newRequest(basePath + path + wparams.toQueryString()).method(HttpMethod.GET);
+    }
+
+    if (SolrRequest.METHOD.DELETE == solrRequest.getMethod()) {
+      return httpClient.newRequest(basePath + path + wparams.toQueryString()).method(HttpMethod.DELETE);
+    }
+
+    if (SolrRequest.METHOD.POST == solrRequest.getMethod() || SolrRequest.METHOD.PUT == solrRequest.getMethod()) {
+
+      final String url = basePath + path;
+      boolean hasNullStreamName = false;
+      if (streams != null) {
+        hasNullStreamName = streams.stream().anyMatch(cs -> cs.getName() == null);
+      }
+      final String contentWriterUrl = url + toQueryString(wparams, false);
+
+//      final boolean isMultipart = streams != null && streams.size() > 1 && !hasNullStreamName;
+      boolean isMultipart;
+      // If the solrRequest is an UpdateRequest it means it is a commit or a delete request so we must use regular way (SolrJ default one) to set isMultipart
+      if (this.useMultiPartPost && !(solrRequest instanceof UpdateRequest)) {
+        final Collection<ContentStream> requestStreams = requestWriter.getContentStreams(solrRequest);
+        // Do we have streams?
+        if (requestStreams != null && requestStreams.size() > 0) {
+
+          // Also, is the contentWriter URL too big?
+          final boolean urlTooBig = contentWriterUrl.length() > 4000;
+          // System.out.println("RequestStreams present? "+(requestStreams != null && requestStreams.size() > 0)+"; hasNullStreamName? "+hasNullStreamName+"; url length = "+contentWriterUrl.length());
+          isMultipart = requestStreams != null && requestStreams.size() > 0 && ((solrRequest.getMethod() == SolrRequest.METHOD.POST && !hasNullStreamName) || urlTooBig);
+          if (isMultipart) {
+            // System.out.println("Overriding with multipart post");
+            streams = requestStreams;
+          }
+        } else {
+          isMultipart = false;
+        }
+      } else {
+        // SolrJ default way to set isMultipart
+        isMultipart = streams != null && streams.size() > 1 && !hasNullStreamName;
+      }
+
+      final HttpMethod method = SolrRequest.METHOD.POST == solrRequest.getMethod() ? HttpMethod.POST : HttpMethod.PUT;
+
+      if (contentWriter != null && !isMultipart) {
+        final Request req = httpClient.newRequest(url + wparams.toQueryString()).method(method);
+        final BinaryRequestWriter.BAOS baos = new BinaryRequestWriter.BAOS();
+        contentWriter.write(baos);
+
+        // SOLR-16265: TODO reduce memory usage
+        return req.content(
+            // We're throwing this BAOS away, so no need to copy the byte[], just use the raw buf
+            new ByteBufferContentProvider(contentWriter.getContentType(), ByteBuffer.wrap(baos.getbuf(), 0, baos.size())));
+      } else if (streams == null || isMultipart) {
+        // send server list and request list as query string params
+        final ModifiableSolrParams queryParams = calculateQueryParams(this.queryParams, wparams);
+        queryParams.add(calculateQueryParams(solrRequest.getQueryParams(), wparams));
+        final Request req = httpClient.newRequest(url + queryParams.toQueryString()).method(method);
+        return fillContentStream(req, streams, wparams, isMultipart);
+      } else {
+        // It is has one stream, it is the post body, put the params in the URL
+        final ContentStream contentStream = streams.iterator().next();
+        return httpClient.newRequest(url + wparams.toQueryString()).method(method).content(new InputStreamContentProvider(contentStream.getStream()), contentStream.getContentType());
+      }
+    }
+
+    throw new SolrServerException("Unsupported method: " + solrRequest.getMethod());
+  }
+
+  public static String toQueryString(final SolrParams params, final boolean xml) {
+    final StringBuilder sb = new StringBuilder(128);
+    try {
+      final String amp = xml ? "&amp;" : "&";
+      boolean first = true;
+      final Iterator<String> names = params.getParameterNamesIterator();
+      while (names.hasNext()) {
+        final String key = names.next();
+        final String[] valarr = params.getParams(key);
+        if (valarr == null) {
+          sb.append(first ? "?" : amp);
+          sb.append(URLEncoder.encode(key, "UTF-8"));
+          first = false;
+        } else {
+          for (final String val : valarr) {
+            sb.append(first ? "?" : amp);
+            sb.append(key);
+            if (val != null) {
+              sb.append('=');
+              sb.append(URLEncoder.encode(val, "UTF-8"));
+            }
+            first = false;
+          }
+        }
+      }
+    } catch (final IOException e) {
+      throw new RuntimeException(e);
+    } // can't happen
+    return sb.toString();
+  }
+
+  private Request fillContentStream(final Request req, final Collection<ContentStream> streams, final ModifiableSolrParams wparams, final boolean isMultipart) throws IOException {
+    if (isMultipart) {
+      // multipart/form-data
+      final MultiPartContentProvider content = new MultiPartContentProvider();
+      final Iterator<String> iter = wparams.getParameterNamesIterator();
+      while (iter.hasNext()) {
+        final String key = iter.next();
+        final String[] vals = wparams.getParams(key);
+        if (vals != null) {
+          for (final String val : vals) {
+            content.addFieldPart(key, new StringContentProvider(val), null);
+          }
+        }
+      }
+      if (streams != null) {
+        for (final ContentStream contentStream : streams) {
+          String contentType = contentStream.getContentType();
+          if (contentType == null) {
+            contentType = BinaryResponseParser.BINARY_CONTENT_TYPE; // default
+          }
+          String name = contentStream.getName();
+          if (name == null) {
+            name = "";
+          }
+          final HttpFields fields = new HttpFields();
+          fields.add(HttpHeader.CONTENT_TYPE, contentType);
+          content.addFilePart(name, contentStream.getName(), new InputStreamContentProvider(contentStream.getStream()), fields);
+        }
+      }
+      req.content(content);
+    } else {
+      // application/x-www-form-urlencoded
+      final Fields fields = new Fields();
+      final Iterator<String> iter = wparams.getParameterNamesIterator();
+      while (iter.hasNext()) {
+        final String key = iter.next();
+        final String[] vals = wparams.getParams(key);
+        if (vals != null) {
+          for (final String val : vals) {
+            fields.add(key, val);
+          }
+        }
+      }
+      req.content(new FormContentProvider(fields, FALLBACK_CHARSET));
+    }
+
+    return req;
+  }
+
+  private boolean wantStream(final ResponseParser processor) {
+    return processor == null || processor instanceof InputStreamResponseParser;
+  }
+
+  @SuppressWarnings({ "unchecked", "rawtypes" })
+  private NamedList<Object> processErrorsAndResponse(final Response response, final ResponseParser processor, final InputStream is, final String mimeType, final String encoding, final boolean isV2Api)
+      throws SolrServerException {
+    boolean shouldClose = true;
+    try {
+      // handle some http level checks before trying to parse the response
+      final int httpStatus = response.getStatus();
+
+      switch (httpStatus) {
+      case HttpStatus.OK_200:
+      case HttpStatus.BAD_REQUEST_400:
+      case HttpStatus.CONFLICT_409:
+        break;
+      case HttpStatus.MOVED_PERMANENTLY_301:
+      case HttpStatus.MOVED_TEMPORARILY_302:
+        if (!httpClient.isFollowRedirects()) {
+          throw new SolrServerException("Server at " + getBaseURL() + " sent back a redirect (" + httpStatus + ").");
+        }
+        break;
+      default:
+        if (processor == null || mimeType == null) {
+          throw new RemoteSolrException(serverBaseUrl, httpStatus, "non ok status: " + httpStatus + ", message:" + response.getReason(), null);
+        }
+      }
+
+      if (wantStream(parser)) {
+        // no processor specified, return raw stream
+        final NamedList<Object> rsp = new NamedList<>();
+        rsp.add("stream", is);
+        // Only case where stream should not be closed
+        shouldClose = false;
+        return rsp;
+      }
+
+      final String procCt = processor.getContentType();
+      if (procCt != null) {
+        final String procMimeType = MimeTypes.getContentTypeWithoutCharset(procCt).trim().toLowerCase(Locale.ROOT);
+        if (!procMimeType.equals(mimeType)) {
+          // unexpected mime type
+          final String prefix = "Expected mime type " + procMimeType + " but got " + mimeType + ". ";
+          final String exceptionEncoding = encoding != null ? encoding : FALLBACK_CHARSET.name();
+          try {
+            final ByteArrayOutputStream body = new ByteArrayOutputStream();
+//            is.transferTo(body);
+            IOUtils.copy(is, body);
+            throw new RemoteSolrException(serverBaseUrl, httpStatus, prefix + body.toString(exceptionEncoding), null);
+          } catch (final IOException e) {
+            throw new RemoteSolrException(serverBaseUrl, httpStatus, "Could not parse response with encoding " + exceptionEncoding, e);
+          }
+        }
+      }
+
+      NamedList<Object> rsp;
+      try {
+        rsp = processor.processResponse(is, encoding);
+      } catch (final Exception e) {
+        throw new RemoteSolrException(serverBaseUrl, httpStatus, e.getMessage(), e);
+      }
+
+      final Object error = rsp == null ? null : rsp.get("error");
+      if (error != null && (String.valueOf(getObjectByPath(error, true, errPath)).endsWith("ExceptionWithErrObject"))) {
+        throw RemoteExecutionException.create(serverBaseUrl, rsp);
+      }
+      if (httpStatus != HttpStatus.OK_200 && !isV2Api) {
+        NamedList<String> metadata = null;
+        String reason = null;
+        try {
+          if (error != null) {
+            reason = (String) Utils.getObjectByPath(error, false, Collections.singletonList("msg"));
+            if (reason == null) {
+              reason = (String) Utils.getObjectByPath(error, false, Collections.singletonList("trace"));
+            }
+            final Object metadataObj = Utils.getObjectByPath(error, false, Collections.singletonList("metadata"));
+            if (metadataObj instanceof NamedList) {
+              metadata = (NamedList<String>) metadataObj;
+            } else if (metadataObj instanceof List) {
+              // NamedList parsed as List convert to NamedList again
+              final List<Object> list = (List<Object>) metadataObj;
+              metadata = new NamedList<>(list.size() / 2);
+              for (int i = 0; i < list.size(); i += 2) {
+                metadata.add((String) list.get(i), (String) list.get(i + 1));
+              }
+            } else if (metadataObj instanceof Map) {
+              metadata = new NamedList((Map) metadataObj);
+            }
+          }
+        } catch (final Exception ex) {
+          /* Ignored */
+        }
+        if (reason == null) {
+          final StringBuilder msg = new StringBuilder();
+          msg.append(response.getReason()).append("\n\n").append("request: ").append(response.getRequest().getMethod());
+          try {
+            reason = java.net.URLDecoder.decode(msg.toString(), FALLBACK_CHARSET.name());
+          } catch (final UnsupportedEncodingException e) {
+            // Quiet
+          }
+        }
+        final RemoteSolrException rss = new RemoteSolrException(serverBaseUrl, httpStatus, reason, null);
+        if (metadata != null)
+          rss.setMetadata(metadata);
+        throw rss;
+      }
+      return rsp;
+    } finally {
+      if (shouldClose) {
+        try {
+          is.close();
+          assert ObjectReleaseTracker.release(is);
+        } catch (final IOException e) {
+          // quitely
+        }
+      }
+    }
+  }
+
+  public void setRequestWriter(final RequestWriter requestWriter) {
+    this.requestWriter = requestWriter;
+  }
+
+  protected RequestWriter getRequestWriter() {
+    return requestWriter;
+  }
+
+  public void setFollowRedirects(final boolean follow) {
+    httpClient.setFollowRedirects(follow);
+  }
+
+  public String getBaseURL() {
+    return serverBaseUrl;
+  }
+
+  private static class AsyncTracker {
+    private static final int MAX_OUTSTANDING_REQUESTS = 1000;
+
+    // wait for async requests
+    private final Phaser phaser;
+    // maximum outstanding requests left
+    private final Semaphore available;
+    private final Request.QueuedListener queuedListener;
+    private final Response.CompleteListener completeListener;
+
+    AsyncTracker() {
+      // TODO: what about shared instances?
+      phaser = new Phaser(1);
+      available = new Semaphore(MAX_OUTSTANDING_REQUESTS, false);
+      queuedListener = request -> {
+        phaser.register();
+        try {
+          available.acquire();
+        } catch (final InterruptedException ignored) {
+
+        }
+      };
+      completeListener = result -> {
+        phaser.arriveAndDeregister();
+        available.release();
+      };
+    }
+
+    int getMaxRequestsQueuedPerDestination() {
+      // comfortably above max outstanding requests
+      return MAX_OUTSTANDING_REQUESTS * 3;
+    }
+
+    public void waitForComplete() {
+      phaser.arriveAndAwaitAdvance();
+      phaser.arriveAndDeregister();
+    }
+  }
+
+  public static class Builder {
+
+    private ModifiedHttp2SolrClient http2SolrClient;
+    private SSLConfig sslConfig = defaultSSLConfig;
+    private Integer idleTimeout;
+    private Integer connectionTimeout;
+    private Integer maxConnectionsPerHost;
+    private String basicAuthUser;
+    private String basicAuthPassword;
+    private boolean useHttp1_1 = Boolean.getBoolean("solr.http1");
+    protected String baseSolrUrl;
+    private ExecutorService executor;
+
+    public Builder() {
+    }
+
+    public Builder(final String baseSolrUrl) {
+      this.baseSolrUrl = baseSolrUrl;
+    }
+
+    public ModifiedHttp2SolrClient build() {
+      final ModifiedHttp2SolrClient client = new ModifiedHttp2SolrClient(baseSolrUrl, this);
+      try {
+        httpClientBuilderSetup(client);
+      } catch (final RuntimeException e) {
+        try {
+          client.close();
+        } catch (final Exception exceptionOnClose) {
+          e.addSuppressed(exceptionOnClose);
+        }
+        throw e;
+      }
+      return client;
+    }
+
+    private void httpClientBuilderSetup(final ModifiedHttp2SolrClient client) {
+      final String factoryClassName = System.getProperty(HttpClientUtil.SYS_PROP_HTTP_CLIENT_BUILDER_FACTORY);
+      if (factoryClassName != null) {
+        log.debug("Using Http Builder Factory: {}", factoryClassName);
+        ModifiedHttpClientBuilderFactory factory;
+        try {
+          if (factoryClassName.contains("Krb5HttpClientBuilder")) {
+            factory = new ModifiedKrb5HttpClientBuilder();
+          } else if (factoryClassName.contains("PreemptiveBasicAuthClientBuilderFactory")) {
+            factory = new ModifiedPreemptiveBasicAuthClientBuilderFactory();
+          } else {
+            throw new ClassNotFoundException("factoryClassName");
+          }
+        } catch (final ClassNotFoundException e) {
+          throw new RuntimeException("Unable to instantiate " + Http2SolrClient.class.getName(), e);
+        }
+        factory.setup(client);
+      }
+    }
+
+    /** Reuse {@code httpClient} connections pool */
+    public Builder withHttpClient(final ModifiedHttp2SolrClient httpClient) {
+      this.http2SolrClient = httpClient;
+      return this;
+    }
+
+    public Builder withExecutor(final ExecutorService executor) {
+      this.executor = executor;
+      return this;
+    }
+
+    public Builder withSSLConfig(final SSLConfig sslConfig) {
+      this.sslConfig = sslConfig;
+      return this;
+    }
+
+    public Builder withBasicAuthCredentials(final String user, final String pass) {
+      if (user != null || pass != null) {
+        if (user == null || pass == null) {
+          throw new IllegalStateException("Invalid Authentication credentials. Either both username and password or none must be provided");
+        }
+      }
+      this.basicAuthUser = user;
+      this.basicAuthPassword = pass;
+      return this;
+    }
+
+    /**
+     * Set maxConnectionsPerHost for http1 connections, maximum number http2 connections is limited by 4
+     */
+    public Builder maxConnectionsPerHost(final int max) {
+      this.maxConnectionsPerHost = max;
+      return this;
+    }
+
+    public Builder idleTimeout(final int idleConnectionTimeout) {
+      this.idleTimeout = idleConnectionTimeout;
+      return this;
+    }
+
+    public Builder useHttp1_1(final boolean useHttp1_1) {
+      this.useHttp1_1 = useHttp1_1;
+      return this;
+    }
+
+    public Builder connectionTimeout(final int connectionTimeOut) {
+      this.connectionTimeout = connectionTimeOut;
+      return this;
+    }
+  }
+
+  public Set<String> getQueryParams() {
+    return queryParams;
+  }
+
+  /**
+   * Expert Method
+   *
+   * @param queryParams set of param keys to only send via the query string Note that the param will be sent as a query string if the key is part of this Set or the SolrRequest's query params.
+   * @see org.apache.solr.client.solrj.SolrRequest#getQueryParams
+   */
+  public void setQueryParams(final Set<String> queryParams) {
+    this.queryParams = queryParams;
+  }
+
+  private ModifiableSolrParams calculateQueryParams(final Set<String> queryParamNames, final ModifiableSolrParams wparams) {
+    final ModifiableSolrParams queryModParams = new ModifiableSolrParams();
+    if (queryParamNames != null) {
+      for (final String param : queryParamNames) {
+        final String[] value = wparams.getParams(param);
+        if (value != null) {
+          for (final String v : value) {
+            queryModParams.add(param, v);
+          }
+          wparams.remove(param);
+        }
+      }
+    }
+    return queryModParams;
+  }
+
+  public ResponseParser getParser() {
+    return parser;
+  }
+
+  public void setParser(final ResponseParser processor) {
+    parser = processor;
+  }
+
+  public static void setDefaultSSLConfig(final SSLConfig sslConfig) {
+    ModifiedHttp2SolrClient.defaultSSLConfig = sslConfig;
+  }
+
+  // public for testing, only used by tests
+  public static void resetSslContextFactory() {
+    ModifiedHttp2SolrClient.defaultSSLConfig = null;
+  }
+
+  /* package-private for testing */
+  static SslContextFactory.Client getDefaultSslContextFactory() {
+    final String checkPeerNameStr = System.getProperty(HttpClientUtil.SYS_PROP_CHECK_PEER_NAME);
+    boolean sslCheckPeerName = true;
+    if (checkPeerNameStr == null || "false".equalsIgnoreCase(checkPeerNameStr)) {
+      sslCheckPeerName = false;
+    }
+
+    final SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(!sslCheckPeerName);
+
+    if (null != System.getProperty("javax.net.ssl.keyStore")) {
+      sslContextFactory.setKeyStorePath(System.getProperty("javax.net.ssl.keyStore"));
+    }
+    if (null != System.getProperty("javax.net.ssl.keyStorePassword")) {
+      sslContextFactory.setKeyStorePassword(System.getProperty("javax.net.ssl.keyStorePassword"));
+    }
+    if (null != System.getProperty("javax.net.ssl.keyStoreType")) {
+      sslContextFactory.setKeyStoreType(System.getProperty("javax.net.ssl.keyStoreType"));
+    }
+    if (null != System.getProperty("javax.net.ssl.trustStore")) {
+      sslContextFactory.setTrustStorePath(System.getProperty("javax.net.ssl.trustStore"));
+    }
+    if (null != System.getProperty("javax.net.ssl.trustStorePassword")) {
+      sslContextFactory.setTrustStorePassword(System.getProperty("javax.net.ssl.trustStorePassword"));
+    }
+    if (null != System.getProperty("javax.net.ssl.trustStoreType")) {
+      sslContextFactory.setTrustStoreType(System.getProperty("javax.net.ssl.trustStoreType"));
+    }
+
+    sslContextFactory.setEndpointIdentificationAlgorithm(System.getProperty("solr.jetty.ssl.verifyClientHostName"));
+
+    return sslContextFactory;
+  }
+
+}
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedHttpClientBuilderFactory.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedHttpClientBuilderFactory.java
new file mode 100644
index 0000000..83a9bd9
--- /dev/null
+++ b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedHttpClientBuilderFactory.java
@@ -0,0 +1,19 @@
+package org.apache.manifoldcf.agents.output.solr;
+
+import java.io.Closeable;
+
+import org.apache.solr.client.solrj.impl.SolrHttpClientBuilder;
+
+public interface ModifiedHttpClientBuilderFactory extends Closeable {
+
+  /**
+   * This method configures the {@linkplain SolrHttpClientBuilder} by overriding the configuration of passed SolrHttpClientBuilder or as a new instance.
+   *
+   * @param builder The instance of the {@linkplain SolrHttpClientBuilder} which should by configured (optional).
+   * @return the {@linkplain SolrHttpClientBuilder}
+   */
+  public SolrHttpClientBuilder getHttpClientBuilder(SolrHttpClientBuilder builder);
+
+  public default void setup(final ModifiedHttp2SolrClient client) {
+  }
+}
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedHttpMultipart.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedHttpMultipart.java
deleted file mode 100644
index 8005a11..0000000
--- a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedHttpMultipart.java
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-
-package org.apache.manifoldcf.agents.output.solr;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.http.entity.mime.content.ContentBody;
-import org.apache.http.entity.mime.MinimalField;
-import org.apache.http.entity.mime.FormBodyPart;
-import org.apache.http.entity.mime.HttpMultipartMode;
-import org.apache.http.entity.mime.MIME;
-import org.apache.http.entity.mime.Header;
-import org.apache.http.util.ByteArrayBuffer;
-
-/**
- * HttpMultipart represents a collection of MIME multipart encoded content bodies. This class is
- * capable of operating either in the strict (RFC 822, RFC 2045, RFC 2046 compliant) or
- * the browser compatible modes.
- *
- * @since 4.0
- */
-public class ModifiedHttpMultipart {
-
-    private static ByteArrayBuffer encode(
-            final Charset charset, final String string) {
-        ByteBuffer encoded = charset.encode(CharBuffer.wrap(string));
-        ByteArrayBuffer bab = new ByteArrayBuffer(encoded.remaining());
-        bab.append(encoded.array(), encoded.position(), encoded.remaining());
-        return bab;
-    }
-
-    private static void writeBytes(
-            final ByteArrayBuffer b, final OutputStream out) throws IOException {
-        out.write(b.buffer(), 0, b.length());
-    }
-
-    private static void writeBytes(
-            final String s, final Charset charset, final OutputStream out) throws IOException {
-        ByteArrayBuffer b = encode(charset, s);
-        writeBytes(b, out);
-    }
-
-    private static void writeBytes(
-            final String s, final OutputStream out) throws IOException {
-        ByteArrayBuffer b = encode(MIME.DEFAULT_CHARSET, s);
-        writeBytes(b, out);
-    }
-
-    private static void writeField(
-            final MinimalField field, final OutputStream out) throws IOException {
-        writeBytes(field.getName(), out);
-        writeBytes(FIELD_SEP, out);
-        writeBytes(field.getBody(), out);
-        writeBytes(CR_LF, out);
-    }
-
-    private static void writeField(
-            final MinimalField field, final Charset charset, final OutputStream out) throws IOException {
-        writeBytes(field.getName(), charset, out);
-        writeBytes(FIELD_SEP, out);
-        writeBytes(field.getBody(), charset, out);
-        writeBytes(CR_LF, out);
-    }
-
-    private static final ByteArrayBuffer FIELD_SEP = encode(MIME.DEFAULT_CHARSET, ": ");
-    private static final ByteArrayBuffer CR_LF = encode(MIME.DEFAULT_CHARSET, "\r\n");
-    private static final ByteArrayBuffer TWO_DASHES = encode(MIME.DEFAULT_CHARSET, "--");
-
-
-    private final String subType;
-    private final Charset charset;
-    private final String boundary;
-    private final List<FormBodyPart> parts;
-
-    private final HttpMultipartMode mode;
-
-    /**
-     * Creates an instance with the specified settings.
-     *
-     * @param subType mime subtype - must not be {@code null}
-     * @param charset the character set to use. May be {@code null}, in which case {@link MIME#DEFAULT_CHARSET} - i.e. US-ASCII - is used.
-     * @param boundary to use  - must not be {@code null}
-     * @param mode the mode to use
-     * @throws IllegalArgumentException if charset is null or boundary is null
-     */
-    public ModifiedHttpMultipart(final String subType, final Charset charset, final String boundary, HttpMultipartMode mode) {
-        super();
-        if (subType == null) {
-            throw new IllegalArgumentException("Multipart subtype may not be null");
-        }
-        if (boundary == null) {
-            throw new IllegalArgumentException("Multipart boundary may not be null");
-        }
-        this.subType = subType;
-        this.charset = charset != null ? charset : MIME.DEFAULT_CHARSET;
-        this.boundary = boundary;
-        this.parts = new ArrayList<FormBodyPart>();
-        this.mode = mode;
-    }
-
-    /**
-     * Creates an instance with the specified settings.
-     * Mode is set to {@link HttpMultipartMode#STRICT}
-     *
-     * @param subType mime subtype - must not be {@code null}
-     * @param charset the character set to use. May be {@code null}, in which case {@link MIME#DEFAULT_CHARSET} - i.e. US-ASCII - is used.
-     * @param boundary to use  - must not be {@code null}
-     * @throws IllegalArgumentException if charset is null or boundary is null
-     */
-    public ModifiedHttpMultipart(final String subType, final Charset charset, final String boundary) {
-        this(subType, charset, boundary, HttpMultipartMode.STRICT);
-    }
-
-    public ModifiedHttpMultipart(final String subType, final String boundary) {
-        this(subType, null, boundary);
-    }
-
-    public String getSubType() {
-        return this.subType;
-    }
-
-    public Charset getCharset() {
-        return this.charset;
-    }
-
-    public HttpMultipartMode getMode() {
-        return this.mode;
-    }
-
-    public List<FormBodyPart> getBodyParts() {
-        return this.parts;
-    }
-
-    public void addBodyPart(final FormBodyPart part) {
-        if (part == null) {
-            return;
-        }
-        this.parts.add(part);
-    }
-
-    public String getBoundary() {
-        return this.boundary;
-    }
-
-    private void doWriteTo(
-        final HttpMultipartMode mode,
-        final OutputStream out,
-        boolean writeContent) throws IOException {
-
-        ByteArrayBuffer boundary = encode(this.charset, getBoundary());
-        for (FormBodyPart part: this.parts) {
-            writeBytes(TWO_DASHES, out);
-            writeBytes(boundary, out);
-            writeBytes(CR_LF, out);
-
-            Header header = part.getHeader();
-
-            switch (mode) {
-            case STRICT:
-                for (MinimalField field: header) {
-                    writeField(field, this.charset, out);
-                }
-                break;
-            case BROWSER_COMPATIBLE:
-                // Only write Content-Disposition
-                // Use content charset
-                MinimalField cd = part.getHeader().getField(MIME.CONTENT_DISPOSITION);
-                writeField(cd, this.charset, out);
-                String filename = part.getBody().getFilename();
-                if (filename != null) {
-                    MinimalField ct = part.getHeader().getField(MIME.CONTENT_TYPE);
-                    writeField(ct, this.charset, out);
-                }
-                break;
-            }
-            writeBytes(CR_LF, out);
-
-            if (writeContent) {
-                part.getBody().writeTo(out);
-            }
-            writeBytes(CR_LF, out);
-        }
-        writeBytes(TWO_DASHES, out);
-        writeBytes(boundary, out);
-        writeBytes(TWO_DASHES, out);
-        writeBytes(CR_LF, out);
-    }
-
-    /**
-     * Writes out the content in the multipart/form encoding. This method
-     * produces slightly different formatting depending on its compatibility
-     * mode.
-     *
-     * @see #getMode()
-     */
-    public void writeTo(final OutputStream out) throws IOException {
-        doWriteTo(this.mode, out, true);
-    }
-
-    /**
-     * Determines the total length of the multipart content (content length of
-     * individual parts plus that of extra elements required to delimit the parts
-     * from one another). If any of the @{link BodyPart}s contained in this object
-     * is of a streaming entity of unknown length the total length is also unknown.
-     * <p>
-     * This method buffers only a small amount of data in order to determine the
-     * total length of the entire entity. The content of individual parts is not
-     * buffered.
-     *
-     * @return total length of the multipart entity if known, <code>-1</code>
-     *   otherwise.
-     */
-    public long getTotalLength() {
-        long contentLen = 0;
-        for (FormBodyPart part: this.parts) {
-            ContentBody body = part.getBody();
-            long len = body.getContentLength();
-            if (len >= 0) {
-                contentLen += len;
-            } else {
-                return -1;
-            }
-        }
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
-        try {
-            doWriteTo(this.mode, out, false);
-            byte[] extra = out.toByteArray();
-            return contentLen + extra.length;
-        } catch (IOException ex) {
-            // Should never happen
-            return -1;
-        }
-    }
-
-}
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedHttpSolrClient.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedHttpSolrClient.java
deleted file mode 100644
index 57e4d16..0000000
--- a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedHttpSolrClient.java
+++ /dev/null
@@ -1,567 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.apache.manifoldcf.agents.output.solr;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.ConnectException;
-import java.net.SocketTimeoutException;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.net.URLEncoder;
-
-import org.apache.http.Header;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.NameValuePair;
-import org.apache.http.NoHttpResponseException;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.entity.UrlEncodedFormEntity;
-import org.apache.http.client.methods.HttpDelete;
-import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.methods.HttpPut;
-import org.apache.http.client.methods.HttpRequestBase;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.client.params.ClientPNames;
-import org.apache.http.conn.ClientConnectionManager;
-import org.apache.http.entity.BasicHttpEntity;
-import org.apache.http.entity.ContentType;
-import org.apache.http.entity.InputStreamEntity;
-import org.apache.http.entity.mime.FormBodyPart;
-import org.apache.http.entity.mime.HttpMultipartMode;
-import org.apache.http.entity.mime.MultipartEntity;
-import org.apache.http.entity.mime.content.InputStreamBody;
-import org.apache.http.entity.mime.content.StringBody;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.http.message.BasicHeader;
-import org.apache.http.message.BasicNameValuePair;
-import org.apache.http.util.EntityUtils;
-import org.apache.http.entity.ContentType;
-import org.apache.http.entity.BasicHttpEntity;
-import org.apache.manifoldcf.core.util.URLDecoder;
-import org.apache.solr.client.solrj.ResponseParser;
-import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.SolrRequest;
-import org.apache.solr.client.solrj.SolrServerException;
-import org.apache.solr.client.solrj.V2RequestSupport;
-import org.apache.solr.client.solrj.request.RequestWriter;
-import org.apache.solr.client.solrj.request.V2Request;
-import org.apache.solr.client.solrj.impl.HttpSolrClient;
-import org.apache.solr.client.solrj.impl.BinaryResponseParser;
-import org.apache.solr.client.solrj.SolrServerException;
-import org.apache.solr.client.solrj.request.RequestWriter;
-import org.apache.solr.client.solrj.request.UpdateRequest;
-import org.apache.solr.client.solrj.response.UpdateResponse;
-import org.apache.solr.client.solrj.util.ClientUtils;
-import org.apache.solr.common.SolrException;
-import org.apache.solr.common.SolrInputDocument;
-import org.apache.solr.common.params.CommonParams;
-import org.apache.solr.common.params.ModifiableSolrParams;
-import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.common.util.ContentStream;
-import org.apache.solr.common.util.NamedList;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import java.nio.charset.Charset;
-
-/** This class overrides and somewhat changes the behavior of the
-* SolrJ HttpSolrServer class.  The point of all this is simply to get
-* the right information to Tika.  When SolrJ uses GET or POST but not
-* multipart-post, it does not include multipart headers that Tika uses -
-* specifically, the name of the document and the length of the document.
-* Patches have been submitted to the SOLR ticket queue to address this
-* problem in a method-insensitive way, but so far there has been no sign that
-* the Solr team is interested in committing them.
-*/
-public class ModifiedHttpSolrClient extends HttpSolrClient
-{
-  // Here we duplicate all the private fields we need
-  
-
-  private static final String DEFAULT_PATH = "/select";
-
-  private static Charset UTF8_CHARSET;
-
-  
-  private final HttpClient httpClient;
-  private final boolean useMultiPartPost = true;
-
-  public ModifiedHttpSolrClient(String baseURL, HttpClient client, ResponseParser parser, boolean allowCompression) {
-    super(baseURL, client, parser, allowCompression);
-    httpClient = client;
-  }
-
-  @Override
-  protected HttpRequestBase createMethod(SolrRequest request, String collection) throws IOException, SolrServerException {
-    if (request instanceof V2RequestSupport) {
-      request = ((V2RequestSupport) request).getV2Request();
-    }
-    SolrParams params = request.getParams();
-    RequestWriter.ContentWriter contentWriter = requestWriter.getContentWriter(request);
-    Collection<ContentStream> streams = contentWriter == null ? requestWriter.getContentStreams(request) : null;
-    String path = requestWriter.getPath(request);
-    if (path == null || !path.startsWith("/")) {
-      path = DEFAULT_PATH;
-    }
-    
-    ResponseParser parser = request.getResponseParser();
-    if (parser == null) {
-      parser = this.parser;
-    }
-    
-    // The parser 'wt=' and 'version=' params are used instead of the original
-    // params
-    ModifiableSolrParams wparams = new ModifiableSolrParams(params);
-    if (parser != null) {
-      wparams.set(CommonParams.WT, parser.getWriterType());
-      wparams.set(CommonParams.VERSION, parser.getVersion());
-    }
-    if (invariantParams != null) {
-      wparams.add(invariantParams);
-    }
-
-    String basePath = baseUrl;
-    if (collection != null)
-      basePath += "/" + collection;
-
-    if (request instanceof V2Request) {
-      if (System.getProperty("solr.v2RealPath") == null) {
-        basePath = baseUrl.replace("/solr", "/api");
-      } else {
-        basePath = baseUrl + "/____v2";
-      }
-    }
-
-    if (SolrRequest.METHOD.GET == request.getMethod()) {
-      if (streams != null || contentWriter != null) {
-        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "GET can't send streams!");
-      }
-
-      return new HttpGet(basePath + path + toQueryString(wparams, false));
-    }
-
-    if (SolrRequest.METHOD.DELETE == request.getMethod()) {
-      return new HttpDelete(basePath + path + toQueryString(wparams, false));
-    }
-
-    if (SolrRequest.METHOD.POST == request.getMethod() || SolrRequest.METHOD.PUT == request.getMethod()) {
-
-      //System.out.println("Post or put");
-      String url = basePath + path;
-      
-      // UpdateRequest uses PUT now, and ContentStreamUpdateHandler uses POST.
-      // We must override PUT with POST if multipart is required.
-      // If useMultipart is on, we fall back to getting streams directly from the request, for now.
-      final String contentWriterUrl = url + toQueryString(wparams, false);
-
-      final boolean isMultipart;
-      if (this.useMultiPartPost) {
-        final Collection<ContentStream> requestStreams = request.getContentStreams();
-        // Do we have streams?
-        if (requestStreams != null && requestStreams.size() > 0) {
-          
-          // Need to know if we have a stream name
-          boolean hasNullStreamName = false;
-          if (requestStreams != null) {
-            for (ContentStream cs : requestStreams) {
-              if (cs.getName() == null) {
-                hasNullStreamName = true;
-                break;
-              }
-            }
-          }
-
-          // Also, is the contentWriter URL too big?
-          final boolean urlTooBig = contentWriterUrl.length() > 4000;
-          //System.out.println("RequestStreams present? "+(requestStreams != null && requestStreams.size() > 0)+"; hasNullStreamName? "+hasNullStreamName+"; url length = "+contentWriterUrl.length());
-          isMultipart = requestStreams != null && requestStreams.size() > 0 && ((request.getMethod() == SolrRequest.METHOD.POST && !hasNullStreamName)  || urlTooBig);
-          if (isMultipart) {
-            //System.out.println("Overriding with multipart post");
-            streams = requestStreams;
-          }
-        } else {
-          isMultipart = false;
-        }
-      } else {
-        isMultipart = false;
-      }
-
-      final SolrRequest.METHOD methodToUse = isMultipart?SolrRequest.METHOD.POST:request.getMethod();
-
-      /*
-      final boolean isMultipart = ((this.useMultiPartPost && SolrRequest.METHOD.POST == methodToUse)
-          || (streams != null && streams.size() > 1)) && !hasNullStreamName;
-      */
-      //System.out.println("isMultipart = "+isMultipart);
-      
-      LinkedList<NameValuePair> postOrPutParams = new LinkedList<>();
-
-      if(contentWriter != null && !isMultipart) {
-        //System.out.println(" using contentwriter");
-        String fullQueryUrl = contentWriterUrl;
-        HttpEntityEnclosingRequestBase postOrPut = SolrRequest.METHOD.POST == methodToUse ?
-            new HttpPost(fullQueryUrl) : new HttpPut(fullQueryUrl);
-        postOrPut.addHeader("Content-Type",
-            contentWriter.getContentType());
-        postOrPut.setEntity(new BasicHttpEntity(){
-          @Override
-          public boolean isStreaming() {
-            return true;
-          }
-
-          @Override
-          public void writeTo(OutputStream outstream) throws IOException {
-            contentWriter.write(outstream);
-          }
-        });
-        return postOrPut;
-
-      } else if (streams == null || isMultipart) {
-        // send server list and request list as query string params
-        ModifiableSolrParams queryParams = calculateQueryParams(getQueryParams(), wparams);
-        queryParams.add(calculateQueryParams(request.getQueryParams(), wparams));
-        String fullQueryUrl = url + toQueryString(queryParams, false);
-        HttpEntityEnclosingRequestBase postOrPut = fillContentStream(methodToUse, streams, wparams, isMultipart, postOrPutParams, fullQueryUrl);
-        return postOrPut;
-      }
-      // It is has one stream, it is the post body, put the params in the URL
-      else {
-        String fullQueryUrl = url + toQueryString(wparams, false);
-        HttpEntityEnclosingRequestBase postOrPut = SolrRequest.METHOD.POST == methodToUse ?
-            new HttpPost(fullQueryUrl) : new HttpPut(fullQueryUrl);
-        fillSingleContentStream(streams, postOrPut);
-
-        return postOrPut;
-      }
-    }
-
-    throw new SolrServerException("Unsupported method: " + request.getMethod());
-
-  }
-
-  private void fillSingleContentStream(Collection<ContentStream> streams, HttpEntityEnclosingRequestBase postOrPut) throws IOException {
-    // Single stream as body
-    // Using a loop just to get the first one
-    final ContentStream[] contentStream = new ContentStream[1];
-    for (ContentStream content : streams) {
-      contentStream[0] = content;
-      break;
-    }
-    Long size = contentStream[0].getSize();
-    postOrPut.setEntity(new InputStreamEntity(contentStream[0].getStream(), size == null ? -1 : size) {
-      @Override
-      public Header getContentType() {
-        return new BasicHeader("Content-Type", contentStream[0].getContentType());
-      }
-
-      @Override
-      public boolean isRepeatable() {
-        return false;
-      }
-    });
-
-  }
-
-  private HttpEntityEnclosingRequestBase fillContentStream(SolrRequest.METHOD methodToUse, Collection<ContentStream> streams, ModifiableSolrParams wparams, boolean isMultipart, LinkedList<NameValuePair> postOrPutParams, String fullQueryUrl) throws IOException {
-    HttpEntityEnclosingRequestBase postOrPut = SolrRequest.METHOD.POST == methodToUse ?
-        new HttpPost(fullQueryUrl) : new HttpPut(fullQueryUrl);
-
-    if (!isMultipart) {
-      postOrPut.addHeader("Content-Type",
-          "application/x-www-form-urlencoded; charset=UTF-8");
-    }
-
-    List<FormBodyPart> parts = new LinkedList<>();
-    Iterator<String> iter = wparams.getParameterNamesIterator();
-    while (iter.hasNext()) {
-      String p = iter.next();
-      String[] vals = wparams.getParams(p);
-      if (vals != null) {
-        for (String v : vals) {
-          if (isMultipart) {
-            parts.add(new FormBodyPart(p, new StringBody(v, StandardCharsets.UTF_8)));
-          } else {
-            postOrPutParams.add(new BasicNameValuePair(p, v));
-          }
-        }
-      }
-    }
-
-    // TODO: remove deprecated - first simple attempt failed, see {@link MultipartEntityBuilder}
-    if (isMultipart && streams != null) {
-      for (ContentStream content : streams) {
-        String contentType = content.getContentType();
-        if (contentType == null) {
-          contentType = BinaryResponseParser.BINARY_CONTENT_TYPE; // default
-        }
-        String name = content.getName();
-        if (name == null) {
-          name = "";
-        }
-        parts.add(new FormBodyPart(encodeForHeader(name),
-            new InputStreamBody(
-                content.getStream(),
-                ContentType.parse(contentType),
-                encodeForHeader(content.getName()))));
-      }
-    }
-
-    //System.out.println("Using multipart post!");
-    if (parts.size() > 0) {
-      ModifiedMultipartEntity entity = new ModifiedMultipartEntity(HttpMultipartMode.STRICT, null, StandardCharsets.UTF_8);
-      //MultipartEntity entity = new MultipartEntity(HttpMultipartMode.STRICT);
-      for (FormBodyPart p : parts) {
-        entity.addPart(p);
-      }
-      postOrPut.setEntity(entity);
-    } else {
-      //not using multipart
-      postOrPut.setEntity(new UrlEncodedFormEntity(postOrPutParams, StandardCharsets.UTF_8));
-    }
-    return postOrPut;
-  }
-
-    /*
-  @Override
-  protected HttpRequestBase createMethod(final SolrRequest request, String collection) throws IOException, SolrServerException {
-    
-    SolrParams params = request.getParams();
-    RequestWriter.ContentWriter contentWriter = requestWriter.getContentWriter(request);
-    Collection<ContentStream> streams = contentWriter == null ? requestWriter.getContentStreams(request) : null;
-    String path = requestWriter.getPath(request);
-    if (path == null || !path.startsWith("/")) {
-      path = DEFAULT_PATH;
-    }
-    
-    ResponseParser parser = request.getResponseParser();
-    if (parser == null) {
-      parser = this.parser;
-    }
-    
-    // The parser 'wt=' and 'version=' params are used instead of the original
-    // params
-    ModifiableSolrParams wparams = new ModifiableSolrParams(params);
-    if (parser != null) {
-      wparams.set(CommonParams.WT, parser.getWriterType());
-      wparams.set(CommonParams.VERSION, parser.getVersion());
-    }
-    if (invariantParams != null) {
-      wparams.add(invariantParams);
-    }
-
-    String basePath = baseUrl;
-    if (collection != null)
-      basePath += "/" + collection;
-
-    if (SolrRequest.METHOD.GET == request.getMethod()) {
-      if (streams != null) {
-        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "GET can't send streams!");
-      }
-      return new HttpGet(basePath + path + toQueryString(wparams, false));
-    }
-
-    if (SolrRequest.METHOD.POST == request.getMethod() || SolrRequest.METHOD.PUT == request.getMethod()) {
-
-      String url = basePath + path;
-      
-      // Hack to allow short queries to go one way, and long queries to go another.
-      final boolean mustUseMultipart = request instanceof org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
-      if (mustUseMultipart) {
-        streams = requestWriter.getContentStreams(request);
-      }
-      
-      boolean hasNullStreamName = false;
-      if (streams != null) {
-        for (ContentStream cs : streams) {
-          if (cs.getName() == null) {
-            hasNullStreamName = true;
-            break;
-          }
-        }
-      }
-      boolean isMultipart = ((this.useMultiPartPost && SolrRequest.METHOD.POST == request.getMethod())
-          || (streams != null && streams.size() > 1)) && !hasNullStreamName;
-
-      LinkedList<NameValuePair> postOrPutParams = new LinkedList<>();
-      if(contentWriter != null && !isMultipart) {
-        String fullQueryUrl = url + toQueryString(wparams, false);
-        HttpEntityEnclosingRequestBase postOrPut = SolrRequest.METHOD.POST == request.getMethod() ?
-            new HttpPost(fullQueryUrl) : new HttpPut(fullQueryUrl);
-        postOrPut.addHeader("Content-Type",
-            contentWriter.getContentType());
-        postOrPut.setEntity(new BasicHttpEntity(){
-          @Override
-          public boolean isStreaming() {
-            return true;
-          }
-
-          @Override
-          public void writeTo(OutputStream outstream) throws IOException {
-            contentWriter.write(outstream);
-          }
-        });
-        return postOrPut;
-
-      } else if (streams == null || isMultipart) {
-        // send server list and request list as query string params
-        ModifiableSolrParams queryParams = calculateQueryParams(getQueryParams(), wparams);
-        queryParams.add(calculateQueryParams(request.getQueryParams(), wparams));
-        String fullQueryUrl = url + toQueryString(queryParams, false);
-        HttpEntityEnclosingRequestBase postOrPut = SolrRequest.METHOD.POST == request.getMethod() ?
-            new HttpPost(fullQueryUrl) : new HttpPut(fullQueryUrl);
-        if (!isMultipart) {
-          postOrPut.addHeader("Content-Type",
-              "application/x-www-form-urlencoded; charset=UTF-8");
-        }
-
-        List<FormBodyPart> parts = new LinkedList<>();
-        Iterator<String> iter = wparams.getParameterNamesIterator();
-        while (iter.hasNext()) {
-          String p = iter.next();
-          String[] vals = wparams.getParams(p);
-          if (vals != null) {
-            for (String v : vals) {
-              if (isMultipart) {
-                parts.add(new FormBodyPart(p, new StringBody(v, StandardCharsets.UTF_8)));
-              } else {
-                postOrPutParams.add(new BasicNameValuePair(p, v));
-              }
-            }
-          }
-        }
-
-        if (isMultipart && streams != null) {
-          for (ContentStream content : streams) {
-            String contentType = content.getContentType();
-            if (contentType == null) {
-              contentType = BinaryResponseParser.BINARY_CONTENT_TYPE; // default
-            }
-            String name = content.getName();
-            if (name == null) {
-              name = "";
-            }
-            parts.add(new FormBodyPart(encodeForHeader(name),
-                new InputStreamBody(
-                    content.getStream(),
-                    ContentType.parse(contentType),
-                    encodeForHeader(content.getName()))));
-          }
-        }
-
-        if (parts.size() > 0) {
-          ModifiedMultipartEntity entity = new ModifiedMultipartEntity(HttpMultipartMode.STRICT, null, StandardCharsets.UTF_8);
-          //MultipartEntity entity = new MultipartEntity(HttpMultipartMode.STRICT);
-          for (FormBodyPart p : parts) {
-            entity.addPart(p);
-          }
-          postOrPut.setEntity(entity);
-        } else {
-          //not using multipart
-          postOrPut.setEntity(new UrlEncodedFormEntity(postOrPutParams, StandardCharsets.UTF_8));
-        }
-
-        return postOrPut;
-      }
-      // It is has one stream, it is the post body, put the params in the URL
-      else {
-        String pstr = toQueryString(wparams, false);
-        HttpEntityEnclosingRequestBase postOrPut = SolrRequest.METHOD.POST == request.getMethod() ?
-            new HttpPost(url + pstr) : new HttpPut(url + pstr);
-
-        // Single stream as body
-        // Using a loop just to get the first one
-        final ContentStream[] contentStream = new ContentStream[1];
-        for (ContentStream content : streams) {
-          contentStream[0] = content;
-          break;
-        }
-        postOrPut.setEntity(new InputStreamEntity(contentStream[0].getStream(), -1) {
-          @Override
-          public Header getContentType() {
-            return new BasicHeader("Content-Type", contentStream[0].getContentType());
-          }
-
-          @Override
-          public boolean isRepeatable() {
-            return false;
-          }
-
-        });
-        return postOrPut;
-      }
-    }
-
-    throw new SolrServerException("Unsupported method: " + request.getMethod());
-
-  }
-*/
-  public static String toQueryString( SolrParams params, boolean xml ) {
-    StringBuilder sb = new StringBuilder(128);
-    try {
-      String amp = xml ? "&amp;" : "&";
-      boolean first=true;
-      Iterator<String> names = params.getParameterNamesIterator();
-      while( names.hasNext() ) {
-        String key = names.next();
-        String[] valarr = params.getParams( key );
-        if( valarr == null ) {
-          sb.append( first?"?":amp );
-          sb.append( URLEncoder.encode(key, "UTF-8") );
-          first=false;
-        }
-        else {
-          for (String val : valarr) {
-            sb.append( first? "?":amp );
-            sb.append(key);
-            if( val != null ) {
-              sb.append('=');
-              sb.append( URLEncoder.encode( val, "UTF-8" ) );
-            }
-            first=false;
-          }
-        }
-      }
-    }
-    catch (IOException e) {throw new RuntimeException(e);}  // can't happen
-    return sb.toString();
-  }
-  
-  // This is a hack added by KDW on 6/21/2017 because HttpClient doesn't do any character
-  // escaping when it puts together header and file names
-  private static String encodeForHeader(final String headerName) {
-    if (headerName == null) {
-      return null;
-    }
-    final StringBuilder sb = new StringBuilder();
-    for (int i = 0; i < headerName.length(); i++) {
-      final char x = headerName.charAt(i);
-      if (x == '"' || x == '\\' || x == '\r') {
-        sb.append("\\");
-      }
-      sb.append(x);
-    }
-    return sb.toString();
-  }
-  
-}
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedKrb5HttpClientBuilder.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedKrb5HttpClientBuilder.java
new file mode 100644
index 0000000..deb4dca
--- /dev/null
+++ b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedKrb5HttpClientBuilder.java
@@ -0,0 +1,225 @@
+package org.apache.manifoldcf.agents.output.solr;
+
+import java.lang.invoke.MethodHandles;
+import java.net.URI;
+import java.nio.file.Paths;
+import java.security.Principal;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.auth.AuthSchemeProvider;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.Credentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.config.AuthSchemes;
+import org.apache.http.config.Lookup;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.cookie.CookieSpecProvider;
+import org.apache.http.entity.BufferedHttpEntity;
+import org.apache.http.impl.auth.SPNegoSchemeFactory;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.solr.client.solrj.impl.HttpClientUtil;
+import org.apache.solr.client.solrj.impl.SolrHttpClientBuilder;
+import org.apache.solr.client.solrj.impl.SolrPortAwareCookieSpecFactory;
+import org.eclipse.jetty.client.HttpAuthenticationStore;
+import org.eclipse.jetty.client.WWWAuthenticationProtocolHandler;
+import org.eclipse.jetty.client.util.SPNEGOAuthentication;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ModifiedKrb5HttpClientBuilder implements ModifiedHttpClientBuilderFactory {
+
+  public static final String LOGIN_CONFIG_PROP = "java.security.auth.login.config";
+  private static final String SPNEGO_OID = "1.3.6.1.5.5.2";
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private static Configuration jaasConfig = new SolrJaasConfiguration();
+
+  public ModifiedKrb5HttpClientBuilder() {
+  }
+
+  /**
+   * The jaasConfig is static, which makes it problematic for testing in the same jvm. Call this function to regenerate the static config (this is not thread safe). Note: only used for tests
+   */
+  public static void regenerateJaasConfiguration() {
+    jaasConfig = new SolrJaasConfiguration();
+  }
+
+  public SolrHttpClientBuilder getBuilder() {
+    return getBuilder(HttpClientUtil.getHttpClientBuilder());
+  }
+
+  @Override
+  public void close() {
+    HttpClientUtil.removeRequestInterceptor(bufferedEntityInterceptor);
+  }
+
+  @Override
+  public SolrHttpClientBuilder getHttpClientBuilder(final SolrHttpClientBuilder builder) {
+    return builder == null ? getBuilder() : getBuilder(builder);
+  }
+
+  private SPNEGOAuthentication createSPNEGOAuthentication() {
+    final SPNEGOAuthentication authentication = new SPNEGOAuthentication(null) {
+
+      @Override
+      public boolean matches(final String type, final URI uri, final String realm) {
+        return this.getType().equals(type);
+      }
+    };
+    final String clientAppName = System.getProperty("solr.kerberos.jaas.appname", "Client");
+    final AppConfigurationEntry[] entries = jaasConfig.getAppConfigurationEntry(clientAppName);
+    if (entries == null) {
+      log.warn("Could not find login configuration entry for {}. SPNego authentication may not be successful.", clientAppName);
+      return authentication;
+    }
+    if (entries.length != 1) {
+      log.warn("Multiple login modules are specified in the configuration file");
+      return authentication;
+    }
+
+    final Map<String, ?> options = entries[0].getOptions();
+    setAuthenticationOptions(authentication, options, (String) options.get("principal"));
+    return authentication;
+  }
+
+  static void setAuthenticationOptions(final SPNEGOAuthentication authentication, final Map<String, ?> options, final String username) {
+    final String keyTab = (String) options.get("keyTab");
+    if (keyTab != null) {
+      authentication.setUserKeyTabPath(Paths.get(keyTab));
+    }
+    authentication.setServiceName("HTTP");
+    authentication.setUserName(username);
+    if ("true".equalsIgnoreCase((String) options.get("useTicketCache"))) {
+      authentication.setUseTicketCache(true);
+      final String ticketCachePath = (String) options.get("ticketCache");
+      if (ticketCachePath != null) {
+        authentication.setTicketCachePath(Paths.get(ticketCachePath));
+      }
+      authentication.setRenewTGT("true".equalsIgnoreCase((String) options.get("renewTGT")));
+    }
+  }
+
+  @Override
+  public void setup(final ModifiedHttp2SolrClient http2Client) {
+    final HttpAuthenticationStore authenticationStore = new HttpAuthenticationStore();
+    authenticationStore.addAuthentication(createSPNEGOAuthentication());
+    http2Client.getHttpClient().setAuthenticationStore(authenticationStore);
+    http2Client.getProtocolHandlers().put(new WWWAuthenticationProtocolHandler(http2Client.getHttpClient()));
+  }
+
+  public SolrHttpClientBuilder getBuilder(final SolrHttpClientBuilder builder) {
+    if (System.getProperty(LOGIN_CONFIG_PROP) != null) {
+      final String configValue = System.getProperty(LOGIN_CONFIG_PROP);
+
+      if (configValue != null) {
+        log.info("Setting up SPNego auth with config: {}", configValue);
+        final String useSubjectCredsProp = "javax.security.auth.useSubjectCredsOnly";
+        final String useSubjectCredsVal = System.getProperty(useSubjectCredsProp);
+
+        // "javax.security.auth.useSubjectCredsOnly" should be false so that the underlying
+        // authentication mechanism can load the credentials from the JAAS configuration.
+        if (useSubjectCredsVal == null) {
+          System.setProperty(useSubjectCredsProp, "false");
+        } else if (!useSubjectCredsVal.toLowerCase(Locale.ROOT).equals("false")) {
+          // Don't overwrite the prop value if it's already been written to something else,
+          // but log because it is likely the Credentials won't be loaded correctly.
+          log.warn("System Property: {} set to: {} not false.  SPNego authentication may not be successful.", useSubjectCredsProp, useSubjectCredsVal);
+        }
+
+        javax.security.auth.login.Configuration.setConfiguration(jaasConfig);
+        // Enable only SPNEGO authentication scheme.
+
+        builder.setAuthSchemeRegistryProvider(() -> {
+          final Lookup<AuthSchemeProvider> authProviders = RegistryBuilder.<AuthSchemeProvider>create().register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true, false)).build();
+          return authProviders;
+        });
+        // Get the credentials from the JAAS configuration rather than here
+        final Credentials useJaasCreds = new Credentials() {
+          @Override
+          public String getPassword() {
+            return null;
+          }
+
+          @Override
+          public Principal getUserPrincipal() {
+            return null;
+          }
+        };
+
+        HttpClientUtil.setCookiePolicy(SolrPortAwareCookieSpecFactory.POLICY_NAME);
+
+        builder.setCookieSpecRegistryProvider(() -> {
+          final SolrPortAwareCookieSpecFactory cookieFactory = new SolrPortAwareCookieSpecFactory();
+
+          final Lookup<CookieSpecProvider> cookieRegistry = RegistryBuilder.<CookieSpecProvider>create().register(SolrPortAwareCookieSpecFactory.POLICY_NAME, cookieFactory).build();
+
+          return cookieRegistry;
+        });
+
+        builder.setDefaultCredentialsProvider(() -> {
+          final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+          credentialsProvider.setCredentials(AuthScope.ANY, useJaasCreds);
+          return credentialsProvider;
+        });
+        HttpClientUtil.addRequestInterceptor(bufferedEntityInterceptor);
+      }
+    } else {
+      log.warn("{} is configured without specifying system property '{}'", getClass().getName(), LOGIN_CONFIG_PROP);
+    }
+
+    return builder;
+  }
+
+  // Set a buffered entity based request interceptor
+  private final HttpRequestInterceptor bufferedEntityInterceptor = (request, context) -> {
+    if (request instanceof HttpEntityEnclosingRequest) {
+      final HttpEntityEnclosingRequest enclosingRequest = ((HttpEntityEnclosingRequest) request);
+      final HttpEntity requestEntity = enclosingRequest.getEntity();
+      enclosingRequest.setEntity(new BufferedHttpEntity(requestEntity));
+    }
+  };
+
+  public static class SolrJaasConfiguration extends javax.security.auth.login.Configuration {
+
+    private javax.security.auth.login.Configuration baseConfig;
+
+    // the com.sun.security.jgss appNames
+    private final Set<String> initiateAppNames = new HashSet<>(Arrays.asList("com.sun.security.jgss.krb5.initiate", "com.sun.security.jgss.initiate"));
+
+    public SolrJaasConfiguration() {
+      try {
+
+        this.baseConfig = javax.security.auth.login.Configuration.getConfiguration();
+      } catch (final SecurityException e) {
+        this.baseConfig = null;
+      }
+    }
+
+    @Override
+    public AppConfigurationEntry[] getAppConfigurationEntry(final String appName) {
+      if (baseConfig == null)
+        return null;
+
+      if (log.isDebugEnabled()) {
+        log.debug("Login prop: {}", System.getProperty(LOGIN_CONFIG_PROP));
+      }
+
+      final String clientAppName = System.getProperty("solr.kerberos.jaas.appname", "Client");
+      if (initiateAppNames.contains(appName)) {
+        log.debug("Using AppConfigurationEntry for appName '{}' instead of: '{}'", clientAppName, appName);
+        return baseConfig.getAppConfigurationEntry(clientAppName);
+      }
+      return baseConfig.getAppConfigurationEntry(appName);
+    }
+  }
+}
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedLBHttp2SolrClient.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedLBHttp2SolrClient.java
new file mode 100644
index 0000000..f941560
--- /dev/null
+++ b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedLBHttp2SolrClient.java
@@ -0,0 +1,186 @@
+package org.apache.manifoldcf.agents.output.solr;
+
+import static org.apache.solr.common.params.CommonParams.ADMIN_PATHS;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.solr.client.solrj.ResponseParser;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.BaseHttpSolrClient;
+import org.apache.solr.client.solrj.request.IsUpdateRequest;
+import org.apache.solr.client.solrj.request.RequestWriter;
+import org.apache.solr.client.solrj.util.AsyncListener;
+import org.apache.solr.client.solrj.util.Cancellable;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.NamedList;
+import org.slf4j.MDC;
+
+public class ModifiedLBHttp2SolrClient extends ModifiedLBSolrClient {
+  private static final long serialVersionUID = -1147138830059067321L;
+  private final ModifiedHttp2SolrClient httpClient;
+
+  public ModifiedLBHttp2SolrClient(final ModifiedHttp2SolrClient httpClient, final String... baseSolrUrls) {
+    super(Arrays.asList(baseSolrUrls));
+    this.httpClient = httpClient;
+  }
+
+  @Override
+  protected SolrClient getClient(final String baseUrl) {
+    return httpClient;
+  }
+
+  @Override
+  public void setParser(final ResponseParser parser) {
+    super.setParser(parser);
+    this.httpClient.setParser(parser);
+  }
+
+  @Override
+  public void setRequestWriter(final RequestWriter writer) {
+    super.setRequestWriter(writer);
+    this.httpClient.setRequestWriter(writer);
+  }
+
+  @Override
+  public void setQueryParams(final Set<String> queryParams) {
+    super.setQueryParams(queryParams);
+    this.httpClient.setQueryParams(queryParams);
+  }
+
+  @Override
+  public void addQueryParams(final String queryOnlyParam) {
+    super.addQueryParams(queryOnlyParam);
+    this.httpClient.setQueryParams(getQueryParams());
+  }
+
+  public Cancellable asyncReq(final Req req, final AsyncListener<Rsp> asyncListener) {
+    final Rsp rsp = new Rsp();
+    final boolean isNonRetryable = req.request instanceof IsUpdateRequest || ADMIN_PATHS.contains(req.request.getPath());
+    final ServerIterator it = new ServerIterator(req, zombieServers);
+    asyncListener.onStart();
+    final AtomicBoolean cancelled = new AtomicBoolean(false);
+    final AtomicReference<Cancellable> currentCancellable = new AtomicReference<>();
+    final RetryListener retryListener = new RetryListener() {
+
+      @Override
+      public void onSuccess(final Rsp rsp) {
+        asyncListener.onSuccess(rsp);
+      }
+
+      @Override
+      public void onFailure(final Exception e, final boolean retryReq) {
+        if (retryReq) {
+          String url;
+          try {
+            url = it.nextOrError(e);
+          } catch (final SolrServerException ex) {
+            asyncListener.onFailure(e);
+            return;
+          }
+          try {
+            MDC.put("ModifiedLBSolrClient.url", url);
+            synchronized (cancelled) {
+              if (cancelled.get()) {
+                return;
+              }
+              final Cancellable cancellable = doRequest(url, req, rsp, isNonRetryable, it.isServingZombieServer(), this);
+              currentCancellable.set(cancellable);
+            }
+          } finally {
+            MDC.remove("ModifiedLBSolrClient.url");
+          }
+        } else {
+          asyncListener.onFailure(e);
+        }
+      }
+    };
+    try {
+      final Cancellable cancellable = doRequest(it.nextOrError(), req, rsp, isNonRetryable, it.isServingZombieServer(), retryListener);
+      currentCancellable.set(cancellable);
+    } catch (final SolrServerException e) {
+      asyncListener.onFailure(e);
+    }
+    return () -> {
+      synchronized (cancelled) {
+        cancelled.set(true);
+        if (currentCancellable.get() != null) {
+          currentCancellable.get().cancel();
+        }
+      }
+    };
+  }
+
+  private interface RetryListener {
+    void onSuccess(Rsp rsp);
+
+    void onFailure(Exception e, boolean retryReq);
+  }
+
+  private Cancellable doRequest(final String baseUrl, final Req req, final Rsp rsp, final boolean isNonRetryable, final boolean isZombie, final RetryListener listener) {
+    rsp.server = baseUrl;
+    req.getRequest().setBasePath(baseUrl);
+    return ((ModifiedHttp2SolrClient) getClient(baseUrl)).asyncRequest(req.getRequest(), null, new AsyncListener<NamedList<Object>>() {
+      @Override
+      public void onSuccess(final NamedList<Object> result) {
+        rsp.rsp = result;
+        if (isZombie) {
+          zombieServers.remove(baseUrl);
+        }
+        listener.onSuccess(rsp);
+      }
+
+      @Override
+      public void onFailure(final Throwable oe) {
+        try {
+          throw (Exception) oe;
+        } catch (final BaseHttpSolrClient.RemoteExecutionException e) {
+          listener.onFailure(e, false);
+        } catch (final SolrException e) {
+          // we retry on 404 or 403 or 503 or 500
+          // unless it's an update - then we only retry on connect exception
+          if (!isNonRetryable && RETRY_CODES.contains(e.code())) {
+            listener.onFailure((!isZombie) ? addZombie(baseUrl, e) : e, true);
+          } else {
+            // Server is alive but the request was likely malformed or invalid
+            if (isZombie) {
+              zombieServers.remove(baseUrl);
+            }
+            listener.onFailure(e, false);
+          }
+        } catch (final SocketException e) {
+          if (!isNonRetryable || e instanceof ConnectException) {
+            listener.onFailure((!isZombie) ? addZombie(baseUrl, e) : e, true);
+          } else {
+            listener.onFailure(e, false);
+          }
+        } catch (final SocketTimeoutException e) {
+          if (!isNonRetryable) {
+            listener.onFailure((!isZombie) ? addZombie(baseUrl, e) : e, true);
+          } else {
+            listener.onFailure(e, false);
+          }
+        } catch (final SolrServerException e) {
+          final Throwable rootCause = e.getRootCause();
+          if (!isNonRetryable && rootCause instanceof IOException) {
+            listener.onFailure((!isZombie) ? addZombie(baseUrl, e) : e, true);
+          } else if (isNonRetryable && rootCause instanceof ConnectException) {
+            listener.onFailure((!isZombie) ? addZombie(baseUrl, e) : e, true);
+          } else {
+            listener.onFailure(e, false);
+          }
+        } catch (final Exception e) {
+          listener.onFailure(new SolrServerException(e), false);
+        }
+      }
+    });
+  }
+
+}
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedLBHttpSolrClient.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedLBHttpSolrClient.java
deleted file mode 100644
index c9195c0..0000000
--- a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedLBHttpSolrClient.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.apache.manifoldcf.agents.output.solr;
-
-import org.apache.solr.client.solrj.impl.LBHttpSolrClient;
-import org.apache.solr.client.solrj.impl.HttpSolrClient;
-import org.apache.solr.client.solrj.impl.BinaryResponseParser;
-import org.apache.solr.client.solrj.request.RequestWriter;
-import org.apache.solr.client.solrj.*;
-import java.net.MalformedURLException;
-import org.apache.http.client.HttpClient;
-import java.util.Set;
-
-/** This class overrides and somewhat changes the behavior of the
-* SolrJ LBHttpSolrServer class.  This is so it instantiates our modified
-* HttpSolrServer class, so that multipart forms work.
-*/
-public class ModifiedLBHttpSolrClient extends LBHttpSolrClient
-{
-  private final HttpClient httpClient;
-  private final ResponseParser parser;
-  private final boolean allowCompression;
-  
-  public ModifiedLBHttpSolrClient(boolean allowCompression, String... solrServerUrls) throws MalformedURLException {
-    this(null, allowCompression, solrServerUrls);
-  }
-  
-  /** The provided httpClient should use a multi-threaded connection manager */ 
-  public ModifiedLBHttpSolrClient(HttpClient httpClient, boolean allowCompression, String... solrServerUrl)
-          throws MalformedURLException {
-    this(httpClient, new BinaryResponseParser(), allowCompression, solrServerUrl);
-  }
-
-  /** The provided httpClient should use a multi-threaded connection manager */  
-  public ModifiedLBHttpSolrClient(HttpClient httpClient, ResponseParser parser, boolean allowCompression, String... solrServerUrl)
-          throws MalformedURLException {
-    super(httpClient, parser, solrServerUrl);
-    this.httpClient = httpClient;
-    this.parser = parser;
-    this.allowCompression = allowCompression;
-  }
-  
-  @Override
-  protected HttpSolrClient makeSolrClient(String server) {
-    HttpSolrClient client = new ModifiedHttpSolrClient(server, httpClient, parser, allowCompression);
-    if (getRequestWriter() != null) {
-      client.setRequestWriter(getRequestWriter());
-    }
-    if (getQueryParams() != null) {
-      client.setQueryParams(getQueryParams());
-    }
-    return client;
-  }
-
-}
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedLBSolrClient.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedLBSolrClient.java
new file mode 100644
index 0000000..dc8bbf2
--- /dev/null
+++ b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedLBSolrClient.java
@@ -0,0 +1,691 @@
+package org.apache.manifoldcf.agents.output.solr;
+
+import static org.apache.solr.common.params.CommonParams.ADMIN_PATHS;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.net.ConnectException;
+import java.net.MalformedURLException;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.solr.client.solrj.ResponseParser;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.BaseHttpSolrClient;
+import org.apache.solr.client.solrj.request.IsUpdateRequest;
+import org.apache.solr.client.solrj.request.QueryRequest;
+import org.apache.solr.client.solrj.request.RequestWriter;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.ExecutorUtil;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SolrNamedThreadFactory;
+import org.slf4j.MDC;
+
+public abstract class ModifiedLBSolrClient extends SolrClient {
+
+  // defaults
+  protected static final Set<Integer> RETRY_CODES = new HashSet<>(Arrays.asList(404, 403, 503, 500));
+  private static final int CHECK_INTERVAL = 60 * 1000; // 1 minute between checks
+  private static final int NONSTANDARD_PING_LIMIT = 5; // number of times we'll ping dead servers not in the server list
+
+  // keys to the maps are currently of the form "http://localhost:8983/solr"
+  // which should be equivalent to HttpSolrServer.getBaseURL()
+  private final Map<String, ServerWrapper> aliveServers = new LinkedHashMap<>();
+  // access to aliveServers should be synchronized on itself
+
+  protected final Map<String, ServerWrapper> zombieServers = new ConcurrentHashMap<>();
+
+  // changes to aliveServers are reflected in this array, no need to synchronize
+  private volatile ServerWrapper[] aliveServerList = new ServerWrapper[0];
+
+  private volatile ScheduledExecutorService aliveCheckExecutor;
+
+  private int interval = CHECK_INTERVAL;
+  private final AtomicInteger counter = new AtomicInteger(-1);
+
+  private static final SolrQuery solrQuery = new SolrQuery("*:*");
+  protected volatile ResponseParser parser;
+  protected volatile RequestWriter requestWriter;
+
+  protected Set<String> queryParams = new HashSet<>();
+
+  static {
+    solrQuery.setRows(0);
+    /**
+     * Default sort (if we don't supply a sort) is by score and since we request 0 rows any sorting and scoring is not necessary. SolrQuery.DOCID schema-independently specifies a non-scoring sort.
+     * <code>_docid_ asc</code> sort is efficient, <code>_docid_ desc</code> sort is not, so choose ascending DOCID sort.
+     */
+    solrQuery.setSort(SolrQuery.DOCID, SolrQuery.ORDER.asc);
+    // not a top-level request, we are interested only in the server being sent to i.e. it need not
+    // distribute our request to further servers
+    solrQuery.setDistrib(false);
+  }
+
+  protected static class ServerWrapper {
+    final String baseUrl;
+
+    // "standard" servers are used by default. They normally live in the alive list
+    // and move to the zombie list when unavailable. When they become available again,
+    // they move back to the alive list.
+    boolean standard = true;
+
+    int failedPings = 0;
+
+    ServerWrapper(final String baseUrl) {
+      this.baseUrl = baseUrl;
+    }
+
+    public String getBaseUrl() {
+      return baseUrl;
+    }
+
+    @Override
+    public String toString() {
+      return baseUrl;
+    }
+
+    @Override
+    public int hashCode() {
+      return baseUrl.hashCode();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+      if (this == obj)
+        return true;
+      if (!(obj instanceof ServerWrapper))
+        return false;
+      return baseUrl.equals(((ServerWrapper) obj).baseUrl);
+    }
+  }
+
+  protected static class ServerIterator {
+    String serverStr;
+    List<String> skipped;
+    int numServersTried;
+    Iterator<String> it;
+    Iterator<String> skippedIt;
+    String exceptionMessage;
+    long timeAllowedNano;
+    long timeOutTime;
+
+    final Map<String, ServerWrapper> zombieServers;
+    final Req req;
+
+    public ServerIterator(final Req req, final Map<String, ServerWrapper> zombieServers) {
+      this.it = req.getServers().iterator();
+      this.req = req;
+      this.zombieServers = zombieServers;
+      this.timeAllowedNano = getTimeAllowedInNanos(req.getRequest());
+      this.timeOutTime = System.nanoTime() + timeAllowedNano;
+      fetchNext();
+    }
+
+    public synchronized boolean hasNext() {
+      return serverStr != null;
+    }
+
+    private void fetchNext() {
+      serverStr = null;
+      if (req.numServersToTry != null && numServersTried > req.numServersToTry) {
+        exceptionMessage = "Time allowed to handle this request exceeded";
+        return;
+      }
+
+      while (it.hasNext()) {
+        serverStr = it.next();
+        serverStr = normalize(serverStr);
+        // if the server is currently a zombie, just skip to the next one
+        final ServerWrapper wrapper = zombieServers.get(serverStr);
+        if (wrapper != null) {
+          final int numDeadServersToTry = req.getNumDeadServersToTry();
+          if (numDeadServersToTry > 0) {
+            if (skipped == null) {
+              skipped = new ArrayList<>(numDeadServersToTry);
+              skipped.add(wrapper.getBaseUrl());
+            } else if (skipped.size() < numDeadServersToTry) {
+              skipped.add(wrapper.getBaseUrl());
+            }
+          }
+          continue;
+        }
+
+        break;
+      }
+      if (serverStr == null && skipped != null) {
+        if (skippedIt == null) {
+          skippedIt = skipped.iterator();
+        }
+        if (skippedIt.hasNext()) {
+          serverStr = skippedIt.next();
+        }
+      }
+    }
+
+    boolean isServingZombieServer() {
+      return skippedIt != null;
+    }
+
+    public synchronized String nextOrError() throws SolrServerException {
+      return nextOrError(null);
+    }
+
+    public synchronized String nextOrError(final Exception previousEx) throws SolrServerException {
+      String suffix = "";
+      if (previousEx == null) {
+        suffix = ":" + zombieServers.keySet();
+      }
+      // Skipping check time exceeded for the first request
+      if (numServersTried > 0 && isTimeExceeded(timeAllowedNano, timeOutTime)) {
+        throw new SolrServerException("Time allowed to handle this request exceeded" + suffix, previousEx);
+      }
+      if (serverStr == null) {
+        throw new SolrServerException("No live SolrServers available to handle this request" + suffix, previousEx);
+      }
+      numServersTried++;
+      if (req.getNumServersToTry() != null && numServersTried > req.getNumServersToTry()) {
+        throw new SolrServerException("No live SolrServers available to handle this request:" + " numServersTried=" + numServersTried + " numServersToTry=" + req.getNumServersToTry() + suffix,
+            previousEx);
+      }
+      final String rs = serverStr;
+      fetchNext();
+      return rs;
+    }
+  }
+
+  // Req should be parameterized too, but that touches a whole lotta code
+  public static class Req {
+    protected SolrRequest<?> request;
+    protected List<String> servers;
+    protected int numDeadServersToTry;
+    private final Integer numServersToTry;
+
+    public Req(final SolrRequest<?> request, final List<String> servers) {
+      this(request, servers, null);
+    }
+
+    public Req(final SolrRequest<?> request, final List<String> servers, final Integer numServersToTry) {
+      this.request = request;
+      this.servers = servers;
+      this.numDeadServersToTry = servers.size();
+      this.numServersToTry = numServersToTry;
+    }
+
+    public SolrRequest<?> getRequest() {
+      return request;
+    }
+
+    public List<String> getServers() {
+      return servers;
+    }
+
+    /**
+     * @return the number of dead servers to try if there are no live servers left
+     */
+    public int getNumDeadServersToTry() {
+      return numDeadServersToTry;
+    }
+
+    /**
+     * @param numDeadServersToTry The number of dead servers to try if there are no live servers left. Defaults to the number of servers in this request.
+     */
+    public void setNumDeadServersToTry(final int numDeadServersToTry) {
+      this.numDeadServersToTry = numDeadServersToTry;
+    }
+
+    public Integer getNumServersToTry() {
+      return numServersToTry;
+    }
+  }
+
+  public static class Rsp {
+    protected String server;
+    protected NamedList<Object> rsp;
+
+    /** The response from the server */
+    public NamedList<Object> getResponse() {
+      return rsp;
+    }
+
+    /** The server that returned the response */
+    public String getServer() {
+      return server;
+    }
+  }
+
+  public ModifiedLBSolrClient(final List<String> baseSolrUrls) {
+    if (!baseSolrUrls.isEmpty()) {
+      for (final String s : baseSolrUrls) {
+        final ServerWrapper wrapper = createServerWrapper(s);
+        aliveServers.put(wrapper.getBaseUrl(), wrapper);
+      }
+      updateAliveList();
+    }
+  }
+
+  protected void updateAliveList() {
+    synchronized (aliveServers) {
+      aliveServerList = aliveServers.values().toArray(new ServerWrapper[0]);
+    }
+  }
+
+  protected ServerWrapper createServerWrapper(final String baseUrl) {
+    return new ServerWrapper(baseUrl);
+  }
+
+  public Set<String> getQueryParams() {
+    return queryParams;
+  }
+
+  /**
+   * Expert Method.
+   *
+   * @param queryParams set of param keys to only send via the query string
+   */
+  public void setQueryParams(final Set<String> queryParams) {
+    this.queryParams = queryParams;
+  }
+
+  public void addQueryParams(final String queryOnlyParam) {
+    this.queryParams.add(queryOnlyParam);
+  }
+
+  public static String normalize(String server) {
+    if (server.endsWith("/"))
+      server = server.substring(0, server.length() - 1);
+    return server;
+  }
+
+  /**
+   * Tries to query a live server from the list provided in Req. Servers in the dead pool are skipped. If a request fails due to an IOException, the server is moved to the dead pool for a certain
+   * period of time, or until a test request on that server succeeds.
+   *
+   * <p>
+   * Servers are queried in the exact order given (except servers currently in the dead pool are skipped). If no live servers from the provided list remain to be tried, a number of previously skipped
+   * dead servers will be tried. Req.getNumDeadServersToTry() controls how many dead servers will be tried.
+   *
+   * <p>
+   * If no live servers are found a SolrServerException is thrown.
+   *
+   * @param req contains both the request as well as the list of servers to query
+   * @return the result of the request
+   * @throws IOException If there is a low-level I/O error.
+   */
+  public Rsp request(final Req req) throws SolrServerException, IOException {
+    final Rsp rsp = new Rsp();
+    Exception ex = null;
+    final boolean isNonRetryable = req.request instanceof IsUpdateRequest || ADMIN_PATHS.contains(req.request.getPath());
+    final ServerIterator serverIterator = new ServerIterator(req, zombieServers);
+    String serverStr;
+    while ((serverStr = serverIterator.nextOrError(ex)) != null) {
+      try {
+        MDC.put("ModifiedLBSolrClient.url", serverStr);
+        ex = doRequest(serverStr, req, rsp, isNonRetryable, serverIterator.isServingZombieServer());
+        if (ex == null) {
+          return rsp; // SUCCESS
+        }
+      } finally {
+        MDC.remove("ModifiedLBSolrClient.url");
+      }
+    }
+    throw new SolrServerException("No live SolrServers available to handle this request:" + zombieServers.keySet(), ex);
+  }
+
+  /**
+   * @return time allowed in nanos, returns -1 if no time_allowed is specified.
+   */
+  private static long getTimeAllowedInNanos(final SolrRequest<?> req) {
+    final SolrParams reqParams = req.getParams();
+    return reqParams == null ? -1 : TimeUnit.NANOSECONDS.convert(reqParams.getInt(CommonParams.TIME_ALLOWED, -1), TimeUnit.MILLISECONDS);
+  }
+
+  private static boolean isTimeExceeded(final long timeAllowedNano, final long timeOutTime) {
+    return timeAllowedNano > 0 && System.nanoTime() > timeOutTime;
+  }
+
+  protected Exception doRequest(final String baseUrl, final Req req, final Rsp rsp, final boolean isNonRetryable, final boolean isZombie) throws SolrServerException, IOException {
+    Exception ex = null;
+    try {
+      rsp.server = baseUrl;
+      req.getRequest().setBasePath(baseUrl);
+      rsp.rsp = getClient(baseUrl).request(req.getRequest(), (String) null);
+      if (isZombie) {
+        zombieServers.remove(baseUrl);
+      }
+    } catch (final BaseHttpSolrClient.RemoteExecutionException e) {
+      throw e;
+    } catch (final SolrException e) {
+      // we retry on 404 or 403 or 503 or 500
+      // unless it's an update - then we only retry on connect exception
+      if (!isNonRetryable && RETRY_CODES.contains(e.code())) {
+        ex = (!isZombie) ? addZombie(baseUrl, e) : e;
+      } else {
+        // Server is alive but the request was likely malformed or invalid
+        if (isZombie) {
+          zombieServers.remove(baseUrl);
+        }
+        throw e;
+      }
+    } catch (final SocketException e) {
+      if (!isNonRetryable || e instanceof ConnectException) {
+        ex = (!isZombie) ? addZombie(baseUrl, e) : e;
+      } else {
+        throw e;
+      }
+    } catch (final SocketTimeoutException e) {
+      if (!isNonRetryable) {
+        ex = (!isZombie) ? addZombie(baseUrl, e) : e;
+      } else {
+        throw e;
+      }
+    } catch (final SolrServerException e) {
+      final Throwable rootCause = e.getRootCause();
+      if (!isNonRetryable && rootCause instanceof IOException) {
+        ex = (!isZombie) ? addZombie(baseUrl, e) : e;
+      } else if (isNonRetryable && rootCause instanceof ConnectException) {
+        ex = (!isZombie) ? addZombie(baseUrl, e) : e;
+      } else {
+        throw e;
+      }
+    } catch (final Exception e) {
+      throw new SolrServerException(e);
+    }
+
+    return ex;
+  }
+
+  protected abstract SolrClient getClient(String baseUrl);
+
+  protected Exception addZombie(final String serverStr, final Exception e) {
+    final ServerWrapper wrapper = createServerWrapper(serverStr);
+    wrapper.standard = false;
+    zombieServers.put(serverStr, wrapper);
+    startAliveCheckExecutor();
+    return e;
+  }
+
+  /**
+   * LBHttpSolrServer keeps pinging the dead servers at fixed interval to find if it is alive. Use this to set that interval
+   *
+   * @param interval time in milliseconds
+   */
+  public void setAliveCheckInterval(final int interval) {
+    if (interval <= 0) {
+      throw new IllegalArgumentException("Alive check interval must be " + "positive, specified value = " + interval);
+    }
+    this.interval = interval;
+  }
+
+  private void startAliveCheckExecutor() {
+    // double-checked locking, but it's OK because we don't *do* anything with aliveCheckExecutor
+    // if it's not null.
+    if (aliveCheckExecutor == null) {
+      synchronized (this) {
+        if (aliveCheckExecutor == null) {
+          aliveCheckExecutor = Executors.newSingleThreadScheduledExecutor(new SolrNamedThreadFactory("aliveCheckExecutor"));
+          aliveCheckExecutor.scheduleAtFixedRate(getAliveCheckRunner(new WeakReference<>(this)), this.interval, this.interval, TimeUnit.MILLISECONDS);
+        }
+      }
+    }
+  }
+
+  private static Runnable getAliveCheckRunner(final WeakReference<ModifiedLBSolrClient> lbRef) {
+    return () -> {
+      final ModifiedLBSolrClient lb = lbRef.get();
+      if (lb != null && lb.zombieServers != null) {
+        for (final Object zombieServer : lb.zombieServers.values()) {
+          lb.checkAZombieServer((ServerWrapper) zombieServer);
+        }
+      }
+    };
+  }
+
+  public ResponseParser getParser() {
+    return parser;
+  }
+
+  /**
+   * Changes the {@link ResponseParser} that will be used for the internal SolrServer objects.
+   *
+   * @param parser Default Response Parser chosen to parse the response if the parser were not specified as part of the request.
+   * @see org.apache.solr.client.solrj.SolrRequest#getResponseParser()
+   */
+  public void setParser(final ResponseParser parser) {
+    this.parser = parser;
+  }
+
+  /**
+   * Changes the {@link RequestWriter} that will be used for the internal SolrServer objects.
+   *
+   * @param requestWriter Default RequestWriter, used to encode requests sent to the server.
+   */
+  public void setRequestWriter(final RequestWriter requestWriter) {
+    this.requestWriter = requestWriter;
+  }
+
+  public RequestWriter getRequestWriter() {
+    return requestWriter;
+  }
+
+  private void checkAZombieServer(final ServerWrapper zombieServer) {
+    try {
+      final QueryRequest queryRequest = new QueryRequest(solrQuery);
+      queryRequest.setBasePath(zombieServer.baseUrl);
+      final QueryResponse resp = queryRequest.process(getClient(zombieServer.getBaseUrl()));
+      if (resp.getStatus() == 0) {
+        // server has come back up.
+        // make sure to remove from zombies before adding to alive to avoid a race condition
+        // where another thread could mark it down, move it back to zombie, and then we delete
+        // from zombie and lose it forever.
+        final ServerWrapper wrapper = zombieServers.remove(zombieServer.getBaseUrl());
+        if (wrapper != null) {
+          wrapper.failedPings = 0;
+          if (wrapper.standard) {
+            addToAlive(wrapper);
+          }
+        } else {
+          // something else already moved the server from zombie to alive
+        }
+      }
+    } catch (final Exception e) {
+      // Expected. The server is still down.
+      zombieServer.failedPings++;
+
+      // If the server doesn't belong in the standard set belonging to this load balancer
+      // then simply drop it after a certain number of failed pings.
+      if (!zombieServer.standard && zombieServer.failedPings >= NONSTANDARD_PING_LIMIT) {
+        zombieServers.remove(zombieServer.getBaseUrl());
+      }
+    }
+  }
+
+  private ServerWrapper removeFromAlive(final String key) {
+    synchronized (aliveServers) {
+      final ServerWrapper wrapper = aliveServers.remove(key);
+      if (wrapper != null)
+        updateAliveList();
+      return wrapper;
+    }
+  }
+
+  private void addToAlive(final ServerWrapper wrapper) {
+    synchronized (aliveServers) {
+      final ServerWrapper prev = aliveServers.put(wrapper.getBaseUrl(), wrapper);
+      // TODO: warn if there was a previous entry?
+      updateAliveList();
+    }
+  }
+
+  public void addSolrServer(final String server) throws MalformedURLException {
+    addToAlive(createServerWrapper(server));
+  }
+
+  public String removeSolrServer(String server) {
+    try {
+      server = new URL(server).toExternalForm();
+    } catch (final MalformedURLException e) {
+      throw new RuntimeException(e);
+    }
+    if (server.endsWith("/")) {
+      server = server.substring(0, server.length() - 1);
+    }
+
+    // there is a small race condition here - if the server is in the process of being moved between
+    // lists, we could fail to remove it.
+    removeFromAlive(server);
+    zombieServers.remove(server);
+    return null;
+  }
+
+  /**
+   * Tries to query a live server. A SolrServerException is thrown if all servers are dead. If the request failed due to IOException then the live server is moved to dead pool and the request is
+   * retried on another live server. After live servers are exhausted, any servers previously marked as dead will be tried before failing the request.
+   *
+   * @param request the SolrRequest.
+   * @return response
+   * @throws IOException If there is a low-level I/O error.
+   */
+  @Override
+  public NamedList<Object> request(final SolrRequest<?> request, final String collection) throws SolrServerException, IOException {
+    return request(request, collection, null);
+  }
+
+  public NamedList<Object> request(final SolrRequest<?> request, final String collection, final Integer numServersToTry) throws SolrServerException, IOException {
+    Exception ex = null;
+    final ServerWrapper[] serverList = aliveServerList;
+
+    final int maxTries = (numServersToTry == null ? serverList.length : numServersToTry.intValue());
+    int numServersTried = 0;
+    Map<String, ServerWrapper> justFailed = null;
+
+    boolean timeAllowedExceeded = false;
+    final long timeAllowedNano = getTimeAllowedInNanos(request);
+    final long timeOutTime = System.nanoTime() + timeAllowedNano;
+    for (int attempts = 0; attempts < maxTries; attempts++) {
+      if (timeAllowedExceeded = isTimeExceeded(timeAllowedNano, timeOutTime)) {
+        break;
+      }
+
+      final ServerWrapper wrapper = pickServer(serverList, request);
+      try {
+        ++numServersTried;
+        request.setBasePath(wrapper.baseUrl);
+        return getClient(wrapper.getBaseUrl()).request(request, collection);
+      } catch (final SolrException e) {
+        // Server is alive but the request was malformed or invalid
+        throw e;
+      } catch (final SolrServerException e) {
+        if (e.getRootCause() instanceof IOException) {
+          ex = e;
+          moveAliveToDead(wrapper);
+          if (justFailed == null)
+            justFailed = new HashMap<>();
+          justFailed.put(wrapper.getBaseUrl(), wrapper);
+        } else {
+          throw e;
+        }
+      } catch (final Exception e) {
+        throw new SolrServerException(e);
+      }
+    }
+
+    // try other standard servers that we didn't try just now
+    for (final ServerWrapper wrapper : zombieServers.values()) {
+      if (timeAllowedExceeded = isTimeExceeded(timeAllowedNano, timeOutTime)) {
+        break;
+      }
+
+      if (wrapper.standard == false || justFailed != null && justFailed.containsKey(wrapper.getBaseUrl()))
+        continue;
+      try {
+        ++numServersTried;
+        request.setBasePath(wrapper.baseUrl);
+        final NamedList<Object> rsp = getClient(wrapper.baseUrl).request(request, collection);
+        // remove from zombie list *before* adding to alive to avoid a race that could lose a server
+        zombieServers.remove(wrapper.getBaseUrl());
+        addToAlive(wrapper);
+        return rsp;
+      } catch (final SolrException e) {
+        // Server is alive but the request was malformed or invalid
+        throw e;
+      } catch (final SolrServerException e) {
+        if (e.getRootCause() instanceof IOException) {
+          ex = e;
+          // still dead
+        } else {
+          throw e;
+        }
+      } catch (final Exception e) {
+        throw new SolrServerException(e);
+      }
+    }
+
+    final String solrServerExceptionMessage;
+    if (timeAllowedExceeded) {
+      solrServerExceptionMessage = "Time allowed to handle this request exceeded";
+    } else {
+      if (numServersToTry != null && numServersTried > numServersToTry.intValue()) {
+        solrServerExceptionMessage = "No live SolrServers available to handle this request:" + " numServersTried=" + numServersTried + " numServersToTry=" + numServersToTry.intValue();
+      } else {
+        solrServerExceptionMessage = "No live SolrServers available to handle this request";
+      }
+    }
+    if (ex == null) {
+      throw new SolrServerException(solrServerExceptionMessage);
+    } else {
+      throw new SolrServerException(solrServerExceptionMessage, ex);
+    }
+  }
+
+  /**
+   * Pick a server from list to execute request. By default servers are picked in round-robin manner, custom classes can override this method for more advance logic
+   *
+   * @param aliveServerList list of currently alive servers
+   * @param request         the request will be sent to the picked server
+   * @return the picked server
+   */
+  protected ServerWrapper pickServer(final ServerWrapper[] aliveServerList, final SolrRequest<?> request) {
+    final int count = counter.incrementAndGet() & Integer.MAX_VALUE;
+    return aliveServerList[count % aliveServerList.length];
+  }
+
+  private void moveAliveToDead(ServerWrapper wrapper) {
+    wrapper = removeFromAlive(wrapper.getBaseUrl());
+    if (wrapper == null)
+      return; // another thread already detected the failure and removed it
+    zombieServers.put(wrapper.getBaseUrl(), wrapper);
+    startAliveCheckExecutor();
+  }
+
+  @Override
+  public void close() {
+    synchronized (this) {
+      if (aliveCheckExecutor != null) {
+        aliveCheckExecutor.shutdownNow();
+        ExecutorUtil.shutdownAndAwaitTermination(aliveCheckExecutor);
+      }
+    }
+  }
+
+}
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedMultipartEntity.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedMultipartEntity.java
deleted file mode 100644
index 643df72..0000000
--- a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedMultipartEntity.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-
-package org.apache.manifoldcf.agents.output.solr;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.charset.Charset;
-import java.util.Random;
-
-import org.apache.http.Header;
-import org.apache.http.HttpEntity;
-import org.apache.http.entity.mime.content.ContentBody;
-import org.apache.http.entity.mime.FormBodyPart;
-import org.apache.http.entity.mime.HttpMultipartMode;
-import org.apache.http.entity.mime.MIME;
-import org.apache.http.message.BasicHeader;
-import org.apache.http.protocol.HTTP;
-
-/**
- * Multipart/form coded HTTP entity consisting of multiple body parts.
- *
- * @since 4.0
- */
-public class ModifiedMultipartEntity implements HttpEntity {
-
-    /**
-     * The pool of ASCII chars to be used for generating a multipart boundary.
-     */
-    private final static char[] MULTIPART_CHARS =
-        "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
-            .toCharArray();
-
-    private final ModifiedHttpMultipart multipart;
-    private final Header contentType;
-
-    // @GuardedBy("dirty") // we always read dirty before accessing length
-    private long length;
-    private volatile boolean dirty; // used to decide whether to recalculate length
-
-    /**
-     * Creates an instance using the specified parameters
-     * @param mode the mode to use, may be {@code null}, in which case {@link HttpMultipartMode#STRICT} is used
-     * @param boundary the boundary string, may be {@code null}, in which case {@link #generateBoundary()} is invoked to create the string
-     * @param charset the character set to use, may be {@code null}, in which case {@link MIME#DEFAULT_CHARSET} - i.e. US-ASCII - is used.
-     */
-    public ModifiedMultipartEntity(
-            HttpMultipartMode mode,
-            String boundary,
-            Charset charset) {
-        super();
-        if (boundary == null) {
-            boundary = generateBoundary();
-        }
-        if (mode == null) {
-            mode = HttpMultipartMode.STRICT;
-        }
-        this.multipart = new ModifiedHttpMultipart("form-data", charset, boundary, mode);
-        this.contentType = new BasicHeader(
-                HTTP.CONTENT_TYPE,
-                generateContentType(boundary, charset));
-        this.dirty = true;
-    }
-
-    /**
-     * Creates an instance using the specified {@link HttpMultipartMode} mode.
-     * Boundary and charset are set to {@code null}.
-     * @param mode the desired mode
-     */
-    public ModifiedMultipartEntity(final HttpMultipartMode mode) {
-        this(mode, null, null);
-    }
-
-    /**
-     * Creates an instance using mode {@link HttpMultipartMode#STRICT}
-     */
-    public ModifiedMultipartEntity() {
-        this(HttpMultipartMode.STRICT, null, null);
-    }
-
-    protected String generateContentType(
-            final String boundary,
-            final Charset charset) {
-        StringBuilder buffer = new StringBuilder();
-        buffer.append("multipart/form-data; boundary=");
-        buffer.append(boundary);
-        if (charset != null) {
-            buffer.append("; charset=");
-            buffer.append(charset.name());
-        }
-        return buffer.toString();
-    }
-
-    protected String generateBoundary() {
-        StringBuilder buffer = new StringBuilder();
-        Random rand = new Random();
-        int count = rand.nextInt(11) + 30; // a random size from 30 to 40
-        for (int i = 0; i < count; i++) {
-            buffer.append(MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]);
-        }
-        return buffer.toString();
-    }
-
-    public void addPart(final FormBodyPart bodyPart) {
-        this.multipart.addBodyPart(bodyPart);
-        this.dirty = true;
-    }
-
-    public void addPart(final String name, final ContentBody contentBody) {
-        addPart(new FormBodyPart(name, contentBody));
-    }
-
-    public boolean isRepeatable() {
-        for (FormBodyPart part: this.multipart.getBodyParts()) {
-            ContentBody body = part.getBody();
-            if (body.getContentLength() < 0) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    public boolean isChunked() {
-        return !isRepeatable();
-    }
-
-    public boolean isStreaming() {
-        return !isRepeatable();
-    }
-
-    public long getContentLength() {
-        if (this.dirty) {
-            this.length = this.multipart.getTotalLength();
-            this.dirty = false;
-        }
-        return this.length;
-    }
-
-    public Header getContentType() {
-        return this.contentType;
-    }
-
-    public Header getContentEncoding() {
-        return null;
-    }
-
-    @Deprecated
-    public void consumeContent()
-        throws IOException, UnsupportedOperationException{
-        if (isStreaming()) {
-            throw new UnsupportedOperationException(
-                    "Streaming entity does not implement #consumeContent()");
-        }
-    }
-
-    public InputStream getContent() throws IOException, UnsupportedOperationException {
-        throw new UnsupportedOperationException(
-                    "Multipart form entity does not implement #getContent()");
-    }
-
-    public void writeTo(final OutputStream outstream) throws IOException {
-        this.multipart.writeTo(outstream);
-    }
-
-}
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedPreemptiveBasicAuthClientBuilderFactory.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedPreemptiveBasicAuthClientBuilderFactory.java
new file mode 100644
index 0000000..2a1f82f
--- /dev/null
+++ b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedPreemptiveBasicAuthClientBuilderFactory.java
@@ -0,0 +1,137 @@
+package org.apache.manifoldcf.agents.output.solr;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.impl.auth.BasicScheme;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.solr.client.solrj.impl.HttpClientUtil;
+import org.apache.solr.client.solrj.impl.PreemptiveAuth;
+import org.apache.solr.client.solrj.impl.PreemptiveBasicAuthClientBuilderFactory;
+import org.apache.solr.client.solrj.impl.SolrHttpClientBuilder;
+import org.apache.solr.client.solrj.util.SolrBasicAuthentication;
+import org.apache.solr.common.StringUtils;
+import org.apache.solr.common.params.MapSolrParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.StrUtils;
+import org.eclipse.jetty.client.HttpAuthenticationStore;
+import org.eclipse.jetty.client.ProxyAuthenticationProtocolHandler;
+import org.eclipse.jetty.client.WWWAuthenticationProtocolHandler;
+
+public class ModifiedPreemptiveBasicAuthClientBuilderFactory implements ModifiedHttpClientBuilderFactory {
+  /**
+   * A system property used to specify a properties file containing default parameters used for creating a HTTP client. This is specifically useful for configuring the HTTP basic auth credentials
+   * (i.e. username/password). The name of the property must match the relevant Solr config property name.
+   */
+  public static final String SYS_PROP_HTTP_CLIENT_CONFIG = "solr.httpclient.config";
+
+  /**
+   * A system property to configure the Basic auth credentials via a java system property. Since this will expose the password on the command-line, it is not very secure. But this mechanism is added
+   * for backwards compatibility.
+   */
+  public static final String SYS_PROP_BASIC_AUTH_CREDENTIALS = "basicauth";
+
+  private static PreemptiveAuth requestInterceptor = new PreemptiveAuth(new BasicScheme());
+
+  private static CredentialsResolver CREDENTIAL_RESOLVER = new CredentialsResolver();
+
+  /**
+   * This method enables configuring system wide defaults (apart from using a config file based approach).
+   */
+  public static void setDefaultSolrParams(final SolrParams params) {
+    CREDENTIAL_RESOLVER.defaultParams = params;
+  }
+
+  @Override
+  public void close() throws IOException {
+    HttpClientUtil.removeRequestInterceptor(requestInterceptor);
+  }
+
+  @Override
+  public void setup(final ModifiedHttp2SolrClient client) {
+    final String basicAuthUser = CREDENTIAL_RESOLVER.defaultParams.get(HttpClientUtil.PROP_BASIC_AUTH_USER);
+    final String basicAuthPass = CREDENTIAL_RESOLVER.defaultParams.get(HttpClientUtil.PROP_BASIC_AUTH_PASS);
+    this.setup(client, basicAuthUser, basicAuthPass);
+  }
+
+  public void setup(final ModifiedHttp2SolrClient client, final String basicAuthUser, final String basicAuthPass) {
+    if (basicAuthUser == null || basicAuthPass == null) {
+      throw new IllegalArgumentException("username & password must be specified with " + getClass().getName());
+    }
+
+    final HttpAuthenticationStore authenticationStore = new HttpAuthenticationStore();
+    authenticationStore.addAuthentication(new SolrBasicAuthentication(basicAuthUser, basicAuthPass));
+    client.getHttpClient().setAuthenticationStore(authenticationStore);
+    client.getProtocolHandlers().put(new WWWAuthenticationProtocolHandler(client.getHttpClient()));
+    client.getProtocolHandlers().put(new ProxyAuthenticationProtocolHandler(client.getHttpClient()));
+  }
+
+  @Override
+  public SolrHttpClientBuilder getHttpClientBuilder(final SolrHttpClientBuilder builder) {
+    final String basicAuthUser = CREDENTIAL_RESOLVER.defaultParams.get(HttpClientUtil.PROP_BASIC_AUTH_USER);
+    final String basicAuthPass = CREDENTIAL_RESOLVER.defaultParams.get(HttpClientUtil.PROP_BASIC_AUTH_PASS);
+    if (basicAuthUser == null || basicAuthPass == null) {
+      throw new IllegalArgumentException("username & password must be specified with " + getClass().getName());
+    }
+
+    return initHttpClientBuilder(builder == null ? SolrHttpClientBuilder.create() : builder, basicAuthUser, basicAuthPass);
+  }
+
+  private SolrHttpClientBuilder initHttpClientBuilder(final SolrHttpClientBuilder builder, final String basicAuthUser, final String basicAuthPass) {
+    builder.setDefaultCredentialsProvider(() -> {
+      final CredentialsProvider credsProvider = new BasicCredentialsProvider();
+      credsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(basicAuthUser, basicAuthPass));
+      return credsProvider;
+    });
+
+    HttpClientUtil.addRequestInterceptor(requestInterceptor);
+    return builder;
+  }
+
+  static class CredentialsResolver {
+
+    public volatile SolrParams defaultParams;
+
+    public CredentialsResolver() {
+      final String credentials = System.getProperty(PreemptiveBasicAuthClientBuilderFactory.SYS_PROP_BASIC_AUTH_CREDENTIALS);
+      final String configFile = System.getProperty(PreemptiveBasicAuthClientBuilderFactory.SYS_PROP_HTTP_CLIENT_CONFIG);
+
+      if (credentials != null && configFile != null) {
+        throw new IllegalArgumentException("Basic authentication credentials passed via a configuration file" + " as well as java system property. Please choose one mechanism!");
+      }
+
+      if (credentials != null) {
+        final List<String> ss = StrUtils.splitSmart(credentials, ':');
+        if (ss.size() != 2 || StringUtils.isEmpty(ss.get(0)) || StringUtils.isEmpty(ss.get(1))) {
+          throw new IllegalArgumentException("Invalid Authentication credentials: Please provide 'basicauth' in the 'user:password' format");
+        }
+        final Map<String, String> paramMap = new HashMap<>();
+        paramMap.put(HttpClientUtil.PROP_BASIC_AUTH_USER, ss.get(0));
+        paramMap.put(HttpClientUtil.PROP_BASIC_AUTH_PASS, ss.get(1));
+        defaultParams = new MapSolrParams(paramMap);
+      } else if (configFile != null) {
+        final Properties defaultProps = new Properties();
+        try (BufferedReader reader = Files.newBufferedReader(Paths.get(configFile), StandardCharsets.UTF_8)) {
+          defaultProps.load(reader);
+        } catch (final IOException e) {
+          throw new IllegalArgumentException("Unable to read credentials file at " + configFile, e);
+        }
+        final Map<String, String> map = new HashMap<>();
+        defaultProps.forEach((k, v) -> map.put((String) k, (String) v));
+        defaultParams = new MapSolrParams(map);
+      } else {
+        defaultParams = null;
+      }
+    }
+  }
+}
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedUpdateRequest.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedUpdateRequest.java
new file mode 100644
index 0000000..39f106b
--- /dev/null
+++ b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/ModifiedUpdateRequest.java
@@ -0,0 +1,520 @@
+package org.apache.manifoldcf.agents.output.solr;
+
+import static org.apache.solr.common.params.ShardParams._ROUTE_;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.request.AbstractUpdateRequest;
+import org.apache.solr.client.solrj.response.UpdateResponse;
+import org.apache.solr.client.solrj.util.ClientUtils;
+import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.cloud.DocCollection;
+import org.apache.solr.common.cloud.DocRouter;
+import org.apache.solr.common.cloud.Slice;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.UpdateParams;
+import org.apache.solr.common.util.ContentStream;
+import org.apache.solr.common.util.XML;
+
+public class ModifiedUpdateRequest extends AbstractUpdateRequest {
+
+  public static final String REPFACT = "rf";
+  public static final String VER = "ver";
+  public static final String OVERWRITE = "ow";
+  public static final String COMMIT_WITHIN = "cw";
+  private Map<SolrInputDocument, Map<String, Object>> documents = null;
+  private Iterator<SolrInputDocument> docIterator = null;
+  private Map<String, Map<String, Object>> deleteById = null;
+  private List<String> deleteQuery = null;
+
+  private boolean isLastDocInBatch = false;
+
+  public ModifiedUpdateRequest() {
+    super(METHOD.POST, "/update");
+  }
+
+  public ModifiedUpdateRequest(final String url) {
+    super(METHOD.POST, url);
+  }
+
+  // ---------------------------------------------------------------------------
+  // ---------------------------------------------------------------------------
+
+  /** clear the pending documents and delete commands */
+  public void clear() {
+    if (documents != null) {
+      documents.clear();
+    }
+    if (deleteById != null) {
+      deleteById.clear();
+    }
+    if (deleteQuery != null) {
+      deleteQuery.clear();
+    }
+  }
+
+  // ---------------------------------------------------------------------------
+  // ---------------------------------------------------------------------------
+
+  /**
+   * Add a SolrInputDocument to this request
+   *
+   * @throws NullPointerException if the document is null
+   */
+  public ModifiedUpdateRequest add(final SolrInputDocument doc) {
+    Objects.requireNonNull(doc, "Cannot add a null SolrInputDocument");
+    if (documents == null) {
+      documents = new LinkedHashMap<>();
+    }
+    documents.put(doc, null);
+    return this;
+  }
+
+  public ModifiedUpdateRequest add(final String... fields) {
+    return add(new SolrInputDocument(fields));
+  }
+
+  /**
+   * Add a SolrInputDocument to this request
+   *
+   * @param doc       the document
+   * @param overwrite true if the document should overwrite existing docs with the same id
+   * @throws NullPointerException if the document is null
+   */
+  public ModifiedUpdateRequest add(final SolrInputDocument doc, final Boolean overwrite) {
+    return add(doc, null, overwrite);
+  }
+
+  /**
+   * Add a SolrInputDocument to this request
+   *
+   * @param doc          the document
+   * @param commitWithin the time horizon by which the document should be committed (in ms)
+   * @throws NullPointerException if the document is null
+   */
+  public ModifiedUpdateRequest add(final SolrInputDocument doc, final Integer commitWithin) {
+    return add(doc, commitWithin, null);
+  }
+
+  /**
+   * Add a SolrInputDocument to this request
+   *
+   * @param doc          the document
+   * @param commitWithin the time horizon by which the document should be committed (in ms)
+   * @param overwrite    true if the document should overwrite existing docs with the same id
+   * @throws NullPointerException if the document is null
+   */
+  public ModifiedUpdateRequest add(final SolrInputDocument doc, final Integer commitWithin, final Boolean overwrite) {
+    Objects.requireNonNull(doc, "Cannot add a null SolrInputDocument");
+    if (documents == null) {
+      documents = new LinkedHashMap<>();
+    }
+    final Map<String, Object> params = new HashMap<>(2);
+    if (commitWithin != null)
+      params.put(COMMIT_WITHIN, commitWithin);
+    if (overwrite != null)
+      params.put(OVERWRITE, overwrite);
+
+    documents.put(doc, params);
+
+    return this;
+  }
+
+  /**
+   * Add a collection of SolrInputDocuments to this request
+   *
+   * @throws NullPointerException if any of the documents in the collection are null
+   */
+  public ModifiedUpdateRequest add(final Collection<SolrInputDocument> docs) {
+    if (documents == null) {
+      documents = new LinkedHashMap<>();
+    }
+    for (final SolrInputDocument doc : docs) {
+      Objects.requireNonNull(doc, "Cannot add a null SolrInputDocument");
+      documents.put(doc, null);
+    }
+    return this;
+  }
+
+  public ModifiedUpdateRequest deleteById(final String id) {
+    if (deleteById == null) {
+      deleteById = new LinkedHashMap<>();
+    }
+    deleteById.put(id, null);
+    return this;
+  }
+
+  public ModifiedUpdateRequest deleteById(final String id, final String route) {
+    return deleteById(id, route, null);
+  }
+
+  public ModifiedUpdateRequest deleteById(final String id, final String route, final Long version) {
+    if (deleteById == null) {
+      deleteById = new LinkedHashMap<>();
+    }
+    final Map<String, Object> params = (route == null && version == null) ? null : new HashMap<>(1);
+    if (version != null)
+      params.put(VER, version);
+    if (route != null)
+      params.put(_ROUTE_, route);
+    deleteById.put(id, params);
+    return this;
+  }
+
+  public ModifiedUpdateRequest deleteById(final List<String> ids) {
+    if (deleteById == null) {
+      deleteById = new LinkedHashMap<>();
+    }
+
+    for (final String id : ids) {
+      deleteById.put(id, null);
+    }
+
+    return this;
+  }
+
+  public ModifiedUpdateRequest deleteById(final String id, final Long version) {
+    return deleteById(id, null, version);
+  }
+
+  public ModifiedUpdateRequest deleteByQuery(final String q) {
+    if (deleteQuery == null) {
+      deleteQuery = new ArrayList<>();
+    }
+    deleteQuery.add(q);
+    return this;
+  }
+
+  public ModifiedUpdateRequest withRoute(final String route) {
+    if (params == null)
+      params = new ModifiableSolrParams();
+    params.set(_ROUTE_, route);
+    return this;
+  }
+
+  public UpdateResponse commit(final SolrClient client, final String collection) throws IOException, SolrServerException {
+    if (params == null)
+      params = new ModifiableSolrParams();
+    params.set(UpdateParams.COMMIT, "true");
+    return process(client, collection);
+  }
+
+  private interface ReqSupplier<T extends ModifiedLBSolrClient.Req> {
+    T get(ModifiedUpdateRequest request, List<String> servers);
+  }
+
+  private <T extends ModifiedLBSolrClient.Req> Map<String, T> getRoutes(final DocRouter router, final DocCollection col, final Map<String, List<String>> urlMap, final ModifiableSolrParams params,
+      final String idField, final ReqSupplier<T> reqSupplier) {
+    if ((documents == null || documents.size() == 0) && (deleteById == null || deleteById.size() == 0)) {
+      return null;
+    }
+
+    final Map<String, T> routes = new HashMap<>();
+    if (documents != null) {
+      final Set<Entry<SolrInputDocument, Map<String, Object>>> entries = documents.entrySet();
+      for (final Entry<SolrInputDocument, Map<String, Object>> entry : entries) {
+        final SolrInputDocument doc = entry.getKey();
+        final Object id = doc.getFieldValue(idField);
+        if (id == null) {
+          return null;
+        }
+        final Slice slice = router.getTargetSlice(id.toString(), doc, null, null, col);
+        if (slice == null) {
+          return null;
+        }
+        final List<String> urls = urlMap.get(slice.getName());
+        if (urls == null) {
+          return null;
+        }
+        final String leaderUrl = urls.get(0);
+        T request = routes.get(leaderUrl);
+        if (request == null) {
+          final ModifiedUpdateRequest updateRequest = new ModifiedUpdateRequest();
+          updateRequest.setMethod(getMethod());
+          updateRequest.setCommitWithin(getCommitWithin());
+          updateRequest.setParams(params);
+          updateRequest.setPath(getPath());
+          updateRequest.setBasicAuthCredentials(getBasicAuthUser(), getBasicAuthPassword());
+          updateRequest.setResponseParser(getResponseParser());
+          request = reqSupplier.get(updateRequest, urls);
+          routes.put(leaderUrl, request);
+        }
+        final ModifiedUpdateRequest urequest = (ModifiedUpdateRequest) request.getRequest();
+        final Map<String, Object> value = entry.getValue();
+        Boolean ow = null;
+        if (value != null) {
+          ow = (Boolean) value.get(OVERWRITE);
+        }
+        if (ow != null) {
+          urequest.add(doc, ow);
+        } else {
+          urequest.add(doc);
+        }
+      }
+    }
+
+    // Route the deleteById's
+
+    if (deleteById != null) {
+
+      final Iterator<Map.Entry<String, Map<String, Object>>> entries = deleteById.entrySet().iterator();
+      while (entries.hasNext()) {
+
+        final Map.Entry<String, Map<String, Object>> entry = entries.next();
+
+        final String deleteId = entry.getKey();
+        final Map<String, Object> map = entry.getValue();
+        Long version = null;
+        String route = null;
+        if (map != null) {
+          version = (Long) map.get(VER);
+          route = (String) map.get(_ROUTE_);
+        }
+        final Slice slice = router.getTargetSlice(deleteId, null, route, null, col);
+        if (slice == null) {
+          return null;
+        }
+        final List<String> urls = urlMap.get(slice.getName());
+        if (urls == null) {
+          return null;
+        }
+        final String leaderUrl = urls.get(0);
+        T request = routes.get(leaderUrl);
+        if (request != null) {
+          final ModifiedUpdateRequest urequest = (ModifiedUpdateRequest) request.getRequest();
+          urequest.deleteById(deleteId, route, version);
+        } else {
+          final ModifiedUpdateRequest urequest = new ModifiedUpdateRequest();
+          urequest.setParams(params);
+          urequest.deleteById(deleteId, route, version);
+          urequest.setCommitWithin(getCommitWithin());
+          urequest.setBasicAuthCredentials(getBasicAuthUser(), getBasicAuthPassword());
+          request = reqSupplier.get(urequest, urls);
+          routes.put(leaderUrl, request);
+        }
+      }
+    }
+
+    return routes;
+  }
+
+  /**
+   * @param router  to route updates with
+   * @param col     DocCollection for the updates
+   * @param urlMap  of the cluster
+   * @param params  params to use
+   * @param idField the id field
+   * @return a Map of urls to requests
+   */
+  public Map<String, ModifiedLBSolrClient.Req> getRoutesToCollection(final DocRouter router, final DocCollection col, final Map<String, List<String>> urlMap, final ModifiableSolrParams params,
+      final String idField) {
+    return getRoutes(router, col, urlMap, params, idField, ModifiedLBSolrClient.Req::new);
+  }
+
+  public void setDocIterator(final Iterator<SolrInputDocument> docIterator) {
+    this.docIterator = docIterator;
+  }
+
+  public void setDeleteQuery(final List<String> deleteQuery) {
+    this.deleteQuery = deleteQuery;
+  }
+
+  // --------------------------------------------------------------------------
+  // --------------------------------------------------------------------------
+
+  @Override
+  public Collection<ContentStream> getContentStreams() throws IOException {
+    return ClientUtils.toContentStreams(getXML(), ClientUtils.TEXT_XML);
+  }
+
+  public String getXML() throws IOException {
+    final StringWriter writer = new StringWriter();
+    writeXML(writer);
+    writer.flush();
+
+    // If action is COMMIT or OPTIMIZE, it is sent with params
+    final String xml = writer.toString();
+    // System.out.println( "SEND:"+xml );
+    return (xml.length() > 0) ? xml : null;
+  }
+
+  private List<Map<SolrInputDocument, Map<String, Object>>> getDocLists(final Map<SolrInputDocument, Map<String, Object>> documents) {
+    final List<Map<SolrInputDocument, Map<String, Object>>> docLists = new ArrayList<>();
+    Map<SolrInputDocument, Map<String, Object>> docList = null;
+    if (this.documents != null) {
+
+      Boolean lastOverwrite = true;
+      Integer lastCommitWithin = -1;
+
+      final Set<Entry<SolrInputDocument, Map<String, Object>>> entries = this.documents.entrySet();
+      for (final Entry<SolrInputDocument, Map<String, Object>> entry : entries) {
+        final Map<String, Object> map = entry.getValue();
+        Boolean overwrite = null;
+        Integer commitWithin = null;
+        if (map != null) {
+          overwrite = (Boolean) entry.getValue().get(OVERWRITE);
+          commitWithin = (Integer) entry.getValue().get(COMMIT_WITHIN);
+        }
+        if (!Objects.equals(overwrite, lastOverwrite) || !Objects.equals(commitWithin, lastCommitWithin) || docLists.isEmpty()) {
+          docList = new LinkedHashMap<>();
+          docLists.add(docList);
+        }
+        docList.put(entry.getKey(), entry.getValue());
+        lastCommitWithin = commitWithin;
+        lastOverwrite = overwrite;
+      }
+    }
+
+    if (docIterator != null) {
+      docList = new LinkedHashMap<>();
+      docLists.add(docList);
+      while (docIterator.hasNext()) {
+        final SolrInputDocument doc = docIterator.next();
+        if (doc != null) {
+          docList.put(doc, null);
+        }
+      }
+    }
+
+    return docLists;
+  }
+
+  /**
+   * @since solr 1.4
+   */
+  public ModifiedUpdateRequest writeXML(final Writer writer) throws IOException {
+    final List<Map<SolrInputDocument, Map<String, Object>>> getDocLists = getDocLists(documents);
+
+    for (final Map<SolrInputDocument, Map<String, Object>> docs : getDocLists) {
+
+      if ((docs != null && docs.size() > 0)) {
+        final Entry<SolrInputDocument, Map<String, Object>> firstDoc = docs.entrySet().iterator().next();
+        final Map<String, Object> map = firstDoc.getValue();
+        Integer cw = null;
+        Boolean ow = null;
+        if (map != null) {
+          cw = (Integer) firstDoc.getValue().get(COMMIT_WITHIN);
+          ow = (Boolean) firstDoc.getValue().get(OVERWRITE);
+        }
+        if (ow == null)
+          ow = true;
+        final int commitWithin = (cw != null && cw != -1) ? cw : this.commitWithin;
+        final boolean overwrite = ow;
+        if (commitWithin > -1 || overwrite != true) {
+          writer.write("<add commitWithin=\"" + commitWithin + "\" " + "overwrite=\"" + overwrite + "\">");
+        } else {
+          writer.write("<add>");
+        }
+
+        final Set<Entry<SolrInputDocument, Map<String, Object>>> entries = docs.entrySet();
+        for (final Entry<SolrInputDocument, Map<String, Object>> entry : entries) {
+          ClientUtils.writeXML(entry.getKey(), writer);
+        }
+
+        writer.write("</add>");
+      }
+    }
+
+    // Add the delete commands
+    final boolean deleteI = deleteById != null && deleteById.size() > 0;
+    final boolean deleteQ = deleteQuery != null && deleteQuery.size() > 0;
+    if (deleteI || deleteQ) {
+      if (commitWithin > 0) {
+        writer.append("<delete commitWithin=\"").append(String.valueOf(commitWithin)).append("\">");
+      } else {
+        writer.append("<delete>");
+      }
+      if (deleteI) {
+        for (final Map.Entry<String, Map<String, Object>> entry : deleteById.entrySet()) {
+          writer.append("<id");
+          final Map<String, Object> map = entry.getValue();
+          if (map != null) {
+            final Long version = (Long) map.get(VER);
+            final String route = (String) map.get(_ROUTE_);
+            if (version != null) {
+              writer.append(" version=\"").append(String.valueOf(version)).append('"');
+            }
+
+            if (route != null) {
+              writer.append(" _route_=\"").append(route).append('"');
+            }
+          }
+          writer.append(">");
+
+          XML.escapeCharData(entry.getKey(), writer);
+          writer.append("</id>");
+        }
+      }
+      if (deleteQ) {
+        for (final String q : deleteQuery) {
+          writer.append("<query>");
+          XML.escapeCharData(q, writer);
+          writer.append("</query>");
+        }
+      }
+      writer.append("</delete>");
+    }
+    return this;
+  }
+
+  // --------------------------------------------------------------------------
+  // --------------------------------------------------------------------------
+
+  // --------------------------------------------------------------------------
+  //
+  // --------------------------------------------------------------------------
+
+  public List<SolrInputDocument> getDocuments() {
+    if (documents == null)
+      return null;
+    final List<SolrInputDocument> docs = new ArrayList<>(documents.size());
+    docs.addAll(documents.keySet());
+    return docs;
+  }
+
+  public Map<SolrInputDocument, Map<String, Object>> getDocumentsMap() {
+    return documents;
+  }
+
+  public Iterator<SolrInputDocument> getDocIterator() {
+    return docIterator;
+  }
+
+  public List<String> getDeleteById() {
+    if (deleteById == null)
+      return null;
+    final List<String> deletes = new ArrayList<>(deleteById.keySet());
+    return deletes;
+  }
+
+  public Map<String, Map<String, Object>> getDeleteByIdMap() {
+    return deleteById;
+  }
+
+  public List<String> getDeleteQuery() {
+    return deleteQuery;
+  }
+
+  public boolean isLastDocInBatch() {
+    return isLastDocInBatch;
+  }
+
+  public void lastDocInBatch() {
+    isLastDocInBatch = true;
+  }
+
+}
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/PreemptiveBasicAuthInterceptor.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/PreemptiveBasicAuthInterceptor.java
deleted file mode 100644
index 2d58538..0000000
--- a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/PreemptiveBasicAuthInterceptor.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/* $Id$ */

-

-/**

- * Licensed to the Apache Software Foundation (ASF) under one or more

- * contributor license agreements. See the NOTICE file distributed with

- * this work for additional information regarding copyright ownership.

- * The ASF licenses this file to You 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.apache.manifoldcf.agents.output.solr;

-

-import org.apache.http.HttpException;

-import org.apache.http.HttpRequest;

-import org.apache.http.HttpRequestInterceptor;

-import org.apache.http.auth.AuthScope;

-import org.apache.http.auth.AuthState;

-import org.apache.http.auth.Credentials;

-import org.apache.http.client.CredentialsProvider;

-import org.apache.http.client.protocol.HttpClientContext;

-import org.apache.http.impl.auth.BasicScheme;

-import org.apache.http.protocol.HttpContext;

-

-import java.io.IOException;

-

-public class PreemptiveBasicAuthInterceptor implements HttpRequestInterceptor {

-

-  private final AuthScope scope;

-

-  public PreemptiveBasicAuthInterceptor(final AuthScope scope) {

-    this.scope = scope;

-  }

-

-  @Override

-  public void process(final HttpRequest request,

-                      final HttpContext context) throws HttpException, IOException {

-    if (!alreadyAppliesAuthScheme(context)) {

-      final CredentialsProvider provider = getCredentialsProvider(context);

-      final Credentials credentials = provider.getCredentials(scope);

-      if (credentials == null) {

-        throw new HttpException("Missing credentials for preemptive basic authentication.");

-      }

-      final AuthState state = getAuthState(context);

-      state.update(new BasicScheme(), credentials);

-    }

-  }

-

-  private boolean alreadyAppliesAuthScheme(final HttpContext context) {

-    final AuthState authState = getAuthState(context);

-    return authState.getAuthScheme() != null;

-  }

-

-  private AuthState getAuthState(final HttpContext context) {

-    return (AuthState) context.getAttribute(HttpClientContext.TARGET_AUTH_STATE);

-  }

-

-  private CredentialsProvider getCredentialsProvider(final HttpContext context) {

-    return (CredentialsProvider) context.getAttribute(HttpClientContext.CREDS_PROVIDER);

-  }

-}

diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/SolrConfig.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/SolrConfig.java
index 0ec1b57..88cc253 100644
--- a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/SolrConfig.java
+++ b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/SolrConfig.java
@@ -18,11 +18,10 @@
 */
 package org.apache.manifoldcf.agents.output.solr;
 
-
-/** Parameters and output data for SOLR output connector.
-*/
-public class SolrConfig
-{
+/**
+ * Parameters and output data for SOLR output connector.
+ */
+public class SolrConfig {
   public static final String _rcsid = "@(#)$Id: SolrConfig.java 991374 2010-08-31 22:32:08Z kwright $";
 
   // Configuration parameters
@@ -33,9 +32,9 @@
   public static final String SOLR_TYPE_STANDARD = "standard";
   /** Type: Solr Cloud */
   public static final String SOLR_TYPE_SOLRCLOUD = "solrcloud";
-  
+
   // SolrCloud zookeeper parameters
-  
+
   // Zookeeper hosts, as nodes
   /** Zookeeper node */
   public static final String NODE_ZOOKEEPER = "zookeeper";
@@ -43,26 +42,26 @@
   public static final String ATTR_HOST = "host";
   /** Zookeeper port */
   public static final String ATTR_PORT = "port";
-  
+
   /** Zookeeper znode path */
   public static final String PARAM_ZOOKEEPER_ZNODE_PATH = "ZooKeeper znode path";
-  
+
   /** ZooKeeper client timeout */
-  public static final String PARAM_ZOOKEEPER_CLIENT_TIMEOUT = "ZooKeeper client timeout";
+  public static final String PARAM_ZOOKEEPER_SOCKET_TIMEOUT = "ZooKeeper socket timeout";
   /** ZooKeeper connect timeout */
-  public static final String PARAM_ZOOKEEPER_CONNECT_TIMEOUT = "ZooKeeper connect timeout";
+  public static final String PARAM_ZOOKEEPER_CONNECTION_TIMEOUT = "ZooKeeper connection timeout";
   /** Collection name */
   public static final String PARAM_COLLECTION = "Collection";
-  
+
   // General indexing parameters
-  
+
   /** Protocol */
   public static final String PARAM_PROTOCOL = "Server protocol";
   /** Protocol: http */
   public static final String PROTOCOL_TYPE_HTTP = "http";
   /** Protocol: https */
   public static final String PROTOCOL_TYPE_HTTPS = "https";
-  
+
   /** Server name */
   public static final String PARAM_SERVER = "Server name";
   /** Port */
@@ -112,7 +111,7 @@
   /** Included mime types */
   public static final String PARAM_INCLUDEDMIMETYPES = "Included mime types";
   /** Excluded mime types */
-  public static final String PARAM_EXCLUDEDMIMETYPES="Excluded mime types";
+  public static final String PARAM_EXCLUDEDMIMETYPES = "Excluded mime types";
   /** Parameter describing the use of Extract Update handler */
   public static final String PARAM_EXTRACTUPDATE = "Use extract update handler";
   /** Optional content field (if not using extract update handler) */
@@ -123,7 +122,7 @@
   public static final String ATTRIBUTE_NAME = "name";
   /** Attribute with the argument value */
   public static final String ATTRIBUTE_VALUE = "value";
-  
+
   // Output specification
 
 }
diff --git a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/SolrConnector.java b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/SolrConnector.java
index 305e65b..706fec4 100644
--- a/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/SolrConnector.java
+++ b/connectors/solr/connector/src/main/java/org/apache/manifoldcf/agents/output/solr/SolrConnector.java
@@ -21,20 +21,21 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
-import java.util.HashSet;
 
 import org.apache.manifoldcf.agents.interfaces.IOutputAddActivity;
+import org.apache.manifoldcf.agents.interfaces.IOutputCheckActivity;
 import org.apache.manifoldcf.agents.interfaces.IOutputNotifyActivity;
 import org.apache.manifoldcf.agents.interfaces.IOutputRemoveActivity;
-import org.apache.manifoldcf.agents.interfaces.IOutputCheckActivity;
 import org.apache.manifoldcf.agents.interfaces.RepositoryDocument;
 import org.apache.manifoldcf.agents.interfaces.ServiceInterruption;
-import org.apache.manifoldcf.core.interfaces.Specification;
+import org.apache.manifoldcf.connectorcommon.interfaces.IKeystoreManager;
+import org.apache.manifoldcf.connectorcommon.interfaces.KeystoreManagerFactory;
 import org.apache.manifoldcf.core.interfaces.ConfigNode;
 import org.apache.manifoldcf.core.interfaces.ConfigParams;
 import org.apache.manifoldcf.core.interfaces.ConfigurationNode;
@@ -43,16 +44,13 @@
 import org.apache.manifoldcf.core.interfaces.IPostParameters;
 import org.apache.manifoldcf.core.interfaces.IThreadContext;
 import org.apache.manifoldcf.core.interfaces.ManifoldCFException;
-import org.apache.manifoldcf.core.interfaces.SpecificationNode;
+import org.apache.manifoldcf.core.interfaces.Specification;
 import org.apache.manifoldcf.core.interfaces.VersionContext;
-import org.apache.manifoldcf.connectorcommon.interfaces.KeystoreManagerFactory;
-import org.apache.manifoldcf.connectorcommon.interfaces.IKeystoreManager;
 
-
-/** This is the output connector for SOLR.  Currently, no frills.
-*/
-public class SolrConnector extends org.apache.manifoldcf.agents.output.BaseOutputConnector
-{
+/**
+ * This is the output connector for SOLR. Currently, no frills.
+ */
+public class SolrConnector extends org.apache.manifoldcf.agents.output.BaseOutputConnector {
   public static final String _rcsid = "@(#)$Id: SolrConnector.java 998081 2010-09-17 11:33:15Z kwright $";
 
   // Activities we log
@@ -64,10 +62,10 @@
 
   /** Local connection */
   protected HttpPoster poster = null;
-  
+
   /** Expiration */
   protected long expirationTime = -1L;
-  
+
   /** The allow attribute name */
   protected String allowAttributeName = "allow_token_";
   /** The deny attribute name */
@@ -82,7 +80,7 @@
   protected String excludedMimeTypesString = null;
   /** Excluded mime types */
   protected Set<String> excludedMimeTypes = null;
-  
+
   // Attributes going into Solr
   protected String idAttributeName = null;
   protected String originalSizeAttributeName = null;
@@ -94,7 +92,7 @@
   protected String contentAttributeName = null;
   /** Use extractiing update handler? */
   protected boolean useExtractUpdateHandler = true;
-  /** Allow compression?  Currently static */
+  /** Allow compression? Currently static */
   protected final static boolean allowCompression = true;
 
   /** Whether or not to commit */
@@ -102,52 +100,48 @@
 
   /** Collection name (non-empty only if SolrCloud) */
   protected String collectionName = null;
-  
+
   /** Idle connection expiration interval */
   protected final static long EXPIRATION_INTERVAL = 300000L;
 
-  static
-  {
-    System.setProperty("jdk.xml.maxGeneralEntitySizeLimit",new Integer(Integer.MAX_VALUE).toString());
-  }
-  
-  /** Constructor.
-  */
-  public SolrConnector()
-  {
+  static {
+    System.setProperty("jdk.xml.maxGeneralEntitySizeLimit", new Integer(Integer.MAX_VALUE).toString());
   }
 
-  /** Return the list of activities that this connector supports (i.e. writes into the log).
-  *@return the list.
-  */
-  @Override
-  public String[] getActivitiesList()
-  {
-    return new String[]{INGEST_ACTIVITY,REMOVE_ACTIVITY};
+  /**
+   * Constructor.
+   */
+  public SolrConnector() {
   }
 
-  /** Connect.
-  *@param configParameters is the set of configuration parameters, which
-  * in this case describe the target appliance, basic auth configuration, etc.  (This formerly came
-  * out of the ini file.)
-  */
+  /**
+   * Return the list of activities that this connector supports (i.e. writes into the log).
+   *
+   * @return the list.
+   */
   @Override
-  public void connect(ConfigParams configParameters)
-  {
+  public String[] getActivitiesList() {
+    return new String[] { INGEST_ACTIVITY, REMOVE_ACTIVITY };
+  }
+
+  /**
+   * Connect.
+   *
+   * @param configParameters
+   *          is the set of configuration parameters, which in this case describe the target appliance, basic auth configuration, etc. (This formerly came out of the ini file.)
+   */
+  @Override
+  public void connect(final ConfigParams configParameters) {
     super.connect(configParameters);
   }
 
-  /** This method is periodically called for all connectors that are connected but not
-  * in active use.
-  */
+  /**
+   * This method is periodically called for all connectors that are connected but not in active use.
+   */
   @Override
-  public void poll()
-    throws ManifoldCFException
-  {
-    if (poster != null)
-    {
-      if (expirationTime <= System.currentTimeMillis())
-      {
+  public void poll() throws ManifoldCFException {
+    if (poster != null) {
+      if (expirationTime <= System.currentTimeMillis()) {
         // Expire connection
         poster.shutdown();
         poster = null;
@@ -156,24 +150,22 @@
     }
   }
 
-  /** This method is called to assess whether to count this connector instance should
-  * actually be counted as being connected.
-  *@return true if the connector instance is actually connected.
-  */
+  /**
+   * This method is called to assess whether to count this connector instance should actually be counted as being connected.
+   *
+   * @return true if the connector instance is actually connected.
+   */
   @Override
-  public boolean isConnected()
-  {
+  public boolean isConnected() {
     return poster != null;
   }
 
-  /** Close the connection.  Call this before discarding the connection.
-  */
+  /**
+   * Close the connection. Call this before discarding the connection.
+   */
   @Override
-  public void disconnect()
-    throws ManifoldCFException
-  {
-    if (poster != null)
-    {
+  public void disconnect() throws ManifoldCFException {
+    if (poster != null) {
       poster.shutdown();
       poster = null;
       expirationTime = -1L;
@@ -197,11 +189,8 @@
   }
 
   /** Set up a session */
-  protected void getSession()
-    throws ManifoldCFException
-  {
-    if (poster == null)
-    {
+  protected void getSession() throws ManifoldCFException {
+    if (poster == null) {
       String updatePath = params.getParameter(SolrConfig.PARAM_UPDATEPATH);
       if (updatePath == null || updatePath.length() == 0)
         updatePath = "";
@@ -221,7 +210,7 @@
       originalSizeAttributeName = params.getParameter(SolrConfig.PARAM_ORIGINALSIZEFIELD);
       if (originalSizeAttributeName == null || originalSizeAttributeName.length() == 0)
         originalSizeAttributeName = null;
-      
+
       modifiedDateAttributeName = params.getParameter(SolrConfig.PARAM_MODIFIEDDATEFIELD);
       if (modifiedDateAttributeName == null || modifiedDateAttributeName.length() == 0)
         modifiedDateAttributeName = null;
@@ -229,7 +218,7 @@
       createdDateAttributeName = params.getParameter(SolrConfig.PARAM_CREATEDDATEFIELD);
       if (createdDateAttributeName == null || createdDateAttributeName.length() == 0)
         createdDateAttributeName = null;
-  
+
       indexedDateAttributeName = params.getParameter(SolrConfig.PARAM_INDEXEDDATEFIELD);
       if (indexedDateAttributeName == null || indexedDateAttributeName.length() == 0)
         indexedDateAttributeName = null;
@@ -245,8 +234,8 @@
       contentAttributeName = params.getParameter(SolrConfig.PARAM_CONTENTFIELD);
       if (contentAttributeName == null || contentAttributeName.length() == 0)
         contentAttributeName = null;
-      
-      String useExtractUpdateHandlerValue = params.getParameter(SolrConfig.PARAM_EXTRACTUPDATE);
+
+      final String useExtractUpdateHandlerValue = params.getParameter(SolrConfig.PARAM_EXTRACTUPDATE);
       if (useExtractUpdateHandlerValue == null || useExtractUpdateHandlerValue.length() == 0)
         useExtractUpdateHandler = true;
       else
@@ -257,80 +246,70 @@
       String commits = params.getParameter(SolrConfig.PARAM_COMMITS);
       if (commits == null || commits.length() == 0)
         commits = "true";
-      
+
       doCommits = commits.equals("true");
-      
+
       String commitWithin = params.getParameter(SolrConfig.PARAM_COMMITWITHIN);
       if (commitWithin == null || commitWithin.length() == 0)
         commitWithin = null;
-      
-      String docMax = params.getParameter(SolrConfig.PARAM_MAXLENGTH);
+
+      final String docMax = params.getParameter(SolrConfig.PARAM_MAXLENGTH);
       if (docMax == null || docMax.length() == 0)
         maxDocumentLength = null;
       else
         maxDocumentLength = new Long(docMax);
       if (maxDocumentLength == null && !useExtractUpdateHandler)
         throw new ManifoldCFException("Maximum document length required for non-extract-update indexing");
-      
+
       includedMimeTypesString = params.getParameter(SolrConfig.PARAM_INCLUDEDMIMETYPES);
-      if (includedMimeTypesString == null || includedMimeTypesString.length() == 0)
-      {
+      if (includedMimeTypesString == null || includedMimeTypesString.length() == 0) {
         includedMimeTypesString = null;
         includedMimeTypes = null;
-      }
-      else
-      {
+      } else {
         // Parse the included mime types
         includedMimeTypes = parseMimeTypes(includedMimeTypesString);
-        if (includedMimeTypes.size() == 0)
-        {
+        if (includedMimeTypes.size() == 0) {
           includedMimeTypesString = null;
           includedMimeTypes = null;
         }
       }
 
       excludedMimeTypesString = params.getParameter(SolrConfig.PARAM_EXCLUDEDMIMETYPES);
-      if (excludedMimeTypesString == null || excludedMimeTypesString.length() == 0)
-      {
+      if (excludedMimeTypesString == null || excludedMimeTypesString.length() == 0) {
         excludedMimeTypesString = null;
         excludedMimeTypes = null;
-      }
-      else
-      {
+      } else {
         // Parse the included mime types
         excludedMimeTypes = parseMimeTypes(excludedMimeTypesString);
-        if (excludedMimeTypes.size() == 0)
-        {
+        if (excludedMimeTypes.size() == 0) {
           excludedMimeTypesString = null;
           excludedMimeTypes = null;
         }
       }
-      
 
       // Now, initialize Solr-j
       String solrType = params.getParameter(SolrConfig.PARAM_SOLR_TYPE);
       if (solrType == null)
         solrType = SolrConfig.SOLR_TYPE_STANDARD;
 
-      if (solrType.equals(SolrConfig.SOLR_TYPE_STANDARD))
-      {
-        String userID = params.getParameter(SolrConfig.PARAM_USERID);
-        String password = params.getObfuscatedParameter(SolrConfig.PARAM_PASSWORD);
-        String realm = params.getParameter(SolrConfig.PARAM_REALM);
-        String keystoreData = params.getParameter(SolrConfig.PARAM_KEYSTORE);
+      if (solrType.equals(SolrConfig.SOLR_TYPE_STANDARD)) {
+        final String userID = params.getParameter(SolrConfig.PARAM_USERID);
+        final String password = params.getObfuscatedParameter(SolrConfig.PARAM_PASSWORD);
+        final String realm = params.getParameter(SolrConfig.PARAM_REALM);
+        final String keystoreData = params.getParameter(SolrConfig.PARAM_KEYSTORE);
         IKeystoreManager keystoreManager;
         if (keystoreData != null)
-          keystoreManager = KeystoreManagerFactory.make("",keystoreData);
+          keystoreManager = KeystoreManagerFactory.make("", keystoreData);
         else
           keystoreManager = null;
 
-        String protocol = params.getParameter(SolrConfig.PARAM_PROTOCOL);
+        final String protocol = params.getParameter(SolrConfig.PARAM_PROTOCOL);
         if (protocol == null || protocol.length() == 0)
-          throw new ManifoldCFException("Missing parameter: "+SolrConfig.PARAM_PROTOCOL);
+          throw new ManifoldCFException("Missing parameter: " + SolrConfig.PARAM_PROTOCOL);
 
-        String server = params.getParameter(SolrConfig.PARAM_SERVER);
+        final String server = params.getParameter(SolrConfig.PARAM_SERVER);
         if (server == null || server.length() == 0)
-          throw new ManifoldCFException("Missing parameter: "+SolrConfig.PARAM_SERVER);
+          throw new ManifoldCFException("Missing parameter: " + SolrConfig.PARAM_SERVER);
 
         String port = params.getParameter(SolrConfig.PARAM_PORT);
         if (port == null || port.length() == 0)
@@ -351,52 +330,33 @@
         String connectTimeoutString = params.getParameter(SolrConfig.PARAM_CONNECTION_TIMEOUT);
         if (connectTimeoutString == null)
           connectTimeoutString = "60";
-        
+
         collectionName = null;
-        
-        try
-        {
-          int socketTimeout = Integer.parseInt(socketTimeoutString) * 1000;
-          int connectTimeout = Integer.parseInt(connectTimeoutString) * 1000;
-          
-          poster = new HttpPoster(protocol,server,Integer.parseInt(port),webapp,core,
-            connectTimeout,socketTimeout,
-            updatePath,removePath,statusPath,realm,userID,password,
-            allowAttributeName,denyAttributeName,idAttributeName,
-            originalSizeAttributeName,modifiedDateAttributeName,createdDateAttributeName,indexedDateAttributeName,
-            fileNameAttributeName,mimeTypeAttributeName,contentAttributeName,
-            keystoreManager,maxDocumentLength,commitWithin,useExtractUpdateHandler,
-            includedMimeTypes,excludedMimeTypes,
-            allowCompression);
-          
-        }
-        catch (NumberFormatException e)
-        {
+
+        try {
+          final int socketTimeout = Integer.parseInt(socketTimeoutString) * 1000;
+          final int connectTimeout = Integer.parseInt(connectTimeoutString) * 1000;
+
+          poster = new HttpPoster(protocol, server, Integer.parseInt(port), webapp, core, connectTimeout, socketTimeout, updatePath, removePath, statusPath, realm, userID, password, allowAttributeName, denyAttributeName, idAttributeName,
+              originalSizeAttributeName, modifiedDateAttributeName, createdDateAttributeName, indexedDateAttributeName, fileNameAttributeName, mimeTypeAttributeName, contentAttributeName, keystoreManager, maxDocumentLength, commitWithin,
+              useExtractUpdateHandler, includedMimeTypes, excludedMimeTypes, allowCompression);
+
+        } catch (final NumberFormatException e) {
           throw new ManifoldCFException(e.getMessage());
         }
 
-      }
-      else if (solrType.equals(SolrConfig.SOLR_TYPE_SOLRCLOUD))
-      {
-        StringBuilder zookeeperString = new StringBuilder();
+      } else if (solrType.equals(SolrConfig.SOLR_TYPE_SOLRCLOUD)) {
+        final List<String> zookeeperHosts = new ArrayList<>();
         // Pull together the zookeeper string describing the zookeeper nodes
-        for (int i = 0; i < params.getChildCount(); i++)
-        {
-          ConfigurationNode cn = params.getChild(i);
-          if (cn.getType().equals(SolrConfig.NODE_ZOOKEEPER))
-          {
-            if (zookeeperString.length() > 0)
-              zookeeperString.append(",");
-            zookeeperString.append(cn.getAttributeValue(SolrConfig.ATTR_HOST)).append(":").append(cn.getAttributeValue(SolrConfig.ATTR_PORT));
+        for (int i = 0; i < params.getChildCount(); i++) {
+          final ConfigurationNode cn = params.getChild(i);
+          if (cn.getType().equals(SolrConfig.NODE_ZOOKEEPER)) {
+            zookeeperHosts.add(cn.getAttributeValue(SolrConfig.ATTR_HOST) + ":" + cn.getAttributeValue(SolrConfig.ATTR_PORT));
           }
         }
-        
+
         String znodePath = params.getParameter(SolrConfig.PARAM_ZOOKEEPER_ZNODE_PATH);
-        if (znodePath == null)
-          znodePath = "";
-        
-        String zookeeperHost = zookeeperString.toString() + znodePath;
-        
+
         // Get collection
         String collection = params.getParameter(SolrConfig.PARAM_COLLECTION);
         if (collection == null)
@@ -404,59 +364,43 @@
         collectionName = collection;
 
         // Pick up timeouts
-        String zkClientTimeoutString = params.getParameter(SolrConfig.PARAM_ZOOKEEPER_CLIENT_TIMEOUT);
-        if (zkClientTimeoutString == null)
-          zkClientTimeoutString = "60";
-        String zkConnectTimeoutString = params.getParameter(SolrConfig.PARAM_ZOOKEEPER_CONNECT_TIMEOUT);
-        if (zkConnectTimeoutString == null)
-          zkConnectTimeoutString = "60";
-        
-        // Create an httpposter
-        try
-        {
-          int zkClientTimeout = Integer.parseInt(zkClientTimeoutString) * 1000;
-          int zkConnectTimeout = Integer.parseInt(zkConnectTimeoutString) * 1000;
+        String zkSocketTimeoutString = params.getParameter(SolrConfig.PARAM_ZOOKEEPER_SOCKET_TIMEOUT);
+        if (zkSocketTimeoutString == null)
+          zkSocketTimeoutString = "60";
+        String zkConnectionTimeoutString = params.getParameter(SolrConfig.PARAM_ZOOKEEPER_CONNECTION_TIMEOUT);
+        if (zkConnectionTimeoutString == null)
+          zkConnectionTimeoutString = "60";
 
-          poster = new HttpPoster(zookeeperHost,collection,
-            zkClientTimeout,zkConnectTimeout,
-            updatePath,removePath,statusPath,
-            allowAttributeName,denyAttributeName,idAttributeName,
-            originalSizeAttributeName,modifiedDateAttributeName,createdDateAttributeName,indexedDateAttributeName,
-            fileNameAttributeName,mimeTypeAttributeName,contentAttributeName,
-            maxDocumentLength,commitWithin,useExtractUpdateHandler,
-            includedMimeTypes,excludedMimeTypes,
-            allowCompression);
-          
-        }
-        catch (NumberFormatException e)
-        {
+        // Create an httpposter
+        try {
+          final int zkClientTimeout = Integer.parseInt(zkSocketTimeoutString) * 1000;
+          final int zkConnectTimeout = Integer.parseInt(zkConnectionTimeoutString) * 1000;
+
+          poster = new HttpPoster(zookeeperHosts, znodePath, collection, zkClientTimeout, zkConnectTimeout, updatePath, removePath, statusPath, allowAttributeName, denyAttributeName, idAttributeName, originalSizeAttributeName,
+              modifiedDateAttributeName, createdDateAttributeName, indexedDateAttributeName, fileNameAttributeName, mimeTypeAttributeName, contentAttributeName, maxDocumentLength, commitWithin, useExtractUpdateHandler, includedMimeTypes,
+              excludedMimeTypes, allowCompression);
+
+        } catch (final NumberFormatException e) {
           throw new ManifoldCFException(e.getMessage());
         }
 
-      }
-      else
-        throw new ManifoldCFException("Illegal value for parameter '"+SolrConfig.PARAM_SOLR_TYPE+"': '"+solrType+"'");
-      
+      } else
+        throw new ManifoldCFException("Illegal value for parameter '" + SolrConfig.PARAM_SOLR_TYPE + "': '" + solrType + "'");
+
     }
     expirationTime = System.currentTimeMillis() + EXPIRATION_INTERVAL;
   }
 
   /** Parse a mime type field into individual mime types in a hash */
-  protected static Set<String> parseMimeTypes(final String mimeTypes)
-    throws ManifoldCFException
-  {
-    Set<String> rval = new HashSet<>();
-    try
-    {
-      java.io.Reader str = new java.io.StringReader(mimeTypes);
-      try
-      {
-        java.io.BufferedReader is = new java.io.BufferedReader(str);
-        try
-        {
-          while (true)
-          {
-            String nextString = is.readLine();
+  protected static Set<String> parseMimeTypes(final String mimeTypes) throws ManifoldCFException {
+    final Set<String> rval = new HashSet<>();
+    try {
+      final java.io.Reader str = new java.io.StringReader(mimeTypes);
+      try {
+        final java.io.BufferedReader is = new java.io.BufferedReader(str);
+        try {
+          while (true) {
+            final String nextString = is.readLine();
             if (nextString == null)
               break;
             if (nextString.length() == 0)
@@ -464,497 +408,280 @@
             rval.add(nextString.toLowerCase(Locale.ROOT));
           }
           return rval;
-        }
-        finally
-        {
+        } finally {
           is.close();
         }
-      }
-      finally
-      {
+      } finally {
         str.close();
       }
-    }
-    catch (java.io.IOException e)
-    {
-      throw new ManifoldCFException("IO error: "+e.getMessage(),e);
+    } catch (final java.io.IOException e) {
+      throw new ManifoldCFException("IO error: " + e.getMessage(), e);
     }
   }
 
-  /** Test the connection.  Returns a string describing the connection integrity.
-  *@return the connection's status as a displayable string.
-  */
+  /**
+   * Test the connection. Returns a string describing the connection integrity.
+   *
+   * @return the connection's status as a displayable string.
+   */
   @Override
-  public String check()
-    throws ManifoldCFException
-  {
-    try
-    {
+  public String check() throws ManifoldCFException {
+    try {
       getSession();
       poster.checkPost();
       return super.check();
-    }
-    catch (ServiceInterruption e)
-    {
-      return "Transient error: "+e.getMessage();
+    } catch (final ServiceInterruption e) {
+      return "Transient error: " + e.getMessage();
     }
   }
 
-  /** Get an output version string, given an output specification.  The output version string is used to uniquely describe the pertinent details of
-  * the output specification and the configuration, to allow the Connector Framework to determine whether a document will need to be output again.
-  * Note that the contents of the document cannot be considered by this method, and that a different version string (defined in IRepositoryConnector)
-  * is used to describe the version of the actual document.
-  *
-  * This method presumes that the connector object has been configured, and it is thus able to communicate with the output data store should that be
-  * necessary.
-  *@param spec is the current output specification for the job that is doing the crawling.
-  *@return a string, of unlimited length, which uniquely describes output configuration and specification in such a way that if two such strings are equal,
-  * the document will not need to be sent again to the output data store.
-  */
+  /**
+   * Get an output version string, given an output specification. The output version string is used to uniquely describe the pertinent details of the output specification and the configuration, to
+   * allow the Connector Framework to determine whether a document will need to be output again. Note that the contents of the document cannot be considered by this method, and that a different
+   * version string (defined in IRepositoryConnector) is used to describe the version of the actual document.
+   *
+   * This method presumes that the connector object has been configured, and it is thus able to communicate with the output data store should that be necessary.
+   *
+   * @param spec
+   *          is the current output specification for the job that is doing the crawling.
+   * @return a string, of unlimited length, which uniquely describes output configuration and specification in such a way that if two such strings are equal, the document will not need to be sent
+   *         again to the output data store.
+   */
   @Override
-  public VersionContext getPipelineDescription(Specification spec)
-    throws ManifoldCFException, ServiceInterruption
-  {
+  public VersionContext getPipelineDescription(final Specification spec) throws ManifoldCFException, ServiceInterruption {
     getSession();
-    SpecPacker sp = new SpecPacker(spec);
-    return new VersionContext(sp.toPackedString(),params,spec);
+    final SpecPacker sp = new SpecPacker(spec);
+    return new VersionContext(sp.toPackedString(), params, spec);
   }
 
-  /** Detect if a mime type is indexable or not.  This method is used by participating repository connectors to pre-filter the number of
-  * unusable documents that will be passed to this output connector.
-  *@param outputDescription is the document's output version.
-  *@param mimeType is the mime type of the document.
-  *@return true if the mime type is indexable by this connector.
-  */
+  /**
+   * Detect if a mime type is indexable or not. This method is used by participating repository connectors to pre-filter the number of unusable documents that will be passed to this output connector.
+   *
+   * @param outputDescription
+   *          is the document's output version.
+   * @param mimeType
+   *          is the mime type of the document.
+   * @return true if the mime type is indexable by this connector.
+   */
   @Override
-  public boolean checkMimeTypeIndexable(VersionContext outputDescription, String mimeType, IOutputCheckActivity activities)
-    throws ManifoldCFException, ServiceInterruption
-  {
+  public boolean checkMimeTypeIndexable(final VersionContext outputDescription, final String mimeType, final IOutputCheckActivity activities) throws ManifoldCFException, ServiceInterruption {
     getSession();
     return HttpPoster.checkMimeTypeIndexable(mimeType, useExtractUpdateHandler, includedMimeTypes, excludedMimeTypes);
   }
 
-  /** Pre-determine whether a document's length is indexable by this connector.  This method is used by participating repository connectors
-  * to help filter out documents that are too long to be indexable.
-  *@param outputDescription is the document's output version.
-  *@param length is the length of the document.
-  *@return true if the file is indexable.
-  */
+  /**
+   * Pre-determine whether a document's length is indexable by this connector. This method is used by participating repository connectors to help filter out documents that are too long to be
+   * indexable.
+   *
+   * @param outputDescription
+   *          is the document's output version.
+   * @param length
+   *          is the length of the document.
+   * @return true if the file is indexable.
+   */
   @Override
-  public boolean checkLengthIndexable(VersionContext outputDescription, long length, IOutputCheckActivity activities)
-    throws ManifoldCFException, ServiceInterruption
-  {
+  public boolean checkLengthIndexable(final VersionContext outputDescription, final long length, final IOutputCheckActivity activities) throws ManifoldCFException, ServiceInterruption {
     getSession();
     if (maxDocumentLength != null && length > maxDocumentLength.longValue())
       return false;
     return true;
   }
 
-  /** Add (or replace) a document in the output data store using the connector.
-  * This method presumes that the connector object has been configured, and it is thus able to communicate with the output data store should that be
-  * necessary.
-  *@param documentURI is the URI of the document.  The URI is presumed to be the unique identifier which the output data store will use to process
-  * and serve the document.  This URI is constructed by the repository connector which fetches the document, and is thus universal across all output connectors.
-  *@param pipelineDescription includes the description string that was constructed for this document by the getOutputDescription() method.
-  *@param document is the document data to be processed (handed to the output data store).
-  *@param authorityNameString is the name of the authority responsible for authorizing any access tokens passed in with the repository document.  May be null.
-  *@param activities is the handle to an object that the implementer of a pipeline connector may use to perform operations, such as logging processing activity,
-  * or sending a modified document to the next stage in the pipeline.
-  *@return the document status (accepted or permanently rejected).
-  *@throws IOException only if there's a stream error reading the document data.
-  */
+  /**
+   * Add (or replace) a document in the output data store using the connector. This method presumes that the connector object has been configured, and it is thus able to communicate with the output
+   * data store should that be necessary.
+   *
+   * @param documentURI
+   *          is the URI of the document. The URI is presumed to be the unique identifier which the output data store will use to process and serve the document. This URI is constructed by the
+   *          repository connector which fetches the document, and is thus universal across all output connectors.
+   * @param pipelineDescription
+   *          includes the description string that was constructed for this document by the getOutputDescription() method.
+   * @param document
+   *          is the document data to be processed (handed to the output data store).
+   * @param authorityNameString
+   *          is the name of the authority responsible for authorizing any access tokens passed in with the repository document. May be null.
+   * @param activities
+   *          is the handle to an object that the implementer of a pipeline connector may use to perform operations, such as logging processing activity, or sending a modified document to the next
+   *          stage in the pipeline.
+   * @return the document status (accepted or permanently rejected).
+   * @throws IOException
+   *           only if there's a stream error reading the document data.
+   */
   @Override
-  public int addOrReplaceDocumentWithException(String documentURI, VersionContext pipelineDescription, RepositoryDocument document, String authorityNameString, IOutputAddActivity activities)
-    throws ManifoldCFException, ServiceInterruption, IOException
-  {
-    SpecPacker sp = new SpecPacker(pipelineDescription.getSpecification());
+  public int addOrReplaceDocumentWithException(final String documentURI, final VersionContext pipelineDescription, final RepositoryDocument document, final String authorityNameString, final IOutputAddActivity activities)
+      throws ManifoldCFException, ServiceInterruption, IOException {
+    final SpecPacker sp = new SpecPacker(pipelineDescription.getSpecification());
 
     // Establish a session
     getSession();
 
     // Now, go off and call the ingest API.
-    if (poster.indexPost(documentURI,document,sp.getArgs(),authorityNameString,activities))
+    if (poster.indexPost(documentURI, document, sp.getArgs(), authorityNameString, activities))
       return DOCUMENTSTATUS_ACCEPTED;
     return DOCUMENTSTATUS_REJECTED;
   }
 
-  /** Remove a document using the connector.
-  * Note that the last outputDescription is included, since it may be necessary for the connector to use such information to know how to properly remove the document.
-  *@param documentURI is the URI of the document.  The URI is presumed to be the unique identifier which the output data store will use to process
-  * and serve the document.  This URI is constructed by the repository connector which fetches the document, and is thus universal across all output connectors.
-  *@param outputDescription is the last description string that was constructed for this document by the getOutputDescription() method above.
-  *@param activities is the handle to an object that the implementer of an output connector may use to perform operations, such as logging processing activity.
-  */
+  /**
+   * Remove a document using the connector. Note that the last outputDescription is included, since it may be necessary for the connector to use such information to know how to properly remove the
+   * document.
+   *
+   * @param documentURI
+   *          is the URI of the document. The URI is presumed to be the unique identifier which the output data store will use to process and serve the document. This URI is constructed by the
+   *          repository connector which fetches the document, and is thus universal across all output connectors.
+   * @param outputDescription
+   *          is the last description string that was constructed for this document by the getOutputDescription() method above.
+   * @param activities
+   *          is the handle to an object that the implementer of an output connector may use to perform operations, such as logging processing activity.
+   */
   @Override
-  public void removeDocument(String documentURI, String outputDescription, IOutputRemoveActivity activities)
-    throws ManifoldCFException, ServiceInterruption
-  {
+  public void removeDocument(final String documentURI, final String outputDescription, final IOutputRemoveActivity activities) throws ManifoldCFException, ServiceInterruption {
     // Establish a session
     getSession();
-    poster.deletePost(documentURI,activities);
+    poster.deletePost(documentURI, activities);
   }
 
-  /** Notify the connector of a completed job.
-  * This is meant to allow the connector to flush any internal data structures it has been keeping around, or to tell the output repository that this
-  * is a good time to synchronize things.  It is called whenever a job is either completed or aborted.
-  *@param activities is the handle to an object that the implementer of an output connector may use to perform operations, such as logging processing activity.
-  */
+  /**
+   * Notify the connector of a completed job. This is meant to allow the connector to flush any internal data structures it has been keeping around, or to tell the output repository that this is a
+   * good time to synchronize things. It is called whenever a job is either completed or aborted.
+   *
+   * @param activities
+   *          is the handle to an object that the implementer of an output connector may use to perform operations, such as logging processing activity.
+   */
   @Override
-  public void noteJobComplete(IOutputNotifyActivity activities)
-    throws ManifoldCFException, ServiceInterruption
-  {
+  public void noteJobComplete(final IOutputNotifyActivity activities) throws ManifoldCFException, ServiceInterruption {
     // Establish a session
     getSession();
-    
+
     // Do a commit post
-    if (doCommits)
-    {
+    if (doCommits) {
       poster.commitPost();
     }
   }
 
   // UI support methods.
   //
-  // These support methods come in two varieties.  The first bunch is involved in setting up connection configuration information.  The second bunch
-  // is involved in presenting and editing output specification information for a job.  The two kinds of methods are accordingly treated differently,
-  // in that the first bunch cannot assume that the current connector object is connected, while the second bunch can.  That is why the first bunch
+  // These support methods come in two varieties. The first bunch is involved in setting up connection configuration information. The second bunch
+  // is involved in presenting and editing output specification information for a job. The two kinds of methods are accordingly treated differently,
+  // in that the first bunch cannot assume that the current connector object is connected, while the second bunch can. That is why the first bunch
   // receives a thread context argument for all UI methods, while the second bunch does not need one (since it has already been applied via the connect()
   // method, above).
-    
-  /** Output the configuration header section.
-  * This method is called in the head section of the connector's configuration page.  Its purpose is to add the required tabs to the list, and to output any
-  * javascript methods that might be needed by the configuration editing HTML.
-  *@param threadContext is the local thread context.
-  *@param out is the output to which any HTML should be sent.
-  *@param parameters are the configuration parameters, as they currently exist, for this connection being configured.
-  *@param tabsArray is an array of tab names.  Add to this array any tab names that are specific to the connector.
-  */
-  @Override
-  public void outputConfigurationHeader(IThreadContext threadContext, IHTTPOutput out,
-    Locale locale, ConfigParams parameters, List<String> tabsArray)
-    throws ManifoldCFException, IOException
-  {
-    tabsArray.add(Messages.getString(locale,"SolrConnector.SolrType"));
-    tabsArray.add(Messages.getString(locale,"SolrConnector.Server"));
-    tabsArray.add(Messages.getString(locale,"SolrConnector.Zookeeper"));
-    tabsArray.add(Messages.getString(locale,"SolrConnector.Paths"));
-    tabsArray.add(Messages.getString(locale,"SolrConnector.Schema"));
-    tabsArray.add(Messages.getString(locale,"SolrConnector.Arguments"));
-    tabsArray.add(Messages.getString(locale,"SolrConnector.Documents"));
-    tabsArray.add(Messages.getString(locale,"SolrConnector.Commits"));
 
-    out.print(
-"<script type=\"text/javascript\">\n"+
-"<!--\n"+
-"function SolrDeleteCertificate(aliasName)\n"+
-"{\n"+
-"  editconnection.solrkeystorealias.value = aliasName;\n"+
-"  editconnection.configop.value = \"Delete\";\n"+
-"  postForm();\n"+
-"}\n"+
-"\n"+
-"function SolrAddCertificate()\n"+
-"{\n"+
-"  if (editconnection.solrcertificate.value == \"\")\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.ChooseACertificateFile")+"\");\n"+
-"    editconnection.solrcertificate.focus();\n"+
-"  }\n"+
-"  else\n"+
-"  {\n"+
-"    editconnection.configop.value = \"Add\";\n"+
-"    postForm();\n"+
-"  }\n"+
-"}\n"+
-"\n"+
-"function checkConfig()\n"+
-"{\n"+
-"  if (editconnection.servername.value == \"\")\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.PleaseSupplyAValidSolrServerName")+"\");\n"+
-"    editconnection.servername.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.serverport.value != \"\" && !isInteger(editconnection.serverport.value))\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.SolrServerPortMustBeAValidInteger")+"\");\n"+
-"    editconnection.serverport.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.webappname.value != \"\" && editconnection.webappname.value.indexOf(\"/\") != -1)\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.WebApplicationNameCannotHaveCharacters")+"\");\n"+
-"    editconnection.webappname.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.core.value != \"\" && editconnection.core.value.indexOf(\"/\") != -1)\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.CoreNameCannotHaveCharacters")+"\");\n"+
-"    editconnection.core.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.webappname.value == \"\" && editconnection.core.value != \"\")\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.WebApplicationMustBeSpecifiedIfCoreIsSpecified")+"\");\n"+
-"    editconnection.webappname.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (!isInteger(editconnection.connectiontimeout.value))\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.ConnectionTimeoutMustBeInteger")+"\");\n"+
-"    editconnection.connectiontimeout.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (!isInteger(editconnection.sockettimeout.value))\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.SocketTimeoutMustBeInteger")+"\");\n"+
-"    editconnection.sockettimeout.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.updatepath.value != \"\" && editconnection.updatepath.value.substring(0,1) != \"/\")\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.UpdatePathMustStartWithACharacter")+"\");\n"+
-"    editconnection.updatepath.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.removepath.value != \"\" && editconnection.removepath.value.substring(0,1) != \"/\")\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.RemovePathMustStartWithACharacter")+"\");\n"+
-"    editconnection.removepath.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.statuspath.value != \"\" && editconnection.statuspath.value.substring(0,1) != \"/\")\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.StatusPathMustStartWithACharacter")+"\");\n"+
-"    editconnection.statuspath.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.maxdocumentlength.value != \"\" && !isInteger(editconnection.maxdocumentlength.value))\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.MaximumDocumentLengthMustBAnInteger")+"\");\n"+
-"    editconnection.maxdocumentlength.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.commitwithin.value != \"\" && !isInteger(editconnection.commitwithin.value))\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.CommitWithinValueMustBeAnInteger")+"\");\n"+
-"    editconnection.commitwithin.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.znodepath.value != \"\" && editconnection.znodepath.value.substring(0,1) != \"/\")\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.ZnodePathMustStartWithACharacter")+"\");\n"+
-"    editconnection.znodepath.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  return true;\n"+
-"}\n"+
-"\n"+
-"function checkConfigForSave()\n"+
-"{\n"+
-"  if (editconnection.servername.value == \"\")\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.PleaseSupplyAValidSolrServerName")+"\");\n"+
-"    SelectTab(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.Server")+"\");\n"+
-"    editconnection.servername.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.serverport.value != \"\" && !isInteger(editconnection.serverport.value))\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.SolrServerPortMustBeAValidInteger")+"\");\n"+
-"    SelectTab(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.Server")+"\");\n"+
-"    editconnection.serverport.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.webappname.value != \"\" && editconnection.webappname.value.indexOf(\"/\") != -1)\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.WebApplicationNameCannotHaveCharacters")+"\");\n"+
-"    SelectTab(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.Server")+"\");\n"+
-"    editconnection.webappname.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.core.value == \"\")\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.PleaseSupplySolrCoreName")+"\");\n"+
-"    editconnection.core.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.core.value != \"\" && editconnection.core.value.indexOf(\"/\") != -1)\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.CoreNameCannotHaveCharacters")+"\");\n"+
-"    SelectTab(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.Server")+"\");\n"+
-"    editconnection.core.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.webappname.value == \"\" && editconnection.core.value != \"\")\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.WebApplicationMustBeSpecifiedIfCoreIsSpecified")+"\");\n"+
-"    SelectTab(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.Server")+"\");\n"+
-"    editconnection.webappname.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (!isInteger(editconnection.connectiontimeout.value))\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.ConnectionTimeoutMustBeInteger")+"\");\n"+
-"    SelectTab(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.Server")+"\");\n"+
-"    editconnection.connectiontimeout.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (!isInteger(editconnection.sockettimeout.value))\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.SocketTimeoutMustBeInteger")+"\");\n"+
-"    SelectTab(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.Server")+"\");\n"+
-"    editconnection.sockettimeout.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.updatepath.value != \"\" && editconnection.updatepath.value.substring(0,1) != \"/\")\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.UpdatePathMustStartWithACharacter")+"\");\n"+
-"    SelectTab(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.Paths")+"\");\n"+
-"    editconnection.updatepath.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.removepath.value != \"\" && editconnection.removepath.value.substring(0,1) != \"/\")\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.RemovePathMustStartWithACharacter")+"\");\n"+
-"    SelectTab(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.Paths")+"\");\n"+
-"    editconnection.removepath.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.statuspath.value != \"\" && editconnection.statuspath.value.substring(0,1) != \"/\")\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.StatusPathMustStartWithACharacter")+"\");\n"+
-"    SelectTab(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.Paths")+"\");\n"+
-"    editconnection.statuspath.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.maxdocumentlength.value != \"\" && !isInteger(editconnection.maxdocumentlength.value))\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.MaximumDocumentLengthMustBeAnInteger")+"\");\n"+
-"    SelectTab(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.Documents")+"\");\n"+
-"    editconnection.maxdocumentlength.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.maxdocumentlength.value == \"\" && ((editconnection.extractupdatecheckbox.value == \"true\" && editconnection.extractupdate.checked == false) || (editconnection.extractupdatecheckbox.value != \"true\" && editconnection.extractupdate.value != \"true\")))\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.MaximumDocumentLengthRequiredUnlessExtractingUpdateHandler")+"\");\n"+
-"    SelectTab(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.Documents")+"\");\n"+
-"    editconnection.maxdocumentlength.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.contentfield.value == \"\" && ((editconnection.extractupdatecheckbox.value == \"true\" && editconnection.extractupdate.checked == false) || (editconnection.extractupdatecheckbox.value != \"true\" && editconnection.extractupdate.value != \"true\")))\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.ContentFieldNameRequiredUnlessExtractingUpdateHandler")+"\");\n"+
-"    SelectTab(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.Schema")+"\");\n"+
-"    editconnection.contentfield.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.commitwithin.value != \"\" && !isInteger(editconnection.commitwithin.value))\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.CommitWithinValueMustBeAnInteger")+"\");\n"+
-"    SelectTab(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.Commits")+"\");\n"+
-"    editconnection.commitwithin.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  if (editconnection.znodepath.value != \"\" && editconnection.znodepath.value.substring(0,1) != \"/\")\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.ZnodePathMustStartWithACharacter")+"\");\n"+
-"    editconnection.znodepath.focus();\n"+
-"    return false;\n"+
-"  }\n"+
-"  return true;\n"+
-"}\n"+
-"\n"+
-"function deleteZookeeperHost(i)\n"+
-"{\n"+
-"  // Set the operation\n"+
-"  eval(\"editconnection.op_zookeeper_\"+i+\".value=\\\"Delete\\\"\");\n"+
-"  // Submit\n"+
-"  if (editconnection.count_zookeeper.value==i)\n"+
-"    postFormSetAnchor(\"zookeeper\");\n"+
-"  else\n"+
-"    postFormSetAnchor(\"zookeeper_\"+i)\n"+
-"  // Undo, so we won't get two deletes next time\n"+
-"  eval(\"editconnection.op_zookeeper_\"+i+\".value=\\\"Continue\\\"\");\n"+
-"}\n"+
-"\n"+
-"function addZookeeperHost()\n"+
-"{\n"+
-"  if (editconnection.host_zookeeper.value == \"\")\n"+
-"  {\n"+
-"    alert(\"" + Messages.getBodyJavascriptString(locale,"SolrConnector.ZookeeperHostCannotBeNull")+"\");\n"+
-"    editconnection.host_zookeeper.focus();\n"+
-"    return;\n"+
-"  }\n"+
-"  if (editconnection.port_zookeeper.value == \"\")\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.ZookeeperPortCannotBeNull")+"\");\n"+
-"    editconnection.port_zookeeper.focus();\n"+
-"    return;\n"+
-"  }\n"+
-"  if (!isInteger(editconnection.port_zookeeper.value))\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.ZookeeperPortMustBeAnInteger")+"\");\n"+
-"    editconnection.port_zookeeper.focus();\n"+
-"    return;\n"+
-"  }\n"+
-"  editconnection.op_zookeeper.value=\"Add\";\n"+
-"  postFormSetAnchor(\"zookeeper\");\n"+
-"}\n"+
-"\n"+
-"function deleteArgument(i)\n"+
-"{\n"+
-"  // Set the operation\n"+
-"  eval(\"editconnection.argument_\"+i+\"_op.value=\\\"Delete\\\"\");\n"+
-"  // Submit\n"+
-"  if (editconnection.argument_count.value==i)\n"+
-"    postFormSetAnchor(\"argument\");\n"+
-"  else\n"+
-"    postFormSetAnchor(\"argument_\"+i)\n"+
-"  // Undo, so we won't get two deletes next time\n"+
-"  eval(\"editconnection.argument_\"+i+\"_op.value=\\\"Continue\\\"\");\n"+
-"}\n"+
-"\n"+
-"function addArgument()\n"+
-"{\n"+
-"  if (editconnection.argument_name.value == \"\")\n"+
-"  {\n"+
-"    alert(\""+Messages.getBodyJavascriptString(locale,"SolrConnector.ArgumentNameCannotBeAnEmptyString")+"\");\n"+
-"    editconnection.argument_name.focus();\n"+
-"    return;\n"+
-"  }\n"+
-"  editconnection.argument_op.value=\"Add\";\n"+
-"  postFormSetAnchor(\"argument\");\n"+
-"}\n"+
-"\n"+
-"//-->\n"+
-"</script>\n"
-    );
+  /**
+   * Output the configuration header section. This method is called in the head section of the connector's configuration page. Its purpose is to add the required tabs to the list, and to output any
+   * javascript methods that might be needed by the configuration editing HTML.
+   *
+   * @param threadContext
+   *          is the local thread context.
+   * @param out
+   *          is the output to which any HTML should be sent.
+   * @param parameters
+   *          are the configuration parameters, as they currently exist, for this connection being configured.
+   * @param tabsArray
+   *          is an array of tab names. Add to this array any tab names that are specific to the connector.
+   */
+  @Override
+  public void outputConfigurationHeader(final IThreadContext threadContext, final IHTTPOutput out, final Locale locale, final ConfigParams parameters, final List<String> tabsArray) throws ManifoldCFException, IOException {
+    tabsArray.add(Messages.getString(locale, "SolrConnector.SolrType"));
+    tabsArray.add(Messages.getString(locale, "SolrConnector.Server"));
+    tabsArray.add(Messages.getString(locale, "SolrConnector.Zookeeper"));
+    tabsArray.add(Messages.getString(locale, "SolrConnector.Paths"));
+    tabsArray.add(Messages.getString(locale, "SolrConnector.Schema"));
+    tabsArray.add(Messages.getString(locale, "SolrConnector.Arguments"));
+    tabsArray.add(Messages.getString(locale, "SolrConnector.Documents"));
+    tabsArray.add(Messages.getString(locale, "SolrConnector.Commits"));
+
+    out.print("<script type=\"text/javascript\">\n" + "<!--\n" + "function SolrDeleteCertificate(aliasName)\n" + "{\n" + "  editconnection.solrkeystorealias.value = aliasName;\n" + "  editconnection.configop.value = \"Delete\";\n" + "  postForm();\n"
+        + "}\n" + "\n" + "function SolrAddCertificate()\n" + "{\n" + "  if (editconnection.solrcertificate.value == \"\")\n" + "  {\n" + "    alert(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.ChooseACertificateFile") + "\");\n"
+        + "    editconnection.solrcertificate.focus();\n" + "  }\n" + "  else\n" + "  {\n" + "    editconnection.configop.value = \"Add\";\n" + "    postForm();\n" + "  }\n" + "}\n" + "\n" + "function checkConfig()\n" + "{\n"
+        + "  if (editconnection.servername.value == \"\")\n" + "  {\n" + "    alert(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.PleaseSupplyAValidSolrServerName") + "\");\n" + "    editconnection.servername.focus();\n"
+        + "    return false;\n" + "  }\n" + "  if (editconnection.serverport.value != \"\" && !isInteger(editconnection.serverport.value))\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.SolrServerPortMustBeAValidInteger") + "\");\n" + "    editconnection.serverport.focus();\n" + "    return false;\n" + "  }\n"
+        + "  if (editconnection.webappname.value != \"\" && editconnection.webappname.value.indexOf(\"/\") != -1)\n" + "  {\n" + "    alert(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.WebApplicationNameCannotHaveCharacters")
+        + "\");\n" + "    editconnection.webappname.focus();\n" + "    return false;\n" + "  }\n" + "  if (editconnection.core.value != \"\" && editconnection.core.value.indexOf(\"/\") != -1)\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.CoreNameCannotHaveCharacters") + "\");\n" + "    editconnection.core.focus();\n" + "    return false;\n" + "  }\n"
+        + "  if (editconnection.webappname.value == \"\" && editconnection.core.value != \"\")\n" + "  {\n" + "    alert(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.WebApplicationMustBeSpecifiedIfCoreIsSpecified") + "\");\n"
+        + "    editconnection.webappname.focus();\n" + "    return false;\n" + "  }\n" + "  if (!isInteger(editconnection.connectiontimeout.value))\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.ConnectionTimeoutMustBeInteger") + "\");\n" + "    editconnection.connectiontimeout.focus();\n" + "    return false;\n" + "  }\n"
+        + "  if (!isInteger(editconnection.sockettimeout.value))\n" + "  {\n" + "    alert(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.SocketTimeoutMustBeInteger") + "\");\n" + "    editconnection.sockettimeout.focus();\n"
+        + "    return false;\n" + "  }\n" + "  if (editconnection.updatepath.value != \"\" && editconnection.updatepath.value.substring(0,1) != \"/\")\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.UpdatePathMustStartWithACharacter") + "\");\n" + "    editconnection.updatepath.focus();\n" + "    return false;\n" + "  }\n"
+        + "  if (editconnection.removepath.value != \"\" && editconnection.removepath.value.substring(0,1) != \"/\")\n" + "  {\n" + "    alert(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.RemovePathMustStartWithACharacter")
+        + "\");\n" + "    editconnection.removepath.focus();\n" + "    return false;\n" + "  }\n" + "  if (editconnection.statuspath.value != \"\" && editconnection.statuspath.value.substring(0,1) != \"/\")\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.StatusPathMustStartWithACharacter") + "\");\n" + "    editconnection.statuspath.focus();\n" + "    return false;\n" + "  }\n"
+        + "  if (editconnection.maxdocumentlength.value != \"\" && !isInteger(editconnection.maxdocumentlength.value))\n" + "  {\n" + "    alert(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.MaximumDocumentLengthMustBAnInteger")
+        + "\");\n" + "    editconnection.maxdocumentlength.focus();\n" + "    return false;\n" + "  }\n" + "  if (editconnection.commitwithin.value != \"\" && !isInteger(editconnection.commitwithin.value))\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.CommitWithinValueMustBeAnInteger") + "\");\n" + "    editconnection.commitwithin.focus();\n" + "    return false;\n" + "  }\n"
+        + "  if (editconnection.znodepath.value != \"\" && editconnection.znodepath.value.substring(0,1) != \"/\")\n" + "  {\n" + "    alert(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.ZnodePathMustStartWithACharacter") + "\");\n"
+        + "    editconnection.znodepath.focus();\n" + "    return false;\n" + "  }\n" + "  return true;\n" + "}\n" + "\n" + "function checkConfigForSave()\n" + "{\n" + "  if (editconnection.servername.value == \"\")\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.PleaseSupplyAValidSolrServerName") + "\");\n" + "    SelectTab(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.Server") + "\");\n"
+        + "    editconnection.servername.focus();\n" + "    return false;\n" + "  }\n" + "  if (editconnection.serverport.value != \"\" && !isInteger(editconnection.serverport.value))\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.SolrServerPortMustBeAValidInteger") + "\");\n" + "    SelectTab(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.Server") + "\");\n"
+        + "    editconnection.serverport.focus();\n" + "    return false;\n" + "  }\n" + "  if (editconnection.webappname.value != \"\" && editconnection.webappname.value.indexOf(\"/\") != -1)\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.WebApplicationNameCannotHaveCharacters") + "\");\n" + "    SelectTab(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.Server") + "\");\n"
+        + "    editconnection.webappname.focus();\n" + "    return false;\n" + "  }\n" + "  if (editconnection.core.value == \"\")\n" + "  {\n" + "    alert(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.PleaseSupplySolrCoreName")
+        + "\");\n" + "    editconnection.core.focus();\n" + "    return false;\n" + "  }\n" + "  if (editconnection.core.value != \"\" && editconnection.core.value.indexOf(\"/\") != -1)\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.CoreNameCannotHaveCharacters") + "\");\n" + "    SelectTab(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.Server") + "\");\n" + "    editconnection.core.focus();\n"
+        + "    return false;\n" + "  }\n" + "  if (editconnection.webappname.value == \"\" && editconnection.core.value != \"\")\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.WebApplicationMustBeSpecifiedIfCoreIsSpecified") + "\");\n" + "    SelectTab(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.Server") + "\");\n"
+        + "    editconnection.webappname.focus();\n" + "    return false;\n" + "  }\n" + "  if (!isInteger(editconnection.connectiontimeout.value))\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.ConnectionTimeoutMustBeInteger") + "\");\n" + "    SelectTab(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.Server") + "\");\n"
+        + "    editconnection.connectiontimeout.focus();\n" + "    return false;\n" + "  }\n" + "  if (!isInteger(editconnection.sockettimeout.value))\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.SocketTimeoutMustBeInteger") + "\");\n" + "    SelectTab(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.Server") + "\");\n"
+        + "    editconnection.sockettimeout.focus();\n" + "    return false;\n" + "  }\n" + "  if (editconnection.updatepath.value != \"\" && editconnection.updatepath.value.substring(0,1) != \"/\")\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.UpdatePathMustStartWithACharacter") + "\");\n" + "    SelectTab(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.Paths") + "\");\n"
+        + "    editconnection.updatepath.focus();\n" + "    return false;\n" + "  }\n" + "  if (editconnection.removepath.value != \"\" && editconnection.removepath.value.substring(0,1) != \"/\")\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.RemovePathMustStartWithACharacter") + "\");\n" + "    SelectTab(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.Paths") + "\");\n"
+        + "    editconnection.removepath.focus();\n" + "    return false;\n" + "  }\n" + "  if (editconnection.statuspath.value != \"\" && editconnection.statuspath.value.substring(0,1) != \"/\")\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.StatusPathMustStartWithACharacter") + "\");\n" + "    SelectTab(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.Paths") + "\");\n"
+        + "    editconnection.statuspath.focus();\n" + "    return false;\n" + "  }\n" + "  if (editconnection.maxdocumentlength.value != \"\" && !isInteger(editconnection.maxdocumentlength.value))\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.MaximumDocumentLengthMustBeAnInteger") + "\");\n" + "    SelectTab(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.Documents") + "\");\n"
+        + "    editconnection.maxdocumentlength.focus();\n" + "    return false;\n" + "  }\n"
+        + "  if (editconnection.maxdocumentlength.value == \"\" && ((editconnection.extractupdatecheckbox.value == \"true\" && editconnection.extractupdate.checked == false) || (editconnection.extractupdatecheckbox.value != \"true\" && editconnection.extractupdate.value != \"true\")))\n"
+        + "  {\n" + "    alert(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.MaximumDocumentLengthRequiredUnlessExtractingUpdateHandler") + "\");\n" + "    SelectTab(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.Documents") + "\");\n" + "    editconnection.maxdocumentlength.focus();\n" + "    return false;\n" + "  }\n"
+        + "  if (editconnection.contentfield.value == \"\" && ((editconnection.extractupdatecheckbox.value == \"true\" && editconnection.extractupdate.checked == false) || (editconnection.extractupdatecheckbox.value != \"true\" && editconnection.extractupdate.value != \"true\")))\n"
+        + "  {\n" + "    alert(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.ContentFieldNameRequiredUnlessExtractingUpdateHandler") + "\");\n" + "    SelectTab(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.Schema")
+        + "\");\n" + "    editconnection.contentfield.focus();\n" + "    return false;\n" + "  }\n" + "  if (editconnection.commitwithin.value != \"\" && !isInteger(editconnection.commitwithin.value))\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.CommitWithinValueMustBeAnInteger") + "\");\n" + "    SelectTab(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.Commits") + "\");\n"
+        + "    editconnection.commitwithin.focus();\n" + "    return false;\n" + "  }\n" + "  if (editconnection.znodepath.value != \"\" && editconnection.znodepath.value.substring(0,1) != \"/\")\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.ZnodePathMustStartWithACharacter") + "\");\n" + "    editconnection.znodepath.focus();\n" + "    return false;\n" + "  }\n" + "  return true;\n" + "}\n" + "\n"
+        + "function deleteZookeeperHost(i)\n" + "{\n" + "  // Set the operation\n" + "  eval(\"editconnection.op_zookeeper_\"+i+\".value=\\\"Delete\\\"\");\n" + "  // Submit\n" + "  if (editconnection.count_zookeeper.value==i)\n"
+        + "    postFormSetAnchor(\"zookeeper\");\n" + "  else\n" + "    postFormSetAnchor(\"zookeeper_\"+i)\n" + "  // Undo, so we won't get two deletes next time\n" + "  eval(\"editconnection.op_zookeeper_\"+i+\".value=\\\"Continue\\\"\");\n"
+        + "}\n" + "\n" + "function addZookeeperHost()\n" + "{\n" + "  if (editconnection.host_zookeeper.value == \"\")\n" + "  {\n" + "    alert(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.ZookeeperHostCannotBeNull") + "\");\n"
+        + "    editconnection.host_zookeeper.focus();\n" + "    return;\n" + "  }\n" + "  if (editconnection.port_zookeeper.value == \"\")\n" + "  {\n" + "    alert(\""
+        + Messages.getBodyJavascriptString(locale, "SolrConnector.ZookeeperPortCannotBeNull") + "\");\n" + "    editconnection.port_zookeeper.focus();\n" + "    return;\n" + "  }\n" + "  if (!isInteger(editconnection.port_zookeeper.value))\n"
+        + "  {\n" + "    alert(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.ZookeeperPortMustBeAnInteger") + "\");\n" + "    editconnection.port_zookeeper.focus();\n" + "    return;\n" + "  }\n"
+        + "  editconnection.op_zookeeper.value=\"Add\";\n" + "  postFormSetAnchor(\"zookeeper\");\n" + "}\n" + "\n" + "function deleteArgument(i)\n" + "{\n" + "  // Set the operation\n"
+        + "  eval(\"editconnection.argument_\"+i+\"_op.value=\\\"Delete\\\"\");\n" + "  // Submit\n" + "  if (editconnection.argument_count.value==i)\n" + "    postFormSetAnchor(\"argument\");\n" + "  else\n"
+        + "    postFormSetAnchor(\"argument_\"+i)\n" + "  // Undo, so we won't get two deletes next time\n" + "  eval(\"editconnection.argument_\"+i+\"_op.value=\\\"Continue\\\"\");\n" + "}\n" + "\n" + "function addArgument()\n" + "{\n"
+        + "  if (editconnection.argument_name.value == \"\")\n" + "  {\n" + "    alert(\"" + Messages.getBodyJavascriptString(locale, "SolrConnector.ArgumentNameCannotBeAnEmptyString") + "\");\n" + "    editconnection.argument_name.focus();\n"
+        + "    return;\n" + "  }\n" + "  editconnection.argument_op.value=\"Add\";\n" + "  postFormSetAnchor(\"argument\");\n" + "}\n" + "\n" + "//-->\n" + "</script>\n");
 
   }
-  
-  /** Output the configuration body section.
-  * This method is called in the body section of the connector's configuration page.  Its purpose is to present the required form elements for editing.
-  * The coder can presume that the HTML that is output from this configuration will be within appropriate &lt;html&gt;, &lt;body&gt;, and &lt;form&gt; tags.  The name of the
-  * form is "editconnection".
-  *@param threadContext is the local thread context.
-  *@param out is the output to which any HTML should be sent.
-  *@param parameters are the configuration parameters, as they currently exist, for this connection being configured.
-  *@param tabName is the current tab name.
-  */
+
+  /**
+   * Output the configuration body section. This method is called in the body section of the connector's configuration page. Its purpose is to present the required form elements for editing. The coder
+   * can presume that the HTML that is output from this configuration will be within appropriate &lt;html&gt;, &lt;body&gt;, and &lt;form&gt; tags. The name of the form is "editconnection".
+   *
+   * @param threadContext
+   *          is the local thread context.
+   * @param out
+   *          is the output to which any HTML should be sent.
+   * @param parameters
+   *          are the configuration parameters, as they currently exist, for this connection being configured.
+   * @param tabName
+   *          is the current tab name.
+   */
   @Override
-  public void outputConfigurationBody(IThreadContext threadContext, IHTTPOutput out,
-    Locale locale, ConfigParams parameters, String tabName)
-    throws ManifoldCFException, IOException
-  {
+  public void outputConfigurationBody(final IThreadContext threadContext, final IHTTPOutput out, final Locale locale, final ConfigParams parameters, final String tabName) throws ManifoldCFException, IOException {
     String type = parameters.getParameter(SolrConfig.PARAM_SOLR_TYPE);
     if (type == null)
       type = SolrConfig.SOLR_TYPE_STANDARD;
-    
+
     String protocol = parameters.getParameter(SolrConfig.PARAM_PROTOCOL);
     if (protocol == null)
       protocol = SolrConfig.PROTOCOL_TYPE_HTTP;
-		
+
     String server = parameters.getParameter(SolrConfig.PARAM_SERVER);
     if (server == null)
       server = "localhost";
@@ -973,28 +700,28 @@
 
     String znodePath = parameters.getParameter(SolrConfig.PARAM_ZOOKEEPER_ZNODE_PATH);
     if (znodePath == null)
-    	znodePath = "";
-    
+      znodePath = "";
+
     String collection = parameters.getParameter(SolrConfig.PARAM_COLLECTION);
     if (collection == null)
       collection = "collection1";
-    
+
     String connectionTimeout = parameters.getParameter(SolrConfig.PARAM_CONNECTION_TIMEOUT);
     if (connectionTimeout == null)
       connectionTimeout = "60";
-    
+
     String socketTimeout = parameters.getParameter(SolrConfig.PARAM_SOCKET_TIMEOUT);
     if (socketTimeout == null)
       socketTimeout = "900";
 
-    String zkClientTimeout = parameters.getParameter(SolrConfig.PARAM_ZOOKEEPER_CLIENT_TIMEOUT);
-    if (zkClientTimeout == null)
-      zkClientTimeout = "60";
+    String zkSocketTimeout = parameters.getParameter(SolrConfig.PARAM_ZOOKEEPER_SOCKET_TIMEOUT);
+    if (zkSocketTimeout == null)
+      zkSocketTimeout = "60";
 
-    String zkConnectTimeout = parameters.getParameter(SolrConfig.PARAM_ZOOKEEPER_CONNECT_TIMEOUT);
-    if (zkConnectTimeout == null)
-      zkConnectTimeout = "60";
-    
+    String zkConnectionTimeout = parameters.getParameter(SolrConfig.PARAM_ZOOKEEPER_CONNECTION_TIMEOUT);
+    if (zkConnectionTimeout == null)
+      zkConnectionTimeout = "60";
+
     String updatePath = parameters.getParameter(SolrConfig.PARAM_UPDATEPATH);
     if (updatePath == null)
       updatePath = "/update/extract";
@@ -1014,11 +741,11 @@
     String originalSizeField = parameters.getParameter(SolrConfig.PARAM_ORIGINALSIZEFIELD);
     if (originalSizeField == null)
       originalSizeField = "";
-    
+
     String modifiedDateField = parameters.getParameter(SolrConfig.PARAM_MODIFIEDDATEFIELD);
     if (modifiedDateField == null)
       modifiedDateField = "";
-    
+
     String createdDateField = parameters.getParameter(SolrConfig.PARAM_CREATEDDATEFIELD);
     if (createdDateField == null)
       createdDateField = "";
@@ -1026,11 +753,11 @@
     String indexedDateField = parameters.getParameter(SolrConfig.PARAM_INDEXEDDATEFIELD);
     if (indexedDateField == null)
       indexedDateField = "";
-    
+
     String fileNameField = parameters.getParameter(SolrConfig.PARAM_FILENAMEFIELD);
     if (fileNameField == null)
       fileNameField = "";
-    
+
     String mimeTypeField = parameters.getParameter(SolrConfig.PARAM_MIMETYPEFIELD);
     if (mimeTypeField == null)
       mimeTypeField = "";
@@ -1038,7 +765,7 @@
     String contentField = parameters.getParameter(SolrConfig.PARAM_CONTENTFIELD);
     if (contentField == null)
       contentField = "";
-    
+
     String useExtractUpdate = parameters.getParameter(SolrConfig.PARAM_EXTRACTUPDATE);
     if (useExtractUpdate == null || useExtractUpdate.length() == 0)
       useExtractUpdate = "true";
@@ -1050,1281 +777,795 @@
     String userID = parameters.getParameter(SolrConfig.PARAM_USERID);
     if (userID == null)
       userID = "";
-		
+
     String password = parameters.getObfuscatedParameter(SolrConfig.PARAM_PASSWORD);
     if (password == null)
       password = "";
     else
       password = out.mapPasswordToKey(password);
-    
+
     String commits = parameters.getParameter(SolrConfig.PARAM_COMMITS);
     if (commits == null)
       commits = "true";
-    
+
     String commitWithin = parameters.getParameter(SolrConfig.PARAM_COMMITWITHIN);
     if (commitWithin == null)
       commitWithin = "";
 
-    String solrKeystore = parameters.getParameter(SolrConfig.PARAM_KEYSTORE);
+    final String solrKeystore = parameters.getParameter(SolrConfig.PARAM_KEYSTORE);
     IKeystoreManager localKeystore;
     if (solrKeystore == null)
       localKeystore = KeystoreManagerFactory.make("");
     else
-      localKeystore = KeystoreManagerFactory.make("",solrKeystore);
+      localKeystore = KeystoreManagerFactory.make("", solrKeystore);
 
     String maxLength = parameters.getParameter(SolrConfig.PARAM_MAXLENGTH);
     if (maxLength == null)
       maxLength = "";
-    
+
     String includedMimeTypes = parameters.getParameter(SolrConfig.PARAM_INCLUDEDMIMETYPES);
     if (includedMimeTypes == null)
       includedMimeTypes = "";
-    
+
     String excludedMimeTypes = parameters.getParameter(SolrConfig.PARAM_EXCLUDEDMIMETYPES);
     if (excludedMimeTypes == null)
       excludedMimeTypes = "";
-    
+
     // "SOLR type" tab
-    if (tabName.equals(Messages.getString(locale,"SolrConnector.SolrType")))
-    {
-      out.print(
-"<table class=\"displaytable\">\n"+
-"  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.SolrType2") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <select name=\"solrtype\">\n"+
-"        <option value=\""+SolrConfig.SOLR_TYPE_STANDARD+"\""+(type.equals(SolrConfig.SOLR_TYPE_STANDARD)?" selected=\"true\"":"")+">"+Messages.getBodyString(locale,"SolrConnector.SingleServer")+"</option>\n"+
-"        <option value=\""+SolrConfig.SOLR_TYPE_SOLRCLOUD+"\""+(type.equals(SolrConfig.SOLR_TYPE_SOLRCLOUD)?" selected=\"true\"":"")+">"+Messages.getBodyString(locale,"SolrConnector.SolrCloud")+"</option>\n"+
-"      </select>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"</table>\n"
-      );
-    }
-    else
-    {
+    if (tabName.equals(Messages.getString(locale, "SolrConnector.SolrType"))) {
+      out.print("<table class=\"displaytable\">\n" + "  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.SolrType2") + "</nobr></td>\n"
+          + "    <td class=\"value\">\n" + "      <select name=\"solrtype\">\n" + "        <option value=\"" + SolrConfig.SOLR_TYPE_STANDARD + "\"" + (type.equals(SolrConfig.SOLR_TYPE_STANDARD) ? " selected=\"true\"" : "") + ">"
+          + Messages.getBodyString(locale, "SolrConnector.SingleServer") + "</option>\n" + "        <option value=\"" + SolrConfig.SOLR_TYPE_SOLRCLOUD + "\"" + (type.equals(SolrConfig.SOLR_TYPE_SOLRCLOUD) ? " selected=\"true\"" : "") + ">"
+          + Messages.getBodyString(locale, "SolrConnector.SolrCloud") + "</option>\n" + "      </select>\n" + "    </td>\n" + "  </tr>\n" + "</table>\n");
+    } else {
       // Type tab hiddens
-      out.print(
-"<input type=\"hidden\" name=\"solrtype\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(type)+"\"/>\n"
-      );
+      out.print("<input type=\"hidden\" name=\"solrtype\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(type) + "\"/>\n");
     }
 
     // "Server" tab
     // Always pass the whole keystore as a hidden.
-    if (solrKeystore != null)
-    {
-      out.print(
-"<input type=\"hidden\" name=\"keystoredata\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(solrKeystore)+"\"/>\n"
-      );
+    if (solrKeystore != null) {
+      out.print("<input type=\"hidden\" name=\"keystoredata\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(solrKeystore) + "\"/>\n");
     }
-    out.print(
-"<input name=\"configop\" type=\"hidden\" value=\"Continue\"/>\n"
-    );
-    
-    if (tabName.equals(Messages.getString(locale,"SolrConnector.Server")))
-    {
-      out.print(
-"<table class=\"displaytable\">\n"+
-"  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.Protocol") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <select name=\"serverprotocol\">\n"+
-"        <option value=\""+SolrConfig.PROTOCOL_TYPE_HTTP+"\""+(protocol.equals(SolrConfig.PROTOCOL_TYPE_HTTP)?" selected=\"true\"":"")+">http</option>\n"+
-"        <option value=\""+SolrConfig.PROTOCOL_TYPE_HTTPS+"\""+(protocol.equals(SolrConfig.PROTOCOL_TYPE_HTTPS)?" selected=\"true\"":"")+">https</option>\n"+
-"      </select>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.ServerName") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"servername\" type=\"text\" size=\"32\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(server)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.Port") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"serverport\" type=\"text\" size=\"5\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(port)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.WebApplicationName") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"webappname\" type=\"text\" size=\"16\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(webapp)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.CoreName") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"core\" type=\"text\" size=\"16\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(core)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.ConnectionTimeout") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"connectiontimeout\" type=\"text\" size=\"5\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(connectionTimeout)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.SocketTimeout") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"sockettimeout\" type=\"text\" size=\"5\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(socketTimeout)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.Realm") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"realm\" type=\"text\" size=\"32\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(realm)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.UserID") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"userid\" type=\"text\" size=\"32\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(userID)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.Password") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input type=\"password\" size=\"32\" name=\"password\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(password)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.SSLTrustCertificateList") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input type=\"hidden\" name=\"solrkeystorealias\" value=\"\"/>\n"+
-"      <table class=\"displaytable\">\n"
-      );
+    out.print("<input name=\"configop\" type=\"hidden\" value=\"Continue\"/>\n");
+
+    if (tabName.equals(Messages.getString(locale, "SolrConnector.Server"))) {
+      out.print("<table class=\"displaytable\">\n" + "  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.Protocol") + "</nobr></td>\n"
+          + "    <td class=\"value\">\n" + "      <select name=\"serverprotocol\">\n" + "        <option value=\"" + SolrConfig.PROTOCOL_TYPE_HTTP + "\"" + (protocol.equals(SolrConfig.PROTOCOL_TYPE_HTTP) ? " selected=\"true\"" : "")
+          + ">http</option>\n" + "        <option value=\"" + SolrConfig.PROTOCOL_TYPE_HTTPS + "\"" + (protocol.equals(SolrConfig.PROTOCOL_TYPE_HTTPS) ? " selected=\"true\"" : "") + ">https</option>\n" + "      </select>\n" + "    </td>\n"
+          + "  </tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.ServerName") + "</nobr></td>\n" + "    <td class=\"value\">\n"
+          + "      <input name=\"servername\" type=\"text\" size=\"32\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(server) + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>"
+          + Messages.getBodyString(locale, "SolrConnector.Port") + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <input name=\"serverport\" type=\"text\" size=\"5\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(port)
+          + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.WebApplicationName")
+          + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <input name=\"webappname\" type=\"text\" size=\"16\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(webapp) + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "  <tr>\n"
+          + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.CoreName") + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <input name=\"core\" type=\"text\" size=\"16\" value=\""
+          + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(core) + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>"
+          + Messages.getBodyString(locale, "SolrConnector.ConnectionTimeout") + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <input name=\"connectiontimeout\" type=\"text\" size=\"5\" value=\""
+          + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(connectionTimeout) + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.SocketTimeout")
+          + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <input name=\"sockettimeout\" type=\"text\" size=\"5\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(socketTimeout) + "\"/>\n" + "    </td>\n" + "  </tr>\n"
+          + "  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.Realm") + "</nobr></td>\n" + "    <td class=\"value\">\n"
+          + "      <input name=\"realm\" type=\"text\" size=\"32\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(realm) + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>"
+          + Messages.getBodyString(locale, "SolrConnector.UserID") + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <input name=\"userid\" type=\"text\" size=\"32\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(userID)
+          + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.Password") + "</nobr></td>\n" + "    <td class=\"value\">\n"
+          + "      <input type=\"password\" size=\"32\" name=\"password\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(password) + "\"/>\n" + "    </td>\n" + "  </tr>\n"
+          + "  <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.SSLTrustCertificateList") + "</nobr></td>\n"
+          + "    <td class=\"value\">\n" + "      <input type=\"hidden\" name=\"solrkeystorealias\" value=\"\"/>\n" + "      <table class=\"displaytable\">\n");
       // List the individual certificates in the store, with a delete button for each
-      String[] contents = localKeystore.getContents();
-      if (contents.length == 0)
-      {
-        out.print(
-"        <tr><td class=\"message\" colspan=\"2\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.NoCertificatesPresent") + "</nobr></td></tr>\n"
-        );
-      }
-      else
-      {
+      final String[] contents = localKeystore.getContents();
+      if (contents.length == 0) {
+        out.print("        <tr><td class=\"message\" colspan=\"2\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.NoCertificatesPresent") + "</nobr></td></tr>\n");
+      } else {
         int i = 0;
-        while (i < contents.length)
-        {
-          String alias = contents[i];
+        while (i < contents.length) {
+          final String alias = contents[i];
           String description = localKeystore.getDescription(alias);
           if (description.length() > 128)
-            description = description.substring(0,125) + "...";
-          out.print(
-"        <tr>\n"+
-"          <td class=\"value\"><input type=\"button\" onclick='Javascript:SolrDeleteCertificate(\""+org.apache.manifoldcf.ui.util.Encoder.attributeJavascriptEscape(alias)+"\")' alt=\""+Messages.getAttributeString(locale,"SolrConnector.DeleteCert")+" "+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(alias)+"\" value=\"Delete\"/></td>\n"+
-"          <td>"+org.apache.manifoldcf.ui.util.Encoder.bodyEscape(description)+"</td>\n"+
-"        </tr>\n"
-          );
+            description = description.substring(0, 125) + "...";
+          out.print("        <tr>\n" + "          <td class=\"value\"><input type=\"button\" onclick='Javascript:SolrDeleteCertificate(\"" + org.apache.manifoldcf.ui.util.Encoder.attributeJavascriptEscape(alias) + "\")' alt=\""
+              + Messages.getAttributeString(locale, "SolrConnector.DeleteCert") + " " + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(alias) + "\" value=\"Delete\"/></td>\n" + "          <td>"
+              + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(description) + "</td>\n" + "        </tr>\n");
           i++;
         }
       }
       out.print(
-"      </table>\n"+
-"      <input type=\"button\" onclick='Javascript:SolrAddCertificate()' alt=\"" + Messages.getAttributeString(locale,"SolrConnector.AddCert") + "\" value=\"" + Messages.getAttributeString(locale,"SolrConnector.Add") + "\"/>&nbsp;\n"+
-"      " + Messages.getBodyString(locale,"SolrConnector.Certificate") + "&nbsp;<input name=\"solrcertificate\" size=\"50\" type=\"file\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"</table>\n"
-      );
-    }
-    else
-    {
+          "      </table>\n" + "      <input type=\"button\" onclick='Javascript:SolrAddCertificate()' alt=\"" + Messages.getAttributeString(locale, "SolrConnector.AddCert") + "\" value=\"" + Messages.getAttributeString(locale, "SolrConnector.Add")
+              + "\"/>&nbsp;\n" + "      " + Messages.getBodyString(locale, "SolrConnector.Certificate") + "&nbsp;<input name=\"solrcertificate\" size=\"50\" type=\"file\"/>\n" + "    </td>\n" + "  </tr>\n" + "</table>\n");
+    } else {
       // Server tab hiddens
-      out.print(
-"<input type=\"hidden\" name=\"serverprotocol\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(protocol)+"\"/>\n"+
-"<input type=\"hidden\" name=\"servername\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(server)+"\"/>\n"+
-"<input type=\"hidden\" name=\"serverport\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(port)+"\"/>\n"+
-"<input type=\"hidden\" name=\"webappname\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(webapp)+"\"/>\n"+
-"<input type=\"hidden\" name=\"core\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(core)+"\"/>\n"+
-"<input type=\"hidden\" name=\"connectiontimeout\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(connectionTimeout)+"\"/>\n"+
-"<input type=\"hidden\" name=\"sockettimeout\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(socketTimeout)+"\"/>\n"+
-"<input type=\"hidden\" name=\"realm\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(realm)+"\"/>\n"+
-"<input type=\"hidden\" name=\"userid\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(userID)+"\"/>\n"+
-"<input type=\"hidden\" name=\"password\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(password)+"\"/>\n"
-      );
+      out.print("<input type=\"hidden\" name=\"serverprotocol\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(protocol) + "\"/>\n" + "<input type=\"hidden\" name=\"servername\" value=\""
+          + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(server) + "\"/>\n" + "<input type=\"hidden\" name=\"serverport\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(port) + "\"/>\n"
+          + "<input type=\"hidden\" name=\"webappname\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(webapp) + "\"/>\n" + "<input type=\"hidden\" name=\"core\" value=\""
+          + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(core) + "\"/>\n" + "<input type=\"hidden\" name=\"connectiontimeout\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(connectionTimeout) + "\"/>\n"
+          + "<input type=\"hidden\" name=\"sockettimeout\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(socketTimeout) + "\"/>\n" + "<input type=\"hidden\" name=\"realm\" value=\""
+          + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(realm) + "\"/>\n" + "<input type=\"hidden\" name=\"userid\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(userID) + "\"/>\n"
+          + "<input type=\"hidden\" name=\"password\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(password) + "\"/>\n");
     }
 
     // "Zookeeper" tab
-    if (tabName.equals(Messages.getString(locale,"SolrConnector.Zookeeper")))
-    {
-      out.print(
-"<table class=\"displaytable\">\n"+
-"  <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.ZookeeperHosts") + "</nobr></td>\n"+
-"    <td class=\"boxcell\">\n"+
-"      <table class=\"formtable\">\n"+
-"        <tr class=\"formheaderrow\">\n"+
-"          <td class=\"formcolumnheader\"></td>\n"+
-"          <td class=\"formcolumnheader\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.Host") + "</nobr></td>\n"+
-"          <td class=\"formcolumnheader\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.Port") + "</nobr></td>\n"+
-"        </tr>\n"
-      );
+    if (tabName.equals(Messages.getString(locale, "SolrConnector.Zookeeper"))) {
+      out.print("<table class=\"displaytable\">\n" + "  <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.ZookeeperHosts")
+          + "</nobr></td>\n" + "    <td class=\"boxcell\">\n" + "      <table class=\"formtable\">\n" + "        <tr class=\"formheaderrow\">\n" + "          <td class=\"formcolumnheader\"></td>\n" + "          <td class=\"formcolumnheader\"><nobr>"
+          + Messages.getBodyString(locale, "SolrConnector.Host") + "</nobr></td>\n" + "          <td class=\"formcolumnheader\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.Port") + "</nobr></td>\n" + "        </tr>\n");
 
       // Loop through the existing zookeeper nodes
       int k = 0;
-      for (int i = 0; i < parameters.getChildCount(); i++)
-      {
-        ConfigurationNode cn = parameters.getChild(i);
-        if (cn.getType().equals(SolrConfig.NODE_ZOOKEEPER))
-        {
-          String host = cn.getAttributeValue(SolrConfig.ATTR_HOST);
-          String zkport = cn.getAttributeValue(SolrConfig.ATTR_PORT);
-          String postfix = "zookeeper_"+k;
-          out.print(
-"        <tr class=\""+(((k % 2)==0)?"evenformrow":"oddformrow")+"\">\n"+
-"          <td class=\"formcolumncell\">\n"+
-"            <a name=\""+postfix+"\">\n"+
-"              <input type=\"button\" value=\"" + Messages.getAttributeString(locale,"SolrConnector.Delete") + "\" alt=\""+Messages.getAttributeString(locale,"SolrConnector.DeleteZookeeperHost")+Integer.toString(k+1)+"\" onclick='javascript:deleteZookeeperHost("+Integer.toString(k)+");'/>\n"+
-"              <input type=\"hidden\" name=\""+"op_"+postfix+"\" value=\"Continue\"/>\n"+
-"              <input type=\"hidden\" name=\""+"host_"+postfix+"\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(host)+"\"/>\n"+
-"              <input type=\"hidden\" name=\""+"port_"+postfix+"\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(zkport)+"\"/>\n"+
-"            </a>\n"+
-"          </td>\n"+
-"          <td class=\"formcolumncell\">\n"+
-"            <nobr>"+org.apache.manifoldcf.ui.util.Encoder.bodyEscape(host)+"</nobr>\n"+
-"          </td>\n"+
-"          <td class=\"formcolumncell\">\n"+
-"            <nobr>"+org.apache.manifoldcf.ui.util.Encoder.bodyEscape(zkport)+"</nobr>\n"+
-"          </td>\n"+
-"        </tr>\n"
-          );
+      for (int i = 0; i < parameters.getChildCount(); i++) {
+        final ConfigurationNode cn = parameters.getChild(i);
+        if (cn.getType().equals(SolrConfig.NODE_ZOOKEEPER)) {
+          final String host = cn.getAttributeValue(SolrConfig.ATTR_HOST);
+          final String zkport = cn.getAttributeValue(SolrConfig.ATTR_PORT);
+          final String postfix = "zookeeper_" + k;
+          out.print("        <tr class=\"" + (((k % 2) == 0) ? "evenformrow" : "oddformrow") + "\">\n" + "          <td class=\"formcolumncell\">\n" + "            <a name=\"" + postfix + "\">\n" + "              <input type=\"button\" value=\""
+              + Messages.getAttributeString(locale, "SolrConnector.Delete") + "\" alt=\"" + Messages.getAttributeString(locale, "SolrConnector.DeleteZookeeperHost") + Integer.toString(k + 1) + "\" onclick='javascript:deleteZookeeperHost("
+              + Integer.toString(k) + ");'/>\n" + "              <input type=\"hidden\" name=\"" + "op_" + postfix + "\" value=\"Continue\"/>\n" + "              <input type=\"hidden\" name=\"" + "host_" + postfix + "\" value=\""
+              + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(host) + "\"/>\n" + "              <input type=\"hidden\" name=\"" + "port_" + postfix + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(zkport) + "\"/>\n"
+              + "            </a>\n" + "          </td>\n" + "          <td class=\"formcolumncell\">\n" + "            <nobr>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(host) + "</nobr>\n" + "          </td>\n"
+              + "          <td class=\"formcolumncell\">\n" + "            <nobr>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(zkport) + "</nobr>\n" + "          </td>\n" + "        </tr>\n");
           k++;
         }
       }
       // If this looks like the first time through for this connection, add a default zookeeper setup.
       // Only works because after the first post, parameters always will have children.
-      if (parameters.getChildCount() == 0)
-      {
-        String postfix = "zookeeper_"+k;
-        out.print(
-"        <tr class=\""+(((k % 2)==0)?"evenformrow":"oddformrow")+"\">\n"+
-"          <td class=\"formcolumncell\">\n"+
-"            <a name=\""+postfix+"\">\n"+
-"              <input type=\"button\" value=\"" + Messages.getAttributeString(locale,"SolrConnector.Delete") + "\" alt=\""+Messages.getAttributeString(locale,"SolrConnector.DeleteZookeeperHost")+Integer.toString(k+1)+"\" onclick='javascript:deleteZookeeperHost("+Integer.toString(k)+");'/>\n"+
-"              <input type=\"hidden\" name=\""+"op_"+postfix+"\" value=\"Continue\"/>\n"+
-"              <input type=\"hidden\" name=\""+"host_"+postfix+"\" value=\"localhost\"/>\n"+
-"              <input type=\"hidden\" name=\""+"port_"+postfix+"\" value=\"2181\"/>\n"+
-"            </a>\n"+
-"          </td>\n"+
-"          <td class=\"formcolumncell\">\n"+
-"            <nobr>localhost</nobr>\n"+
-"          </td>\n"+
-"          <td class=\"formcolumncell\">\n"+
-"            <nobr>2181</nobr>\n"+
-"          </td>\n"+
-"        </tr>\n"
-        );
+      if (parameters.getChildCount() == 0) {
+        final String postfix = "zookeeper_" + k;
+        out.print("        <tr class=\"" + (((k % 2) == 0) ? "evenformrow" : "oddformrow") + "\">\n" + "          <td class=\"formcolumncell\">\n" + "            <a name=\"" + postfix + "\">\n" + "              <input type=\"button\" value=\""
+            + Messages.getAttributeString(locale, "SolrConnector.Delete") + "\" alt=\"" + Messages.getAttributeString(locale, "SolrConnector.DeleteZookeeperHost") + Integer.toString(k + 1) + "\" onclick='javascript:deleteZookeeperHost("
+            + Integer.toString(k) + ");'/>\n" + "              <input type=\"hidden\" name=\"" + "op_" + postfix + "\" value=\"Continue\"/>\n" + "              <input type=\"hidden\" name=\"" + "host_" + postfix + "\" value=\"localhost\"/>\n"
+            + "              <input type=\"hidden\" name=\"" + "port_" + postfix + "\" value=\"2181\"/>\n" + "            </a>\n" + "          </td>\n" + "          <td class=\"formcolumncell\">\n" + "            <nobr>localhost</nobr>\n"
+            + "          </td>\n" + "          <td class=\"formcolumncell\">\n" + "            <nobr>2181</nobr>\n" + "          </td>\n" + "        </tr>\n");
         k++;
       }
-      if (k == 0)
-      {
-        out.print(
-"        <tr class=\"formrow\"><td class=\"formmessage\" colspan=\"3\">"+Messages.getBodyString(locale,"SolrConnector.NoZookeeperHostsSpecified")+"</td></tr>\n"
-        );
+      if (k == 0) {
+        out.print("        <tr class=\"formrow\"><td class=\"formmessage\" colspan=\"3\">" + Messages.getBodyString(locale, "SolrConnector.NoZookeeperHostsSpecified") + "</td></tr>\n");
       }
-      out.print(
-"        <tr class=\"formrow\"><td class=\"formseparator\" colspan=\"3\"><hr/></td></tr>\n"+
-"        <tr class=\"formrow\">\n"+
-"          <td class=\"formcolumncell\">\n"+
-"            <a name=\"zookeeper\">\n"+
-"              <input type=\"button\" value=\"" + Messages.getAttributeString(locale,"SolrConnector.Add") + "\" alt=\"" + Messages.getAttributeString(locale,"SolrConnector.AddZookeeperHost") + "\" onclick=\"javascript:addZookeeperHost();\"/>\n"+
-"            </a>\n"+
-"            <input type=\"hidden\" name=\"count_zookeeper\" value=\""+k+"\"/>\n"+
-"            <input type=\"hidden\" name=\"op_zookeeper\" value=\"Continue\"/>\n"+
-"          </td>\n"+
-"          <td class=\"formcolumncell\">\n"+
-"            <nobr><input type=\"text\" size=\"30\" name=\"host_zookeeper\" value=\"\"/></nobr>\n"+
-"          </td>\n"+
-"          <td class=\"formcolumncell\">\n"+
-"            <nobr><input type=\"text\" size=\"5\" name=\"port_zookeeper\" value=\"\"/></nobr>\n"+
-"          </td>\n"+
-"        </tr>\n"+
-"      </table>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.ZnodePath") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"znodepath\" type=\"text\" size=\"16\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(znodePath)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.CollectionName") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"collection\" type=\"text\" size=\"16\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(collection)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.ZookeeperClientTimeout") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"zkclienttimeout\" type=\"text\" size=\"5\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(zkClientTimeout)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.ZookeeperConnectTimeout") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"zkconnecttimeout\" type=\"text\" size=\"5\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(zkConnectTimeout)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"</table>\n"
-      );
-    }
-    else
-    {
+      out.print("        <tr class=\"formrow\"><td class=\"formseparator\" colspan=\"3\"><hr/></td></tr>\n" + "        <tr class=\"formrow\">\n" + "          <td class=\"formcolumncell\">\n" + "            <a name=\"zookeeper\">\n"
+          + "              <input type=\"button\" value=\"" + Messages.getAttributeString(locale, "SolrConnector.Add") + "\" alt=\"" + Messages.getAttributeString(locale, "SolrConnector.AddZookeeperHost")
+          + "\" onclick=\"javascript:addZookeeperHost();\"/>\n" + "            </a>\n" + "            <input type=\"hidden\" name=\"count_zookeeper\" value=\"" + k + "\"/>\n"
+          + "            <input type=\"hidden\" name=\"op_zookeeper\" value=\"Continue\"/>\n" + "          </td>\n" + "          <td class=\"formcolumncell\">\n"
+          + "            <nobr><input type=\"text\" size=\"30\" name=\"host_zookeeper\" value=\"\"/></nobr>\n" + "          </td>\n" + "          <td class=\"formcolumncell\">\n"
+          + "            <nobr><input type=\"text\" size=\"5\" name=\"port_zookeeper\" value=\"\"/></nobr>\n" + "          </td>\n" + "        </tr>\n" + "      </table>\n" + "    </td>\n" + "  </tr>\n"
+          + "  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.ZnodePath") + "</nobr></td>\n" + "    <td class=\"value\">\n"
+          + "      <input name=\"znodepath\" type=\"text\" size=\"16\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(znodePath) + "\"/>\n" + "    </td>\n" + "  </tr>\n"
+          + "  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.CollectionName") + "</nobr></td>\n" + "    <td class=\"value\">\n"
+          + "      <input name=\"collection\" type=\"text\" size=\"16\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(collection) + "\"/>\n" + "    </td>\n" + "  </tr>\n"
+          + "  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.ZookeeperSocketTimeout") + "</nobr></td>\n"
+          + "    <td class=\"value\">\n" + "      <input name=\"zkclienttimeout\" type=\"text\" size=\"5\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(zkSocketTimeout) + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "  <tr>\n"
+          + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.ZookeeperConnectionTimeout") + "</nobr></td>\n" + "    <td class=\"value\">\n"
+          + "      <input name=\"zkconnecttimeout\" type=\"text\" size=\"5\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(zkConnectionTimeout) + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "</table>\n");
+    } else {
       // Hiddens for Zookeeper tab
       int k = 0;
-      for (int i = 0; i < parameters.getChildCount(); i++)
-      {
-        ConfigurationNode cn = parameters.getChild(i);
-        if (cn.getType().equals(SolrConfig.NODE_ZOOKEEPER))
-        {
-          String host = cn.getAttributeValue(SolrConfig.ATTR_HOST);
-          String zkport = cn.getAttributeValue(SolrConfig.ATTR_PORT);
-          String postfix = "zookeeper_"+k;
-          out.print(
-"<input type=\"hidden\" name=\"host_"+postfix+"\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(host)+"\"/>\n"+
-"<input type=\"hidden\" name=\"port_"+postfix+"\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(zkport)+"\"/>\n"
-          );
+      for (int i = 0; i < parameters.getChildCount(); i++) {
+        final ConfigurationNode cn = parameters.getChild(i);
+        if (cn.getType().equals(SolrConfig.NODE_ZOOKEEPER)) {
+          final String host = cn.getAttributeValue(SolrConfig.ATTR_HOST);
+          final String zkport = cn.getAttributeValue(SolrConfig.ATTR_PORT);
+          final String postfix = "zookeeper_" + k;
+          out.print("<input type=\"hidden\" name=\"host_" + postfix + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(host) + "\"/>\n" + "<input type=\"hidden\" name=\"port_" + postfix + "\" value=\""
+              + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(zkport) + "\"/>\n");
           k++;
         }
       }
-      if (parameters.getChildCount() == 0)
-      {
-        String postfix = "zookeeper_"+k;
-        out.print(
-"<input type=\"hidden\" name=\"host_"+postfix+"\" value=\"localhost\"/>\n"+
-"<input type=\"hidden\" name=\"port_"+postfix+"\" value=\"2181\"/>\n"
-        );
+      if (parameters.getChildCount() == 0) {
+        final String postfix = "zookeeper_" + k;
+        out.print("<input type=\"hidden\" name=\"host_" + postfix + "\" value=\"localhost\"/>\n" + "<input type=\"hidden\" name=\"port_" + postfix + "\" value=\"2181\"/>\n");
         k++;
       }
-      out.print(
-"<input type=\"hidden\" name=\"count_zookeeper\" value=\""+k+"\"/>\n"+
-"<input type=\"hidden\" name=\"znodepath\" value=\""+znodePath+"\"/>\n"+
-"<input type=\"hidden\" name=\"collection\" value=\""+collection+"\"/>\n"+
-"<input type=\"hidden\" name=\"zkclienttimeout\" value=\""+zkClientTimeout+"\"/>\n"+
-"<input type=\"hidden\" name=\"zkconnecttimeout\" value=\""+zkConnectTimeout+"\"/>\n"
-      );
+      out.print("<input type=\"hidden\" name=\"count_zookeeper\" value=\"" + k + "\"/>\n" + "<input type=\"hidden\" name=\"znodepath\" value=\"" + znodePath + "\"/>\n" + "<input type=\"hidden\" name=\"collection\" value=\"" + collection + "\"/>\n"
+          + "<input type=\"hidden\" name=\"zkclienttimeout\" value=\"" + zkSocketTimeout + "\"/>\n" + "<input type=\"hidden\" name=\"zkconnecttimeout\" value=\"" + zkConnectionTimeout + "\"/>\n");
     }
-    
+
     // "Paths" tab
-    if (tabName.equals(Messages.getString(locale,"SolrConnector.Paths")))
-    {
-      out.print(
-"<table class=\"displaytable\">\n"+
-"  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.UpdateHandler") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"updatepath\" type=\"text\" size=\"32\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(updatePath)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.RemoveHandler") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"removepath\" type=\"text\" size=\"32\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(removePath)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.StatusHandler") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"statuspath\" type=\"text\" size=\"32\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(statusPath)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"</table>\n"
-      );
-    }
-    else
-    {
+    if (tabName.equals(Messages.getString(locale, "SolrConnector.Paths"))) {
+      out.print("<table class=\"displaytable\">\n" + "  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.UpdateHandler")
+          + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <input name=\"updatepath\" type=\"text\" size=\"32\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(updatePath) + "\"/>\n" + "    </td>\n" + "  </tr>\n"
+          + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.RemoveHandler") + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <input name=\"removepath\" type=\"text\" size=\"32\" value=\""
+          + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(removePath) + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.StatusHandler")
+          + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <input name=\"statuspath\" type=\"text\" size=\"32\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(statusPath) + "\"/>\n" + "    </td>\n" + "  </tr>\n"
+          + "</table>\n");
+    } else {
       // Paths tab hiddens
-      out.print(
-"<input type=\"hidden\" name=\"updatepath\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(updatePath)+"\"/>\n"+
-"<input type=\"hidden\" name=\"removepath\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(removePath)+"\"/>\n"+
-"<input type=\"hidden\" name=\"statuspath\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(statusPath)+"\"/>\n"
-      );
+      out.print("<input type=\"hidden\" name=\"updatepath\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(updatePath) + "\"/>\n" + "<input type=\"hidden\" name=\"removepath\" value=\""
+          + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(removePath) + "\"/>\n" + "<input type=\"hidden\" name=\"statuspath\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(statusPath) + "\"/>\n");
     }
-    
+
     // "Schema" tab
-    if (tabName.equals(Messages.getString(locale,"SolrConnector.Schema")))
-    {
-      out.print(
-"<table class=\"displaytable\">\n"+
-"  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.IDFieldName") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"idfield\" type=\"text\" size=\"32\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(idField)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.OriginalSizeFieldName") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"originalsizefield\" type=\"text\" size=\"32\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(originalSizeField)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.ModifiedDateFieldName") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"modifieddatefield\" type=\"text\" size=\"32\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(modifiedDateField)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.CreatedDateFieldName") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"createddatefield\" type=\"text\" size=\"32\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(createdDateField)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.IndexedDateFieldName") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"indexeddatefield\" type=\"text\" size=\"32\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(indexedDateField)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.FileNameFieldName") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"filenamefield\" type=\"text\" size=\"32\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(fileNameField)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.MimeTypeFieldName") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"mimetypefield\" type=\"text\" size=\"32\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(mimeTypeField)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.UseExtractUpdateHandler") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"extractupdatecheckbox\" type=\"hidden\" value=\"true\"/>\n"+
-"      <input name=\"extractupdatepresent\" type=\"hidden\" value=\"true\"/>\n"
-      );
-      if (!useExtractUpdate.equals("false"))
-      {
-        out.print(
-"      <input name=\"extractupdate\" type=\"checkbox\" value=\"true\" checked=\"true\"/>\n"
-        );
+    if (tabName.equals(Messages.getString(locale, "SolrConnector.Schema"))) {
+      out.print("<table class=\"displaytable\">\n" + "  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.IDFieldName")
+          + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <input name=\"idfield\" type=\"text\" size=\"32\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(idField) + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "  <tr>\n"
+          + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.OriginalSizeFieldName") + "</nobr></td>\n" + "    <td class=\"value\">\n"
+          + "      <input name=\"originalsizefield\" type=\"text\" size=\"32\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(originalSizeField) + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "  <tr>\n"
+          + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.ModifiedDateFieldName") + "</nobr></td>\n" + "    <td class=\"value\">\n"
+          + "      <input name=\"modifieddatefield\" type=\"text\" size=\"32\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(modifiedDateField) + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "  <tr>\n"
+          + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.CreatedDateFieldName") + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <input name=\"createddatefield\" type=\"text\" size=\"32\" value=\""
+          + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(createdDateField) + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.IndexedDateFieldName")
+          + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <input name=\"indexeddatefield\" type=\"text\" size=\"32\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(indexedDateField) + "\"/>\n" + "    </td>\n"
+          + "  </tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.FileNameFieldName") + "</nobr></td>\n" + "    <td class=\"value\">\n"
+          + "      <input name=\"filenamefield\" type=\"text\" size=\"32\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(fileNameField) + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>"
+          + Messages.getBodyString(locale, "SolrConnector.MimeTypeFieldName") + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <input name=\"mimetypefield\" type=\"text\" size=\"32\" value=\""
+          + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(mimeTypeField) + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.UseExtractUpdateHandler")
+          + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <input name=\"extractupdatecheckbox\" type=\"hidden\" value=\"true\"/>\n" + "      <input name=\"extractupdatepresent\" type=\"hidden\" value=\"true\"/>\n");
+      if (!useExtractUpdate.equals("false")) {
+        out.print("      <input name=\"extractupdate\" type=\"checkbox\" value=\"true\" checked=\"true\"/>\n");
+      } else {
+        out.print("      <input name=\"extractupdate\" type=\"checkbox\" value=\"true\"/>\n");
       }
-      else
-      {
-        out.print(
-"      <input name=\"extractupdate\" type=\"checkbox\" value=\"true\"/>\n"
-        );
-      }
-      out.print(
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.ContentFieldName") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"contentfield\" type=\"text\" size=\"32\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(contentField)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"</table>\n"
-      );
+      out.print("    </td>\n" + "  </tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.ContentFieldName") + "</nobr></td>\n" + "    <td class=\"value\">\n"
+          + "      <input name=\"contentfield\" type=\"text\" size=\"32\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(contentField) + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "</table>\n");
+    } else {
+      out.print("<input type=\"hidden\" name=\"idfield\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(idField) + "\"/>\n" + "<input type=\"hidden\" name=\"originalsizefield\" value=\""
+          + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(originalSizeField) + "\"/>\n" + "<input type=\"hidden\" name=\"modifieddatefield\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(modifiedDateField) + "\"/>\n"
+          + "<input type=\"hidden\" name=\"createddatefield\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(createdDateField) + "\"/>\n" + "<input type=\"hidden\" name=\"indexeddatefield\" value=\""
+          + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(indexedDateField) + "\"/>\n" + "<input type=\"hidden\" name=\"filenamefield\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(fileNameField) + "\"/>\n"
+          + "<input type=\"hidden\" name=\"mimetypefield\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(mimeTypeField) + "\"/>\n" + "<input type=\"hidden\" name=\"contentfield\" value=\""
+          + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(contentField) + "\"/>\n" + "<input name=\"extractupdatecheckbox\" type=\"hidden\" value=\"false\"/>\n" + "<input type=\"hidden\" name=\"extractupdate\" value=\""
+          + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(useExtractUpdate) + "\"/>\n" + "<input name=\"extractupdatepresent\" type=\"hidden\" value=\"true\"/>\n");
     }
-    else
-    {
-      out.print(
-"<input type=\"hidden\" name=\"idfield\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(idField)+"\"/>\n"+
-"<input type=\"hidden\" name=\"originalsizefield\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(originalSizeField)+"\"/>\n"+
-"<input type=\"hidden\" name=\"modifieddatefield\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(modifiedDateField)+"\"/>\n"+
-"<input type=\"hidden\" name=\"createddatefield\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(createdDateField)+"\"/>\n"+
-"<input type=\"hidden\" name=\"indexeddatefield\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(indexedDateField)+"\"/>\n"+
-"<input type=\"hidden\" name=\"filenamefield\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(fileNameField)+"\"/>\n"+
-"<input type=\"hidden\" name=\"mimetypefield\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(mimeTypeField)+"\"/>\n"+
-"<input type=\"hidden\" name=\"contentfield\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(contentField)+"\"/>\n"+
-"<input name=\"extractupdatecheckbox\" type=\"hidden\" value=\"false\"/>\n"+
-"<input type=\"hidden\" name=\"extractupdate\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(useExtractUpdate)+"\"/>\n"+
-"<input name=\"extractupdatepresent\" type=\"hidden\" value=\"true\"/>\n"
-      );
-    }
-    
+
     // "Documents" tab
-    if (tabName.equals(Messages.getString(locale,"SolrConnector.Documents")))
-    {
-      out.print(
-"<table class=\"displaytable\">\n"+
-"  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.MaximumDocumentLength") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"maxdocumentlength\" type=\"text\" size=\"16\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(maxLength)+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.IncludedMimeTypes") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <textarea rows=\"10\" cols=\"20\" name=\"includedmimetypes\">"+org.apache.manifoldcf.ui.util.Encoder.bodyEscape(includedMimeTypes)+"</textarea>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.ExcludedMimeTypes") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <textarea rows=\"10\" cols=\"20\" name=\"excludedmimetypes\">"+org.apache.manifoldcf.ui.util.Encoder.bodyEscape(excludedMimeTypes)+"</textarea>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"</table>\n"
-      );
+    if (tabName.equals(Messages.getString(locale, "SolrConnector.Documents"))) {
+      out.print("<table class=\"displaytable\">\n" + "  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.MaximumDocumentLength")
+          + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <input name=\"maxdocumentlength\" type=\"text\" size=\"16\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(maxLength) + "\"/>\n" + "    </td>\n" + "  </tr>\n"
+          + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.IncludedMimeTypes") + "</nobr></td>\n" + "    <td class=\"value\">\n"
+          + "      <textarea rows=\"10\" cols=\"20\" name=\"includedmimetypes\">" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(includedMimeTypes) + "</textarea>\n" + "    </td>\n" + "  </tr>\n" + "  <tr>\n"
+          + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.ExcludedMimeTypes") + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <textarea rows=\"10\" cols=\"20\" name=\"excludedmimetypes\">"
+          + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(excludedMimeTypes) + "</textarea>\n" + "    </td>\n" + "  </tr>\n" + "</table>\n");
+    } else {
+      out.print("<input type=\"hidden\" name=\"maxdocumentlength\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(maxLength) + "\"/>\n" + "<input type=\"hidden\" name=\"includedmimetypes\" value=\""
+          + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(includedMimeTypes) + "\"/>\n" + "<input type=\"hidden\" name=\"excludedmimetypes\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(excludedMimeTypes) + "\"/>\n");
     }
-    else
-    {
-      out.print(
-"<input type=\"hidden\" name=\"maxdocumentlength\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(maxLength)+"\"/>\n"+
-"<input type=\"hidden\" name=\"includedmimetypes\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(includedMimeTypes)+"\"/>\n"+
-"<input type=\"hidden\" name=\"excludedmimetypes\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(excludedMimeTypes)+"\"/>\n"
-      );
-    }
-    
+
     // "Commits" tab
-    if (tabName.equals(Messages.getString(locale,"SolrConnector.Commits")))
-    {
-      out.print(
-"<table class=\"displaytable\">\n"+
-"  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.CommitAtEndOfEveryJob") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"commits_present\" type=\"hidden\" value=\"true\"/>\n"+
-"      <input name=\"commits\" type=\"checkbox\" value=\"true\""+(commits.equals("true")?" checked=\"yes\"":"")+"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.CommitEachDocumentWithin") + "</nobr></td>\n"+
-"    <td class=\"value\">\n"+
-"      <input name=\"commitwithin\" type=\"text\" size=\"16\" value=\""+commitWithin+"\"/>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"</table>\n"
-      );
-    }
-    else
-    {
-      out.print(
-"<input type=\"hidden\" name=\"commits_present\" value=\"true\"/>\n"+
-"<input name=\"commits\" type=\"hidden\" value=\""+commits+"\"/>\n"+
-"<input name=\"commitwithin\" type=\"hidden\" value=\""+commitWithin+"\"/>\n"
-      );
+    if (tabName.equals(Messages.getString(locale, "SolrConnector.Commits"))) {
+      out.print("<table class=\"displaytable\">\n" + "  <tr><td colspan=\"2\" class=\"separator\"><hr/></td></tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.CommitAtEndOfEveryJob")
+          + "</nobr></td>\n" + "    <td class=\"value\">\n" + "      <input name=\"commits_present\" type=\"hidden\" value=\"true\"/>\n" + "      <input name=\"commits\" type=\"checkbox\" value=\"true\""
+          + (commits.equals("true") ? " checked=\"yes\"" : "") + "/>\n" + "    </td>\n" + "  </tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.CommitEachDocumentWithin") + "</nobr></td>\n"
+          + "    <td class=\"value\">\n" + "      <input name=\"commitwithin\" type=\"text\" size=\"16\" value=\"" + commitWithin + "\"/>\n" + "    </td>\n" + "  </tr>\n" + "</table>\n");
+    } else {
+      out.print("<input type=\"hidden\" name=\"commits_present\" value=\"true\"/>\n" + "<input name=\"commits\" type=\"hidden\" value=\"" + commits + "\"/>\n" + "<input name=\"commitwithin\" type=\"hidden\" value=\"" + commitWithin + "\"/>\n");
     }
 
     // Prepare for the argument tab
-    Map argumentMap = new HashMap();
+    final Map argumentMap = new HashMap();
     int i = 0;
-    while (i < parameters.getChildCount())
-    {
-      ConfigNode sn = parameters.getChild(i++);
-      if (sn.getType().equals(SolrConfig.NODE_ARGUMENT))
-      {
-        String name = sn.getAttributeValue(SolrConfig.ATTRIBUTE_NAME);
-        String value = sn.getAttributeValue(SolrConfig.ATTRIBUTE_VALUE);
-        ArrayList values = (ArrayList)argumentMap.get(name);
-        if (values == null)
-        {
+    while (i < parameters.getChildCount()) {
+      final ConfigNode sn = parameters.getChild(i++);
+      if (sn.getType().equals(SolrConfig.NODE_ARGUMENT)) {
+        final String name = sn.getAttributeValue(SolrConfig.ATTRIBUTE_NAME);
+        final String value = sn.getAttributeValue(SolrConfig.ATTRIBUTE_VALUE);
+        ArrayList values = (ArrayList) argumentMap.get(name);
+        if (values == null) {
           values = new ArrayList();
-          argumentMap.put(name,values);
+          argumentMap.put(name, values);
         }
         values.add(value);
       }
     }
-    
+
     // "Arguments" tab
-    if (tabName.equals(Messages.getString(locale,"SolrConnector.Arguments")))
-    {
+    if (tabName.equals(Messages.getString(locale, "SolrConnector.Arguments"))) {
       // For the display, sort the arguments into alphabetic order
-      String[] sortArray = new String[argumentMap.size()];
+      final String[] sortArray = new String[argumentMap.size()];
       i = 0;
-      Iterator iter = argumentMap.keySet().iterator();
-      while (iter.hasNext())
-      {
-        sortArray[i++] = (String)iter.next();
+      final Iterator iter = argumentMap.keySet().iterator();
+      while (iter.hasNext()) {
+        sortArray[i++] = (String) iter.next();
       }
       java.util.Arrays.sort(sortArray);
-      out.print(
-"<table class=\"displaytable\">\n"+
-"  <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"+
-"  <tr>\n"+
-"    <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.Arguments2") + "</nobr></td>\n"+
-"    <td class=\"boxcell\">\n"+
-"      <table class=\"formtable\">\n"+
-"        <tr class=\"formheaderrow\">\n"+
-"          <td class=\"formcolumnheader\"></td>\n"+
-"          <td class=\"formcolumnheader\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.Name") + "</nobr></td>\n"+
-"          <td class=\"formcolumnheader\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.Value") + "</nobr></td>\n"+
-"        </tr>\n"
-      );
+      out.print("<table class=\"displaytable\">\n" + "  <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n" + "  <tr>\n" + "    <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.Arguments2") + "</nobr></td>\n"
+          + "    <td class=\"boxcell\">\n" + "      <table class=\"formtable\">\n" + "        <tr class=\"formheaderrow\">\n" + "          <td class=\"formcolumnheader\"></td>\n" + "          <td class=\"formcolumnheader\"><nobr>"
+          + Messages.getBodyString(locale, "SolrConnector.Name") + "</nobr></td>\n" + "          <td class=\"formcolumnheader\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.Value") + "</nobr></td>\n" + "        </tr>\n");
       i = 0;
       int k = 0;
-      while (k < sortArray.length)
-      {
-        String name = sortArray[k++];
-        ArrayList values = (ArrayList)argumentMap.get(name);
+      while (k < sortArray.length) {
+        final String name = sortArray[k++];
+        final ArrayList values = (ArrayList) argumentMap.get(name);
         int j = 0;
-        while (j < values.size())
-        {
-          String value = (String)values.get(j++);
+        while (j < values.size()) {
+          final String value = (String) values.get(j++);
           // Its prefix will be...
-          String prefix = "argument_" + Integer.toString(i);
-          out.print(
-"        <tr class=\""+(((i % 2)==0)?"evenformrow":"oddformrow")+"\">\n"+
-"          <td class=\"formcolumncell\">\n"+
-"            <a name=\""+prefix+"\"><input type=\"button\" value=\"Delete\" alt=\""+Messages.getAttributeString(locale,"SolrConnector.DeleteArgument")+" "+Integer.toString(i+1)+"\" onclick=\"javascript:deleteArgument("+Integer.toString(i)+");"+"\"/>\n"+
-"              <input type=\"hidden\" name=\""+prefix+"_op"+"\" value=\"Continue\"/>\n"+
-"              <input type=\"hidden\" name=\""+prefix+"_name"+"\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(name)+"\"/>\n"+
-"            </a>\n"+
-"          </td>\n"+
-"          <td class=\"formcolumncell\">\n"+
-"            <nobr>"+org.apache.manifoldcf.ui.util.Encoder.bodyEscape(name)+"</nobr>\n"+
-"          </td>\n"+
-"          <td class=\"formcolumncell\">\n"+
-"            <nobr><input type=\"text\" size=\"30\" name=\""+prefix+"_value"+"\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(value)+"\"</nobr>\n"+
-"          </td>\n"+
-"        </tr>\n"
-          );
+          final String prefix = "argument_" + Integer.toString(i);
+          out.print("        <tr class=\"" + (((i % 2) == 0) ? "evenformrow" : "oddformrow") + "\">\n" + "          <td class=\"formcolumncell\">\n" + "            <a name=\"" + prefix + "\"><input type=\"button\" value=\"Delete\" alt=\""
+              + Messages.getAttributeString(locale, "SolrConnector.DeleteArgument") + " " + Integer.toString(i + 1) + "\" onclick=\"javascript:deleteArgument(" + Integer.toString(i) + ");" + "\"/>\n" + "              <input type=\"hidden\" name=\""
+              + prefix + "_op" + "\" value=\"Continue\"/>\n" + "              <input type=\"hidden\" name=\"" + prefix + "_name" + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(name) + "\"/>\n" + "            </a>\n"
+              + "          </td>\n" + "          <td class=\"formcolumncell\">\n" + "            <nobr>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(name) + "</nobr>\n" + "          </td>\n" + "          <td class=\"formcolumncell\">\n"
+              + "            <nobr><input type=\"text\" size=\"30\" name=\"" + prefix + "_value" + "\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(value) + "\"</nobr>\n" + "          </td>\n" + "        </tr>\n");
           i++;
         }
       }
-      if (i == 0)
-      {
-        out.print(
-"        <tr class=\"formrow\"><td class=\"formmessage\" colspan=\"3\">" + Messages.getBodyString(locale,"SolrConnector.NoArgumentsSpecified") + "</td></tr>\n"
-        );
+      if (i == 0) {
+        out.print("        <tr class=\"formrow\"><td class=\"formmessage\" colspan=\"3\">" + Messages.getBodyString(locale, "SolrConnector.NoArgumentsSpecified") + "</td></tr>\n");
       }
-      out.print(
-"        <tr class=\"formrow\"><td class=\"formseparator\" colspan=\"3\"><hr/></td></tr>\n"+
-"        <tr class=\"formrow\">\n"+
-"          <td class=\"formcolumncell\">\n"+
-"            <a name=\"argument\"><input type=\"button\" value=\"Add\" alt=\"Add argument\" onclick=\"javascript:addArgument();\"/>\n"+
-"              <input type=\"hidden\" name=\"argument_count\" value=\""+Integer.toString(i)+"\"/>\n"+
-"              <input type=\"hidden\" name=\"argument_op\" value=\"Continue\"/>\n"+
-"            </a>\n"+
-"          </td>\n"+
-"          <td class=\"formcolumncell\">\n"+
-"            <nobr><input type=\"text\" size=\"30\" name=\"argument_name\" value=\"\"/></nobr>\n"+
-"          </td>\n"+
-"          <td class=\"formcolumncell\">\n"+
-"            <nobr><input type=\"text\" size=\"30\" name=\"argument_value\" value=\"\"/></nobr>\n"+
-"          </td>\n"+
-"        </tr>\n"+
-"      </table>\n"+
-"    </td>\n"+
-"  </tr>\n"+
-"</table>\n"
-      );
-    }
-    else
-    {
+      out.print("        <tr class=\"formrow\"><td class=\"formseparator\" colspan=\"3\"><hr/></td></tr>\n" + "        <tr class=\"formrow\">\n" + "          <td class=\"formcolumncell\">\n"
+          + "            <a name=\"argument\"><input type=\"button\" value=\"Add\" alt=\"Add argument\" onclick=\"javascript:addArgument();\"/>\n" + "              <input type=\"hidden\" name=\"argument_count\" value=\"" + Integer.toString(i)
+          + "\"/>\n" + "              <input type=\"hidden\" name=\"argument_op\" value=\"Continue\"/>\n" + "            </a>\n" + "          </td>\n" + "          <td class=\"formcolumncell\">\n"
+          + "            <nobr><input type=\"text\" size=\"30\" name=\"argument_name\" value=\"\"/></nobr>\n" + "          </td>\n" + "          <td class=\"formcolumncell\">\n"
+          + "            <nobr><input type=\"text\" size=\"30\" name=\"argument_value\" value=\"\"/></nobr>\n" + "          </td>\n" + "        </tr>\n" + "      </table>\n" + "    </td>\n" + "  </tr>\n" + "</table>\n");
+    } else {
       // Emit hiddens for argument tab
       i = 0;
-      Iterator iter = argumentMap.keySet().iterator();
-      while (iter.hasNext())
-      {
-        String name = (String)iter.next();
-        ArrayList values = (ArrayList)argumentMap.get(name);
+      final Iterator iter = argumentMap.keySet().iterator();
+      while (iter.hasNext()) {
+        final String name = (String) iter.next();
+        final ArrayList values = (ArrayList) argumentMap.get(name);
         int j = 0;
-        while (j < values.size())
-        {
-          String value = (String)values.get(j++);
+        while (j < values.size()) {
+          final String value = (String) values.get(j++);
           // It's prefix will be...
-          String prefix = "argument_" + Integer.toString(i++);
-          out.print(
-"<input type=\"hidden\" name=\""+prefix+"_name\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(name)+"\"/>\n"+
-"<input type=\"hidden\" name=\""+prefix+"_value\" value=\""+org.apache.manifoldcf.ui.util.Encoder.attributeEscape(value)+"\"/>\n"
-          );
+          final String prefix = "argument_" + Integer.toString(i++);
+          out.print("<input type=\"hidden\" name=\"" + prefix + "_name\" value=\"" + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(name) + "\"/>\n" + "<input type=\"hidden\" name=\"" + prefix + "_value\" value=\""
+              + org.apache.manifoldcf.ui.util.Encoder.attributeEscape(value) + "\"/>\n");
         }
       }
-      out.print(
-"<input type=\"hidden\" name=\"argument_count\" value=\""+Integer.toString(i)+"\"/>\n"
-      );
+      out.print("<input type=\"hidden\" name=\"argument_count\" value=\"" + Integer.toString(i) + "\"/>\n");
     }
   }
-  
-  /** Process a configuration post.
-  * This method is called at the start of the connector's configuration page, whenever there is a possibility that form data for a connection has been
-  * posted.  Its purpose is to gather form information and modify the configuration parameters accordingly.
-  * The name of the posted form is "editconnection".
-  *@param threadContext is the local thread context.
-  *@param variableContext is the set of variables available from the post, including binary file post information.
-  *@param parameters are the configuration parameters, as they currently exist, for this connection being configured.
-  *@return null if all is well, or a string error message if there is an error that should prevent saving of the connection (and cause a redirection to an error page).
-  */
+
+  /**
+   * Process a configuration post. This method is called at the start of the connector's configuration page, whenever there is a possibility that form data for a connection has been posted. Its
+   * purpose is to gather form information and modify the configuration parameters accordingly. The name of the posted form is "editconnection".
+   *
+   * @param threadContext
+   *          is the local thread context.
+   * @param variableContext
+   *          is the set of variables available from the post, including binary file post information.
+   * @param parameters
+   *          are the configuration parameters, as they currently exist, for this connection being configured.
+   * @return null if all is well, or a string error message if there is an error that should prevent saving of the connection (and cause a redirection to an error page).
+   */
   @Override
-  public String processConfigurationPost(IThreadContext threadContext, IPostParameters variableContext,
-    Locale locale, ConfigParams parameters)
-    throws ManifoldCFException
-  {
-    String type = variableContext.getParameter("solrtype");
+  public String processConfigurationPost(final IThreadContext threadContext, final IPostParameters variableContext, final Locale locale, final ConfigParams parameters) throws ManifoldCFException {
+    final String type = variableContext.getParameter("solrtype");
     if (type != null)
-      parameters.setParameter(SolrConfig.PARAM_SOLR_TYPE,type);
+      parameters.setParameter(SolrConfig.PARAM_SOLR_TYPE, type);
 
-    String protocol = variableContext.getParameter("serverprotocol");
+    final String protocol = variableContext.getParameter("serverprotocol");
     if (protocol != null)
-      parameters.setParameter(SolrConfig.PARAM_PROTOCOL,protocol);
-		
-    String server = variableContext.getParameter("servername");
+      parameters.setParameter(SolrConfig.PARAM_PROTOCOL, protocol);
+
+    final String server = variableContext.getParameter("servername");
     if (server != null)
-      parameters.setParameter(SolrConfig.PARAM_SERVER,server);
+      parameters.setParameter(SolrConfig.PARAM_SERVER, server);
 
-    String port = variableContext.getParameter("serverport");
+    final String port = variableContext.getParameter("serverport");
     if (port != null)
-      parameters.setParameter(SolrConfig.PARAM_PORT,port);
+      parameters.setParameter(SolrConfig.PARAM_PORT, port);
 
-    String webapp = variableContext.getParameter("webappname");
+    final String webapp = variableContext.getParameter("webappname");
     if (webapp != null)
-      parameters.setParameter(SolrConfig.PARAM_WEBAPPNAME,webapp);
+      parameters.setParameter(SolrConfig.PARAM_WEBAPPNAME, webapp);
 
-    String core = variableContext.getParameter("core");
+    final String core = variableContext.getParameter("core");
     if (core != null)
-      parameters.setParameter(SolrConfig.PARAM_CORE,core);
+      parameters.setParameter(SolrConfig.PARAM_CORE, core);
 
-    String collection = variableContext.getParameter("collection");
+    final String collection = variableContext.getParameter("collection");
     if (collection != null)
-      parameters.setParameter(SolrConfig.PARAM_COLLECTION,collection);
+      parameters.setParameter(SolrConfig.PARAM_COLLECTION, collection);
 
-    String connectionTimeout = variableContext.getParameter("connectiontimeout");
+    final String connectionTimeout = variableContext.getParameter("connectiontimeout");
     if (connectionTimeout != null)
-      parameters.setParameter(SolrConfig.PARAM_CONNECTION_TIMEOUT,connectionTimeout);
-    
-    String socketTimeout = variableContext.getParameter("sockettimeout");
+      parameters.setParameter(SolrConfig.PARAM_CONNECTION_TIMEOUT, connectionTimeout);
+
+    final String socketTimeout = variableContext.getParameter("sockettimeout");
     if (socketTimeout != null)
-      parameters.setParameter(SolrConfig.PARAM_SOCKET_TIMEOUT,socketTimeout);
+      parameters.setParameter(SolrConfig.PARAM_SOCKET_TIMEOUT, socketTimeout);
 
-    String znodePath = variableContext.getParameter("znodepath");
+    final String znodePath = variableContext.getParameter("znodepath");
     if (znodePath != null)
-      parameters.setParameter(SolrConfig.PARAM_ZOOKEEPER_ZNODE_PATH,znodePath);
-    
-    String zkClientTimeout = variableContext.getParameter("zkclienttimeout");
+      parameters.setParameter(SolrConfig.PARAM_ZOOKEEPER_ZNODE_PATH, znodePath);
+
+    final String zkClientTimeout = variableContext.getParameter("zkclienttimeout");
     if (zkClientTimeout != null)
-      parameters.setParameter(SolrConfig.PARAM_ZOOKEEPER_CLIENT_TIMEOUT,zkClientTimeout);
-    
-    String zkConnectTimeout = variableContext.getParameter("zkconnecttimeout");
+      parameters.setParameter(SolrConfig.PARAM_ZOOKEEPER_SOCKET_TIMEOUT, zkClientTimeout);
+
+    final String zkConnectTimeout = variableContext.getParameter("zkconnecttimeout");
     if (zkConnectTimeout != null)
-      parameters.setParameter(SolrConfig.PARAM_ZOOKEEPER_CONNECT_TIMEOUT,zkConnectTimeout);
-    
-    String updatePath = variableContext.getParameter("updatepath");
+      parameters.setParameter(SolrConfig.PARAM_ZOOKEEPER_CONNECTION_TIMEOUT, zkConnectTimeout);
+
+    final String updatePath = variableContext.getParameter("updatepath");
     if (updatePath != null)
-      parameters.setParameter(SolrConfig.PARAM_UPDATEPATH,updatePath);
+      parameters.setParameter(SolrConfig.PARAM_UPDATEPATH, updatePath);
 
-    String removePath = variableContext.getParameter("removepath");
+    final String removePath = variableContext.getParameter("removepath");
     if (removePath != null)
-      parameters.setParameter(SolrConfig.PARAM_REMOVEPATH,removePath);
+      parameters.setParameter(SolrConfig.PARAM_REMOVEPATH, removePath);
 
-    String statusPath = variableContext.getParameter("statuspath");
+    final String statusPath = variableContext.getParameter("statuspath");
     if (statusPath != null)
-      parameters.setParameter(SolrConfig.PARAM_STATUSPATH,statusPath);
+      parameters.setParameter(SolrConfig.PARAM_STATUSPATH, statusPath);
 
-    String idField = variableContext.getParameter("idfield");
+    final String idField = variableContext.getParameter("idfield");
     if (idField != null)
-      parameters.setParameter(SolrConfig.PARAM_IDFIELD,idField);
+      parameters.setParameter(SolrConfig.PARAM_IDFIELD, idField);
 
-    String originalSizeField = variableContext.getParameter("originalsizefield");
+    final String originalSizeField = variableContext.getParameter("originalsizefield");
     if (originalSizeField != null)
-      parameters.setParameter(SolrConfig.PARAM_ORIGINALSIZEFIELD,originalSizeField);
+      parameters.setParameter(SolrConfig.PARAM_ORIGINALSIZEFIELD, originalSizeField);
 
-    String modifiedDateField = variableContext.getParameter("modifieddatefield");
+    final String modifiedDateField = variableContext.getParameter("modifieddatefield");
     if (modifiedDateField != null)
-      parameters.setParameter(SolrConfig.PARAM_MODIFIEDDATEFIELD,modifiedDateField);
+      parameters.setParameter(SolrConfig.PARAM_MODIFIEDDATEFIELD, modifiedDateField);
 
-    String createdDateField = variableContext.getParameter("createddatefield");
+    final String createdDateField = variableContext.getParameter("createddatefield");
     if (createdDateField != null)
-      parameters.setParameter(SolrConfig.PARAM_CREATEDDATEFIELD,createdDateField);
+      parameters.setParameter(SolrConfig.PARAM_CREATEDDATEFIELD, createdDateField);
 
-    String indexedDateField = variableContext.getParameter("indexeddatefield");
+    final String indexedDateField = variableContext.getParameter("indexeddatefield");
     if (indexedDateField != null)
-      parameters.setParameter(SolrConfig.PARAM_INDEXEDDATEFIELD,indexedDateField);
+      parameters.setParameter(SolrConfig.PARAM_INDEXEDDATEFIELD, indexedDateField);
 
-    String fileNameField = variableContext.getParameter("filenamefield");
+    final String fileNameField = variableContext.getParameter("filenamefield");
     if (fileNameField != null)
-      parameters.setParameter(SolrConfig.PARAM_FILENAMEFIELD,fileNameField);
+      parameters.setParameter(SolrConfig.PARAM_FILENAMEFIELD, fileNameField);
 
-    String mimeTypeField = variableContext.getParameter("mimetypefield");
+    final String mimeTypeField = variableContext.getParameter("mimetypefield");
     if (mimeTypeField != null)
-      parameters.setParameter(SolrConfig.PARAM_MIMETYPEFIELD,mimeTypeField);
+      parameters.setParameter(SolrConfig.PARAM_MIMETYPEFIELD, mimeTypeField);
 
-    String contentField = variableContext.getParameter("contentfield");
+    final String contentField = variableContext.getParameter("contentfield");
     if (contentField != null)
-      parameters.setParameter(SolrConfig.PARAM_CONTENTFIELD,contentField);
+      parameters.setParameter(SolrConfig.PARAM_CONTENTFIELD, contentField);
 
-    String extractUpdatePresent = variableContext.getParameter("extractupdatepresent");
-    if (extractUpdatePresent != null)
-    {
+    final String extractUpdatePresent = variableContext.getParameter("extractupdatepresent");
+    if (extractUpdatePresent != null) {
       String extractUpdate = variableContext.getParameter("extractupdate");
       if (extractUpdate == null || extractUpdate.length() == 0)
         extractUpdate = "false";
-      parameters.setParameter(SolrConfig.PARAM_EXTRACTUPDATE,extractUpdate);
+      parameters.setParameter(SolrConfig.PARAM_EXTRACTUPDATE, extractUpdate);
     }
 
-    String realm = variableContext.getParameter("realm");
+    final String realm = variableContext.getParameter("realm");
     if (realm != null)
-      parameters.setParameter(SolrConfig.PARAM_REALM,realm);
+      parameters.setParameter(SolrConfig.PARAM_REALM, realm);
 
-    String userID = variableContext.getParameter("userid");
+    final String userID = variableContext.getParameter("userid");
     if (userID != null)
-      parameters.setParameter(SolrConfig.PARAM_USERID,userID);
-		
-    String password = variableContext.getParameter("password");
+      parameters.setParameter(SolrConfig.PARAM_USERID, userID);
+
+    final String password = variableContext.getParameter("password");
     if (password != null)
-      parameters.setObfuscatedParameter(SolrConfig.PARAM_PASSWORD,variableContext.mapKeyToPassword(password));
-    
-    String maxLength = variableContext.getParameter("maxdocumentlength");
+      parameters.setObfuscatedParameter(SolrConfig.PARAM_PASSWORD, variableContext.mapKeyToPassword(password));
+
+    final String maxLength = variableContext.getParameter("maxdocumentlength");
     if (maxLength != null)
-      parameters.setParameter(SolrConfig.PARAM_MAXLENGTH,maxLength);
-    
-    String includedMimeTypes = variableContext.getParameter("includedmimetypes");
+      parameters.setParameter(SolrConfig.PARAM_MAXLENGTH, maxLength);
+
+    final String includedMimeTypes = variableContext.getParameter("includedmimetypes");
     if (includedMimeTypes != null)
-      parameters.setParameter(SolrConfig.PARAM_INCLUDEDMIMETYPES,includedMimeTypes);
-    
-    String excludedMimeTypes = variableContext.getParameter("excludedmimetypes");
+      parameters.setParameter(SolrConfig.PARAM_INCLUDEDMIMETYPES, includedMimeTypes);
+
+    final String excludedMimeTypes = variableContext.getParameter("excludedmimetypes");
     if (excludedMimeTypes != null)
-      parameters.setParameter(SolrConfig.PARAM_EXCLUDEDMIMETYPES,excludedMimeTypes);
-    
-    String commitsPresent = variableContext.getParameter("commits_present");
-    if (commitsPresent != null)
-    {
+      parameters.setParameter(SolrConfig.PARAM_EXCLUDEDMIMETYPES, excludedMimeTypes);
+
+    final String commitsPresent = variableContext.getParameter("commits_present");
+    if (commitsPresent != null) {
       String commits = variableContext.getParameter("commits");
       if (commits == null)
         commits = "false";
-      parameters.setParameter(SolrConfig.PARAM_COMMITS,commits);
+      parameters.setParameter(SolrConfig.PARAM_COMMITS, commits);
     }
-    
-    String commitWithin = variableContext.getParameter("commitwithin");
+
+    final String commitWithin = variableContext.getParameter("commitwithin");
     if (commitWithin != null)
-      parameters.setParameter(SolrConfig.PARAM_COMMITWITHIN,commitWithin);
-    
+      parameters.setParameter(SolrConfig.PARAM_COMMITWITHIN, commitWithin);
+
     String keystoreValue = variableContext.getParameter("keystoredata");
-    if (keystoreValue != null)
-    {
-      IKeystoreManager mgr = KeystoreManagerFactory.make("",keystoreValue);
-      parameters.setParameter(SolrConfig.PARAM_KEYSTORE,mgr.getString());
+    if (keystoreValue != null) {
+      final IKeystoreManager mgr = KeystoreManagerFactory.make("", keystoreValue);
+      parameters.setParameter(SolrConfig.PARAM_KEYSTORE, mgr.getString());
     }
 
     String x = variableContext.getParameter("count_zookeeper");
-    if (x != null && x.length() > 0)
-    {
+    if (x != null && x.length() > 0) {
       // About to gather the bandwidth nodes, so get rid of the old ones.
       int i = 0;
-      while (i < parameters.getChildCount())
-      {
-        ConfigNode node = parameters.getChild(i);
+      while (i < parameters.getChildCount()) {
+        final ConfigNode node = parameters.getChild(i);
         if (node.getType().equals(SolrConfig.NODE_ZOOKEEPER))
           parameters.removeChild(i);
         else
           i++;
       }
-      int count = Integer.parseInt(x);
+      final int count = Integer.parseInt(x);
       i = 0;
-      while (i < count)
-      {
-        String postfix = "zookeeper_"+Integer.toString(i);
-        String op = variableContext.getParameter("op_"+postfix);
-        if (op == null || !op.equals("Delete"))
-        {
+      while (i < count) {
+        final String postfix = "zookeeper_" + Integer.toString(i);
+        final String op = variableContext.getParameter("op_" + postfix);
+        if (op == null || !op.equals("Delete")) {
           // Gather the host etc.
-          String host = variableContext.getParameter("host_"+postfix);
-          String zkport = variableContext.getParameter("port_"+postfix);
-          ConfigNode node = new ConfigNode(SolrConfig.NODE_ZOOKEEPER);
-          node.setAttribute(SolrConfig.ATTR_HOST,host);
-          node.setAttribute(SolrConfig.ATTR_PORT,zkport);
-          parameters.addChild(parameters.getChildCount(),node);
+          final String host = variableContext.getParameter("host_" + postfix);
+          final String zkport = variableContext.getParameter("port_" + postfix);
+          final ConfigNode node = new ConfigNode(SolrConfig.NODE_ZOOKEEPER);
+          node.setAttribute(SolrConfig.ATTR_HOST, host);
+          node.setAttribute(SolrConfig.ATTR_PORT, zkport);
+          parameters.addChild(parameters.getChildCount(), node);
         }
         i++;
       }
-      String addop = variableContext.getParameter("op_zookeeper");
-      if (addop != null && addop.equals("Add"))
-      {
-        String host = variableContext.getParameter("host_zookeeper");
-        String zkport = variableContext.getParameter("port_zookeeper");
-        ConfigNode node = new ConfigNode(SolrConfig.NODE_ZOOKEEPER);
-        node.setAttribute(SolrConfig.ATTR_HOST,host);
-        node.setAttribute(SolrConfig.ATTR_PORT,zkport);
-        parameters.addChild(parameters.getChildCount(),node);
+      final String addop = variableContext.getParameter("op_zookeeper");
+      if (addop != null && addop.equals("Add")) {
+        final String host = variableContext.getParameter("host_zookeeper");
+        final String zkport = variableContext.getParameter("port_zookeeper");
+        final ConfigNode node = new ConfigNode(SolrConfig.NODE_ZOOKEEPER);
+        node.setAttribute(SolrConfig.ATTR_HOST, host);
+        node.setAttribute(SolrConfig.ATTR_PORT, zkport);
+        parameters.addChild(parameters.getChildCount(), node);
       }
     }
 
     x = variableContext.getParameter("argument_count");
-    if (x != null && x.length() > 0)
-    {
+    if (x != null && x.length() > 0) {
       // About to gather the argument nodes, so get rid of the old ones.
       int i = 0;
-      while (i < parameters.getChildCount())
-      {
-        ConfigNode node = parameters.getChild(i);
+      while (i < parameters.getChildCount()) {
+        final ConfigNode node = parameters.getChild(i);
         if (node.getType().equals(SolrConfig.NODE_ARGUMENT))
           parameters.removeChild(i);
         else
           i++;
       }
-      int count = Integer.parseInt(x);
+      final int count = Integer.parseInt(x);
       i = 0;
-      while (i < count)
-      {
-        String prefix = "argument_"+Integer.toString(i);
-        String op = variableContext.getParameter(prefix+"_op");
-        if (op == null || !op.equals("Delete"))
-        {
+      while (i < count) {
+        final String prefix = "argument_" + Integer.toString(i);
+        final String op = variableContext.getParameter(prefix + "_op");
+        if (op == null || !op.equals("Delete")) {
           // Gather the name and value.
-          String name = variableContext.getParameter(prefix+"_name");
-          String value = variableContext.getParameter(prefix+"_value");
-          ConfigNode node = new ConfigNode(SolrConfig.NODE_ARGUMENT);
-          node.setAttribute(SolrConfig.ATTRIBUTE_NAME,name);
-          node.setAttribute(SolrConfig.ATTRIBUTE_VALUE,value);
-          parameters.addChild(parameters.getChildCount(),node);
+          final String name = variableContext.getParameter(prefix + "_name");
+          final String value = variableContext.getParameter(prefix + "_value");
+          final ConfigNode node = new ConfigNode(SolrConfig.NODE_ARGUMENT);
+          node.setAttribute(SolrConfig.ATTRIBUTE_NAME, name);
+          node.setAttribute(SolrConfig.ATTRIBUTE_VALUE, value);
+          parameters.addChild(parameters.getChildCount(), node);
         }
         i++;
       }
-      String addop = variableContext.getParameter("argument_op");
-      if (addop != null && addop.equals("Add"))
-      {
-        String name = variableContext.getParameter("argument_name");
-        String value = variableContext.getParameter("argument_value");
-        ConfigNode node = new ConfigNode(SolrConfig.NODE_ARGUMENT);
-        node.setAttribute(SolrConfig.ATTRIBUTE_NAME,name);
-        node.setAttribute(SolrConfig.ATTRIBUTE_VALUE,value);
-        parameters.addChild(parameters.getChildCount(),node);
+      final String addop = variableContext.getParameter("argument_op");
+      if (addop != null && addop.equals("Add")) {
+        final String name = variableContext.getParameter("argument_name");
+        final String value = variableContext.getParameter("argument_value");
+        final ConfigNode node = new ConfigNode(SolrConfig.NODE_ARGUMENT);
+        node.setAttribute(SolrConfig.ATTRIBUTE_NAME, name);
+        node.setAttribute(SolrConfig.ATTRIBUTE_VALUE, value);
+        parameters.addChild(parameters.getChildCount(), node);
       }
     }
-    
-    String configOp = variableContext.getParameter("configop");
-    if (configOp != null)
-    {
+
+    final String configOp = variableContext.getParameter("configop");
+    if (configOp != null) {
       IKeystoreManager mgr;
-      if (configOp.equals("Delete"))
-      {
-        String alias = variableContext.getParameter("solrkeystorealias");
+      if (configOp.equals("Delete")) {
+        final String alias = variableContext.getParameter("solrkeystorealias");
         keystoreValue = parameters.getParameter(SolrConfig.PARAM_KEYSTORE);
         if (keystoreValue != null)
-          mgr = KeystoreManagerFactory.make("",keystoreValue);
+          mgr = KeystoreManagerFactory.make("", keystoreValue);
         else
           mgr = KeystoreManagerFactory.make("");
         mgr.remove(alias);
-        parameters.setParameter(SolrConfig.PARAM_KEYSTORE,mgr.getString());
-      }
-      else if (configOp.equals("Add"))
-      {
-        String alias = IDFactory.make(threadContext);
-        byte[] certificateValue = variableContext.getBinaryBytes("solrcertificate");
+        parameters.setParameter(SolrConfig.PARAM_KEYSTORE, mgr.getString());
+      } else if (configOp.equals("Add")) {
+        final String alias = IDFactory.make(threadContext);
+        final byte[] certificateValue = variableContext.getBinaryBytes("solrcertificate");
         keystoreValue = parameters.getParameter(SolrConfig.PARAM_KEYSTORE);
         if (keystoreValue != null)
-          mgr = KeystoreManagerFactory.make("",keystoreValue);
+          mgr = KeystoreManagerFactory.make("", keystoreValue);
         else
           mgr = KeystoreManagerFactory.make("");
-        java.io.InputStream is = new java.io.ByteArrayInputStream(certificateValue);
+        final java.io.InputStream is = new java.io.ByteArrayInputStream(certificateValue);
         String certError = null;
-        try
-        {
-          mgr.importCertificate(alias,is);
-        }
-        catch (Throwable e)
-        {
+        try {
+          mgr.importCertificate(alias, is);
+        } catch (final Throwable e) {
           certError = e.getMessage();
-        }
-        finally
-        {
-          try
-          {
+        } finally {
+          try {
             is.close();
-          }
-          catch (IOException e)
-          {
+          } catch (final IOException e) {
             // Eat this exception
           }
         }
 
-        if (certError != null)
-        {
-          return "Illegal certificate: "+certError;
+        if (certError != null) {
+          return "Illegal certificate: " + certError;
         }
-        parameters.setParameter(SolrConfig.PARAM_KEYSTORE,mgr.getString());
+        parameters.setParameter(SolrConfig.PARAM_KEYSTORE, mgr.getString());
       }
     }
 
     return null;
   }
-  
-  /** View configuration.
-  * This method is called in the body section of the connector's view configuration page.  Its purpose is to present the connection information to the user.
-  * The coder can presume that the HTML that is output from this configuration will be within appropriate &lt;html&gt; and &lt;body&gt;tags.
-  *@param threadContext is the local thread context.
-  *@param out is the output to which any HTML should be sent.
-  *@param parameters are the configuration parameters, as they currently exist, for this connection being configured.
-  */
+
+  /**
+   * View configuration. This method is called in the body section of the connector's view configuration page. Its purpose is to present the connection information to the user. The coder can presume
+   * that the HTML that is output from this configuration will be within appropriate &lt;html&gt; and &lt;body&gt;tags.
+   *
+   * @param threadContext
+   *          is the local thread context.
+   * @param out
+   *          is the output to which any HTML should be sent.
+   * @param parameters
+   *          are the configuration parameters, as they currently exist, for this connection being configured.
+   */
   @Override
-  public void viewConfiguration(IThreadContext threadContext, IHTTPOutput out,
-    Locale locale, ConfigParams parameters)
-    throws ManifoldCFException, IOException
-  {
-    out.print(
-"<table class=\"displaytable\">\n"+
-"  <tr>\n"+
-"    <td class=\"description\" colspan=\"1\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.Parameters") + "</nobr></td>\n"+
-"    <td class=\"value\" colspan=\"3\">\n"
-    );
-    Iterator iter = parameters.listParameters();
-    while (iter.hasNext())
-    {
-      String param = (String)iter.next();
-      String value = parameters.getParameter(param);
-      if (param.length() >= "password".length() && param.substring(param.length()-"password".length()).equalsIgnoreCase("password"))
-      {
-        out.print(
-"      <nobr>"+org.apache.manifoldcf.ui.util.Encoder.bodyEscape(param)+"=********</nobr><br/>\n"
-        );
-      }
-      else if (param.length() >="keystore".length() && param.substring(param.length()-"keystore".length()).equalsIgnoreCase("keystore"))
-      {
-        IKeystoreManager kmanager = KeystoreManagerFactory.make("",value);
-        out.print(
-"      <nobr>"+org.apache.manifoldcf.ui.util.Encoder.bodyEscape(param)+"=&lt;"+Integer.toString(kmanager.getContents().length)+" certificate(s)&gt;</nobr><br/>\n"
-        );
-      }
-      else
-      {
-        out.print(
-"      <nobr>"+org.apache.manifoldcf.ui.util.Encoder.bodyEscape(param)+"="+org.apache.manifoldcf.ui.util.Encoder.bodyEscape(value)+"</nobr><br/>\n"
-        );
+  public void viewConfiguration(final IThreadContext threadContext, final IHTTPOutput out, final Locale locale, final ConfigParams parameters) throws ManifoldCFException, IOException {
+    out.print("<table class=\"displaytable\">\n" + "  <tr>\n" + "    <td class=\"description\" colspan=\"1\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.Parameters") + "</nobr></td>\n" + "    <td class=\"value\" colspan=\"3\">\n");
+    final Iterator iter = parameters.listParameters();
+    while (iter.hasNext()) {
+      final String param = (String) iter.next();
+      final String value = parameters.getParameter(param);
+      if (param.length() >= "password".length() && param.substring(param.length() - "password".length()).equalsIgnoreCase("password")) {
+        out.print("      <nobr>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(param) + "=********</nobr><br/>\n");
+      } else if (param.length() >= "keystore".length() && param.substring(param.length() - "keystore".length()).equalsIgnoreCase("keystore")) {
+        final IKeystoreManager kmanager = KeystoreManagerFactory.make("", value);
+        out.print("      <nobr>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(param) + "=&lt;" + Integer.toString(kmanager.getContents().length) + " certificate(s)&gt;</nobr><br/>\n");
+      } else {
+        out.print("      <nobr>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(param) + "=" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(value) + "</nobr><br/>\n");
       }
     }
-    
-    out.print(
-"    </td>\n"+
-"  </tr>\n"
-    );
-    
-    out.print(
-"\n"
-    );
-    
-    out.print(
-"  <tr>\n"+
-"    <td class=\"description\" colspan=\"1\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.ZookeeperHosts") + "</nobr></td>\n"+
-"    <td class=\"boxcell\" colspan=\"3\">\n"+
-"      <table class=\"formtable\">\n"+
-"        <tr class=\"formheaderrow\">\n"+
-"          <td class=\"formcolumnheader\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.Host") + "</nobr></td>\n"+
-"          <td class=\"formcolumnheader\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.Port") + "</nobr></td>\n"+
-"        </tr>\n"
-    );
+
+    out.print("    </td>\n" + "  </tr>\n");
+
+    out.print("\n");
+
+    out.print("  <tr>\n" + "    <td class=\"description\" colspan=\"1\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.ZookeeperHosts") + "</nobr></td>\n" + "    <td class=\"boxcell\" colspan=\"3\">\n" + "      <table class=\"formtable\">\n"
+        + "        <tr class=\"formheaderrow\">\n" + "          <td class=\"formcolumnheader\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.Host") + "</nobr></td>\n" + "          <td class=\"formcolumnheader\"><nobr>"
+        + Messages.getBodyString(locale, "SolrConnector.Port") + "</nobr></td>\n" + "        </tr>\n");
 
     int instanceNumber = 0;
-    for (int i = 0; i < parameters.getChildCount(); i++)
-    {
-      ConfigNode cn = parameters.getChild(i);
-      if (cn.getType().equals(SolrConfig.NODE_ZOOKEEPER))
-      {
-        // An argument node!  Look for all its parameters.
-        String host = cn.getAttributeValue(SolrConfig.ATTR_HOST);
-        String zkport = cn.getAttributeValue(SolrConfig.ATTR_PORT);
+    for (int i = 0; i < parameters.getChildCount(); i++) {
+      final ConfigNode cn = parameters.getChild(i);
+      if (cn.getType().equals(SolrConfig.NODE_ZOOKEEPER)) {
+        // An argument node! Look for all its parameters.
+        final String host = cn.getAttributeValue(SolrConfig.ATTR_HOST);
+        final String zkport = cn.getAttributeValue(SolrConfig.ATTR_PORT);
 
-        out.print(
-"        <tr class=\""+(((instanceNumber % 2)==0)?"evenformrow":"oddformrow")+"\">\n"+
-"          <td class=\"formcolumncell\"><nobr>"+org.apache.manifoldcf.ui.util.Encoder.bodyEscape(host)+"</nobr></td>\n"+
-"          <td class=\"formcolumncell\"><nobr>"+org.apache.manifoldcf.ui.util.Encoder.bodyEscape(zkport)+"</nobr></td>\n"+
-"        </tr>\n"
-        );
-        
+        out.print("        <tr class=\"" + (((instanceNumber % 2) == 0) ? "evenformrow" : "oddformrow") + "\">\n" + "          <td class=\"formcolumncell\"><nobr>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(host) + "</nobr></td>\n"
+            + "          <td class=\"formcolumncell\"><nobr>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(zkport) + "</nobr></td>\n" + "        </tr>\n");
+
         instanceNumber++;
       }
     }
-    if (instanceNumber == 0)
-    {
-      out.print(
-"        <tr class=\"formrow\"><td class=\"formmessage\" colspan=\"5\">" + Messages.getBodyString(locale,"SolrConnector.NoZookeeperHostsSpecified") + "</td></tr>\n"
-      );
+    if (instanceNumber == 0) {
+      out.print("        <tr class=\"formrow\"><td class=\"formmessage\" colspan=\"5\">" + Messages.getBodyString(locale, "SolrConnector.NoZookeeperHostsSpecified") + "</td></tr>\n");
     }
 
-    out.print(
-"      </table>\n"+
-"    </td>\n"+
-"  </tr>\n"
-    );
+    out.print("      </table>\n" + "    </td>\n" + "  </tr>\n");
 
-    out.print(
-"\n"
-    );
+    out.print("\n");
 
-    out.print(
-"  <tr>\n"+
-"    <td class=\"description\" colspan=\"1\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.Arguments3") + "</nobr></td>\n"+
-"    <td class=\"boxcell\" colspan=\"3\">\n"+
-"      <table class=\"formtable\">\n"+
-"        <tr class=\"formheaderrow\">\n"+
-"          <td class=\"formcolumnheader\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.Name") + "</nobr></td>\n"+
-"          <td class=\"formcolumnheader\"><nobr>" + Messages.getBodyString(locale,"SolrConnector.Value") + "</nobr></td>\n"+
-"        </tr>\n"
-    );
-    
+    out.print("  <tr>\n" + "    <td class=\"description\" colspan=\"1\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.Arguments3") + "</nobr></td>\n" + "    <td class=\"boxcell\" colspan=\"3\">\n" + "      <table class=\"formtable\">\n"
+        + "        <tr class=\"formheaderrow\">\n" + "          <td class=\"formcolumnheader\"><nobr>" + Messages.getBodyString(locale, "SolrConnector.Name") + "</nobr></td>\n" + "          <td class=\"formcolumnheader\"><nobr>"
+        + Messages.getBodyString(locale, "SolrConnector.Value") + "</nobr></td>\n" + "        </tr>\n");
+
     instanceNumber = 0;
-    for (int i = 0; i < parameters.getChildCount(); i++)
-    {
-      ConfigNode cn = parameters.getChild(i);
-      if (cn.getType().equals(SolrConfig.NODE_ARGUMENT))
-      {
-        // An argument node!  Look for all its parameters.
-        String name = cn.getAttributeValue(SolrConfig.ATTRIBUTE_NAME);
-        String value = cn.getAttributeValue(SolrConfig.ATTRIBUTE_VALUE);
+    for (int i = 0; i < parameters.getChildCount(); i++) {
+      final ConfigNode cn = parameters.getChild(i);
+      if (cn.getType().equals(SolrConfig.NODE_ARGUMENT)) {
+        // An argument node! Look for all its parameters.
+        final String name = cn.getAttributeValue(SolrConfig.ATTRIBUTE_NAME);
+        final String value = cn.getAttributeValue(SolrConfig.ATTRIBUTE_VALUE);
 
-        out.print(
-"        <tr class=\""+(((instanceNumber % 2)==0)?"evenformrow":"oddformrow")+"\">\n"+
-"          <td class=\"formcolumncell\"><nobr>"+org.apache.manifoldcf.ui.util.Encoder.bodyEscape(name)+"</nobr></td>\n"+
-"          <td class=\"formcolumncell\"><nobr>"+org.apache.manifoldcf.ui.util.Encoder.bodyEscape(value)+"</nobr></td>\n"+
-"        </tr>\n"
-        );
-        
+        out.print("        <tr class=\"" + (((instanceNumber % 2) == 0) ? "evenformrow" : "oddformrow") + "\">\n" + "          <td class=\"formcolumncell\"><nobr>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(name) + "</nobr></td>\n"
+            + "          <td class=\"formcolumncell\"><nobr>" + org.apache.manifoldcf.ui.util.Encoder.bodyEscape(value) + "</nobr></td>\n" + "        </tr>\n");
+
         instanceNumber++;
       }
     }
-    if (instanceNumber == 0)
-    {
-      out.print(
-"        <tr class=\"formrow\"><td class=\"formmessage\" colspan=\"5\">" + Messages.getBodyString(locale,"SolrConnector.NoArguments") + "</td></tr>\n"
-      );
+    if (instanceNumber == 0) {
+      out.print("        <tr class=\"formrow\"><td class=\"formmessage\" colspan=\"5\">" + Messages.getBodyString(locale, "SolrConnector.NoArguments") + "</td></tr>\n");
     }
-    
-    out.print(
-"      </table>\n"+
-"    </td>\n"+
-"  </tr>\n"
-    );
-    
-    out.print(
-"</table>\n"
-    );
+
+    out.print("      </table>\n" + "    </td>\n" + "  </tr>\n");
+
+    out.print("</table>\n");
   }
-  
-  /** This class handles Solr connector version string packing/unpacking/interpretation.
-  */
+
+  /**
+   * This class handles Solr connector version string packing/unpacking/interpretation.
+   */
   protected class SpecPacker {
-    
+
     /** Arguments, from configuration */
-    private final Map<String,List<String>> args = new HashMap<String,List<String>>();
-    
-    public SpecPacker(Specification spec) {
+    private final Map<String, List<String>> args = new HashMap<>();
+
+    public SpecPacker(final Specification spec) {
 
       // Process arguments
-      for (int i = 0; i < params.getChildCount(); i++)
-      {
-        ConfigNode node = params.getChild(i);
-        if (node.getType().equals(SolrConfig.NODE_ARGUMENT))
-        {
-          String attrName = node.getAttributeValue(SolrConfig.ATTRIBUTE_NAME);
+      for (int i = 0; i < params.getChildCount(); i++) {
+        final ConfigNode node = params.getChild(i);
+        if (node.getType().equals(SolrConfig.NODE_ARGUMENT)) {
+          final String attrName = node.getAttributeValue(SolrConfig.ATTRIBUTE_NAME);
           List<String> list = args.get(attrName);
-          if (list == null)
-          {
-            list = new ArrayList<String>();
-            args.put(attrName,list);
+          if (list == null) {
+            list = new ArrayList<>();
+            args.put(attrName, list);
           }
           list.add(node.getAttributeValue(SolrConfig.ATTRIBUTE_VALUE));
         }
       }
-    
+
     }
-    
+
     public String toPackedString() {
-      StringBuilder sb = new StringBuilder();
-      String[] sortArray = new String[args.size()];
-      Iterator<String> iter = args.keySet().iterator();
+      final StringBuilder sb = new StringBuilder();
+      final String[] sortArray = new String[args.size()];
+      final Iterator<String> iter = args.keySet().iterator();
       int i = 0;
-      while (iter.hasNext())
-      {
+      while (iter.hasNext()) {
         sortArray[i++] = iter.next();
       }
-      
+
       // Always use sorted order, because we need this to be comparable.
       java.util.Arrays.sort(sortArray);
-      
-      String[] fixedList = new String[2];
-      List<String> nameValues = new ArrayList<String>();
-      for (int k = 0; k < sortArray.length; k++)
-      {
-        String name = sortArray[k];
-        List<String> values = args.get(name);
+
+      final String[] fixedList = new String[2];
+      final List<String> nameValues = new ArrayList<>();
+      for (int k = 0; k < sortArray.length; k++) {
+        final String name = sortArray[k];
+        final List<String> values = args.get(name);
         java.util.Collections.sort(values);
-        for (String value : values)
-        {
+        for (final String value : values) {
           fixedList[0] = name;
           fixedList[1] = value;
-          StringBuilder pairBuffer = new StringBuilder();
-          packFixedList(pairBuffer,fixedList,'=');
+          final StringBuilder pairBuffer = new StringBuilder();
+          packFixedList(pairBuffer, fixedList, '=');
           nameValues.add(pairBuffer.toString());
         }
       }
-      
-      packList(sb,nameValues,'+');
-      
-      // Here, append things which we have no intention of unpacking.  This includes stuff that comes from
+
+      packList(sb, nameValues, '+');
+
+      // Here, append things which we have no intention of unpacking. This includes stuff that comes from
       // the configuration information, for instance.
 
-      if (idAttributeName != null)
-      {
-          sb.append('+');
-          pack(sb,idAttributeName,'+');
-      }
-      else
+      if (idAttributeName != null) {
+        sb.append('+');
+        pack(sb, idAttributeName, '+');
+      } else
         sb.append('-');
 
-      if (originalSizeAttributeName != null)
-      {
-          sb.append('+');
-          pack(sb,originalSizeAttributeName,'+');
-      }
-      else
+      if (originalSizeAttributeName != null) {
+        sb.append('+');
+        pack(sb, originalSizeAttributeName, '+');
+      } else
         sb.append('-');
 
-      if (modifiedDateAttributeName != null)
-      {
-          sb.append('+');
-          pack(sb,modifiedDateAttributeName,'+');
-      }
-      else
-        sb.append('-');
-      
-      if (createdDateAttributeName != null)
-      {
-          sb.append('+');
-          pack(sb,createdDateAttributeName,'+');
-      }
-      else
+      if (modifiedDateAttributeName != null) {
+        sb.append('+');
+        pack(sb, modifiedDateAttributeName, '+');
+      } else
         sb.append('-');
 
-      if (indexedDateAttributeName != null)
-      {
-          sb.append('+');
-          pack(sb,indexedDateAttributeName,'+');
-      }
-      else
+      if (createdDateAttributeName != null) {
+        sb.append('+');
+        pack(sb, createdDateAttributeName, '+');
+      } else
         sb.append('-');
 
-      if (fileNameAttributeName != null)
-      {
-          sb.append('+');
-          pack(sb,fileNameAttributeName,'+');
-      }
-      else
+      if (indexedDateAttributeName != null) {
+        sb.append('+');
+        pack(sb, indexedDateAttributeName, '+');
+      } else
         sb.append('-');
 
-      if (mimeTypeAttributeName != null)
-      {
-          sb.append('+');
-          pack(sb,mimeTypeAttributeName,'+');
-      }
-      else
+      if (fileNameAttributeName != null) {
+        sb.append('+');
+        pack(sb, fileNameAttributeName, '+');
+      } else
         sb.append('-');
 
-      if (contentAttributeName != null)
-      {
-          sb.append('+');
-          pack(sb,contentAttributeName,'+');
-      }
-      else
+      if (mimeTypeAttributeName != null) {
+        sb.append('+');
+        pack(sb, mimeTypeAttributeName, '+');
+      } else
+        sb.append('-');
+
+      if (contentAttributeName != null) {
+        sb.append('+');
+        pack(sb, contentAttributeName, '+');
+      } else
         sb.append('-');
 
       if (useExtractUpdateHandler)
@@ -2332,46 +1573,38 @@
       else
         sb.append('-');
 
-      // Length limitation.  We pack this because when it is changed we want to be sure we get any previously excluded documents.
-      if (maxDocumentLength != null)
-      {
+      // Length limitation. We pack this because when it is changed we want to be sure we get any previously excluded documents.
+      if (maxDocumentLength != null) {
         sb.append('+');
-        pack(sb,maxDocumentLength.toString(),'+');
-      }
-      else
+        pack(sb, maxDocumentLength.toString(), '+');
+      } else
         sb.append('-');
       // Included mime types
-      if (includedMimeTypesString != null)
-      {
+      if (includedMimeTypesString != null) {
         sb.append('+');
-        pack(sb,includedMimeTypesString,'+');
-      }
-      else
+        pack(sb, includedMimeTypesString, '+');
+      } else
         sb.append('-');
       // Excluded mime types
-      if (excludedMimeTypesString != null)
-      {
+      if (excludedMimeTypesString != null) {
         sb.append('+');
-        pack(sb,excludedMimeTypesString,'+');
-      }
-      else
+        pack(sb, excludedMimeTypesString, '+');
+      } else
         sb.append('-');
 
-      if (collectionName != null)
-      {
+      if (collectionName != null) {
         sb.append('+');
-        pack(sb,collectionName,'+');
-      }
-      else
+        pack(sb, collectionName, '+');
+      } else
         sb.append('-');
-      
+
       return sb.toString();
     }
-    
-    public Map<String,List<String>> getArgs() {
+
+    public Map<String, List<String>> getArgs() {
       return args;
     }
-    
+
   }
 
 }
diff --git a/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_en_US.properties b/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_en_US.properties
index d1e1bfb..04c244d 100644
--- a/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_en_US.properties
+++ b/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_en_US.properties
@@ -38,8 +38,8 @@
 SolrConnector.CollectionName=Collection name:
 SolrConnector.ConnectionTimeout=Connection timeout (seconds):
 SolrConnector.SocketTimeout=Socket timeout (seconds):
-SolrConnector.ZookeeperClientTimeout=ZooKeeper client timeout (seconds):
-SolrConnector.ZookeeperConnectTimeout=ZooKeeper connect timeout (seconds):
+SolrConnector.ZookeeperSocketTimeout=Socket timeout (seconds):
+SolrConnector.ZookeeperConnectionTimeout=Connection timeout (seconds):
 SolrConnector.UpdateHandler=Update handler:
 SolrConnector.RemoveHandler=Remove handler:
 SolrConnector.StatusHandler=Status handler:
diff --git a/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_es_ES.properties b/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_es_ES.properties
index ba4e651..3774ce0 100644
--- a/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_es_ES.properties
+++ b/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_es_ES.properties
@@ -38,8 +38,8 @@
 SolrConnector.CollectionName=nombre de la colección:
 SolrConnector.ConnectionTimeout=El tiempo de conexión expiro (segundos):
 SolrConnector.SocketTimeout=Socket timeout (segundos):
-SolrConnector.ZookeeperClientTimeout=Tiempo de espera del cliente ZooKeeper (segundos):
-SolrConnector.ZookeeperConnectTimeout=ZooKeeper tiempo de espera de conexión (segundos):
+SolrConnector.ZookeeperSocketTimeout=Socket timeout (segundos):
+SolrConnector.ZookeeperConnectionTimeout=El tiempo de conexión expiro (segundos):
 SolrConnector.UpdateHandler=actualización de controlador:
 SolrConnector.RemoveHandler=Retire manejador:
 SolrConnector.StatusHandler=manejador de Estado:
diff --git a/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_fr_FR.properties b/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_fr_FR.properties
index 2b1afa8..09cf704 100644
--- a/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_fr_FR.properties
+++ b/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_fr_FR.properties
@@ -37,8 +37,8 @@
 SolrConnector.CollectionName=Nom de Collection:
 SolrConnector.ConnectionTimeout=Timeout de la connexion (secondes):
 SolrConnector.SocketTimeout=Timoute de la Socket (secondes):
-SolrConnector.ZookeeperClientTimeout=Timeout du client ZooKeeper (secondes):
-SolrConnector.ZookeeperConnectTimeout=Timetout du ZooKeeper connect (secondes):
+SolrConnector.ZookeeperSocketTimeout=Timeout de socket (secondes):
+SolrConnector.ZookeeperConnectionTimeout=Timeout de connection (secondes):
 SolrConnector.UpdateHandler=Update handler:
 SolrConnector.RemoveHandler=Remove handler:
 SolrConnector.StatusHandler=Status handler:
diff --git a/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_ja_JP.properties b/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_ja_JP.properties
index b417dc7..162d14a 100644
--- a/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_ja_JP.properties
+++ b/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_ja_JP.properties
@@ -38,8 +38,8 @@
 SolrConnector.CollectionName=コレクション名:
 SolrConnector.ConnectionTimeout=コネクションタイムアウト(秒):
 SolrConnector.SocketTimeout=ソケットタイムアウト(秒):
-SolrConnector.ZookeeperClientTimeout=ZooKeeperクライアントタイムアウト(秒):
-SolrConnector.ZookeeperConnectTimeout=ZooKeeper接続タイムアウト(秒):
+SolrConnector.ZookeeperSocketTimeout=ソケットタイムアウト(秒):
+SolrConnector.ZookeeperConnectionTimeout=コネクションタイムアウト(秒):
 SolrConnector.UpdateHandler=更新ハンドラー:
 SolrConnector.RemoveHandler=除外ハンドラー:
 SolrConnector.StatusHandler=状態ハンドラー:
diff --git a/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_zh_CN.properties b/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_zh_CN.properties
index 906eb70..7d12ccc 100644
--- a/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_zh_CN.properties
+++ b/connectors/solr/connector/src/main/native2ascii/org/apache/manifoldcf/agents/output/solr/common_zh_CN.properties
@@ -38,8 +38,8 @@
 SolrConnector.CollectionName=Collection名:
 SolrConnector.ConnectionTimeout=连接超时(秒):
 SolrConnector.SocketTimeout=套接字超时(秒):
-SolrConnector.ZookeeperClientTimeout=ZooKeeper客户端超时(秒):
-SolrConnector.ZookeeperConnectTimeout=ZooKeeper连接超时(秒):
+SolrConnector.ZookeeperSocketTimeout=套接字超时(秒):
+SolrConnector.ZookeeperConnectionTimeout=连接超时(秒):
 SolrConnector.UpdateHandler=更新处理器: 
 SolrConnector.RemoveHandler=排除处理器: 
 SolrConnector.StatusHandler=状态处理器: 
diff --git a/connectors/solr/connector/src/test/java/org/apache/manifoldcf/agents/output/solr/tests/PreemptiveBasicAuthInterceptorTest.java b/connectors/solr/connector/src/test/java/org/apache/manifoldcf/agents/output/solr/tests/PreemptiveBasicAuthInterceptorTest.java
deleted file mode 100644
index cd95f2c..0000000
--- a/connectors/solr/connector/src/test/java/org/apache/manifoldcf/agents/output/solr/tests/PreemptiveBasicAuthInterceptorTest.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/* $Id$ */

-

-/**

- * Licensed to the Apache Software Foundation (ASF) under one or more

- * contributor license agreements. See the NOTICE file distributed with

- * this work for additional information regarding copyright ownership.

- * The ASF licenses this file to You 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

- * <p>

- * http://www.apache.org/licenses/LICENSE-2.0

- * <p>

- * 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.apache.manifoldcf.agents.output.solr.tests;

-

-import org.apache.http.HttpException;

-import org.apache.http.HttpRequest;

-import org.apache.http.HttpRequestInterceptor;

-import org.apache.http.auth.AuthScope;

-import org.apache.http.auth.AuthState;

-import org.apache.http.auth.Credentials;

-import org.apache.http.auth.UsernamePasswordCredentials;

-import org.apache.http.client.CredentialsProvider;

-import org.apache.http.client.protocol.HttpClientContext;

-import org.apache.http.impl.auth.BasicScheme;

-import org.apache.http.message.BasicHttpRequest;

-import org.apache.http.protocol.HttpContext;

-import org.apache.manifoldcf.agents.output.solr.PreemptiveBasicAuthInterceptor;

-import org.junit.Test;

-

-import java.io.IOException;

-import java.util.HashMap;

-import java.util.Map;

-

-import static org.junit.Assert.assertEquals;

-import static org.junit.Assert.assertTrue;

-import static org.junit.Assert.fail;

-

-public class PreemptiveBasicAuthInterceptorTest {

-

-  @Test

-  public void shouldAddBasicAuthenticationToRequestIfNotAlreadySet() throws Exception {

-    final HttpRequestInterceptor interceptor = new PreemptiveBasicAuthInterceptor(AuthScope.ANY);

-    final HttpContext context = contextWithoutBasicAuth(new UsernamePasswordCredentials("user", "secret"));

-    interceptor.process(get(), context);

-    final AuthState authState = (AuthState) context.getAttribute(HttpClientContext.TARGET_AUTH_STATE);

-    assertTrue(authState.getAuthScheme() instanceof BasicScheme);

-    assertEquals("user", authState.getCredentials().getUserPrincipal().getName());

-    assertEquals("secret", authState.getCredentials().getPassword());

-  }

-

-  @Test

-  public void shouldThrowHttpExceptionIfNoCredentialsWereProvided() {

-    final HttpRequestInterceptor interceptor = new PreemptiveBasicAuthInterceptor(AuthScope.ANY);

-    final HttpContext context = contextWithoutBasicAuth(null);

-    try {

-      interceptor.process(get(), context);

-      fail("Expected an HttpException, but none was raised.");

-    } catch (HttpException e) {

-      assertEquals("Missing credentials for preemptive basic authentication.", e.getMessage());

-    } catch (IOException e) {

-      fail("Expected an HttpException, but an IOException was raised instead.");

-    }

-  }

-

-  private HttpRequest get() {

-    return new BasicHttpRequest("GET", "https://manifoldcf.apache.org/");

-  }

-

-  private HttpContext contextWithoutBasicAuth(final Credentials credentials) {

-    final CredentialsProvider credentialsProvider = new FakeCredentialsProvider();

-    credentialsProvider.setCredentials(AuthScope.ANY, credentials);

-    final AuthState authState = new AuthState();

-    final HttpContext context = new FakeHttpContext();

-    context.setAttribute(HttpClientContext.CREDS_PROVIDER, credentialsProvider);

-    context.setAttribute(HttpClientContext.TARGET_AUTH_STATE, authState);

-    return context;

-  }

-

-  static class FakeHttpContext implements HttpContext {

-

-    private final Map<String, Object> context = new HashMap<>();

-

-    @Override

-    public Object getAttribute(final String id) {

-      return context.get(id);

-    }

-

-    @Override

-    public void setAttribute(final String id, final Object obj) {

-      context.put(id, obj);

-    }

-

-    @Override

-    public Object removeAttribute(final String id) {

-      return context.remove(id);

-    }

-  }

-

-  static class FakeCredentialsProvider implements CredentialsProvider {

-

-    private final Map<AuthScope, Credentials> credentialsByAuthScope = new HashMap<>();

-

-    @Override

-    public void setCredentials(final AuthScope authScope, final Credentials credentials) {

-      credentialsByAuthScope.put(authScope, credentials);

-    }

-

-    @Override

-    public Credentials getCredentials(final AuthScope authScope) {

-      return credentialsByAuthScope.get(authScope);

-    }

-

-    @Override

-    public void clear() {

-      credentialsByAuthScope.clear();

-    }

-  }

-}

diff --git a/connectors/solr/pom.xml b/connectors/solr/pom.xml
index 21c3e47..8cc395d 100644
--- a/connectors/solr/pom.xml
+++ b/connectors/solr/pom.xml
@@ -207,37 +207,21 @@
       <groupId>org.apache.solr</groupId>
       <artifactId>solr-solrj</artifactId>
       <version>${solr.version}</version>
-      <exclusions>
-        <!--  Solrj pulls in an older httpclient -->
-        <exclusion>
-          <artifactId>httpclient</artifactId>
-          <groupId>org.apache.httpcomponents</groupId>
-        </exclusion>
-        <exclusion>
-          <artifactId>httpcore</artifactId>
-          <groupId>org.apache.httpcomponents</groupId>
-        </exclusion>
-        <exclusion>
-          <artifactId>httpmime</artifactId>
-          <groupId>org.apache.httpcomponents</groupId>
-        </exclusion>
-      </exclusions>
     </dependency>
     <dependency>
-      <groupId>org.noggit</groupId>
-      <artifactId>noggit</artifactId>
-      <version>${noggit.version}</version>
+      <groupId>org.apache.solr</groupId>
+      <artifactId>solr-solrj-zookeeper</artifactId>
+      <version>${solr.version}</version>
     </dependency>
     <dependency>
       <groupId>org.apache.zookeeper</groupId>
       <artifactId>zookeeper</artifactId>
       <version>${zookeeper.version}</version>
-      <exclusions>
-        <exclusion>
-          <groupId>log4j</groupId>
-          <artifactId>log4j</artifactId>
-        </exclusion>
-      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.zookeeper</groupId>
+      <artifactId>zookeeper-jute</artifactId>
+      <version>${zookeeper.version}</version>
     </dependency>
     <dependency>
       <groupId>org.apache.httpcomponents</groupId>
@@ -260,6 +244,24 @@
       <version>${slf4j.version}</version>
     </dependency>
     
+    <dependency>
+        <groupId>org.eclipse.jetty</groupId>
+        <artifactId>jetty-client</artifactId>
+        <version>${jetty.version}</version>
+    </dependency>
+    <dependency>
+        <groupId>org.eclipse.jetty.http2</groupId>
+        <artifactId>http2-http-client-transport</artifactId>
+        <version>${jetty.version}</version>
+    </dependency>
+
+
+
+
+
+
+
+    
     <!-- Testing dependencies -->
     
     <dependency>
@@ -341,7 +343,6 @@
       <groupId>org.eclipse.jetty</groupId>
       <artifactId>jetty-util</artifactId>
       <version>${jetty.version}</version>
-      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jetty</groupId>
@@ -359,7 +360,6 @@
       <groupId>org.eclipse.jetty</groupId>
       <artifactId>jetty-http</artifactId>
       <version>${jetty.version}</version>
-      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.eclipse.jetty</groupId>
diff --git a/framework/build.xml b/framework/build.xml
index 8f93f4e..d4f4fde 100644
--- a/framework/build.xml
+++ b/framework/build.xml
@@ -49,6 +49,7 @@
             <include name="commons-collections4-*.jar"/>
             <include name="commons-el*.jar"/>
             <include name="commons-fileupload*.jar"/>
+        	<include name="http2*.jar"/>
             <include name="httpcore*.jar"/>
             <include name="httpclient*.jar"/>
             <include name="commons-io*.jar"/>
@@ -737,8 +738,10 @@
                 <include name="commons-collections4*.jar"/>
                 <include name="commons-el*.jar"/>
                 <include name="commons-fileupload*.jar"/>
+                <include name="http2*.jar"/>
                 <include name="httpcore*.jar"/>
                 <include name="httpclient*.jar"/>
+                <include name="jetty*.jar"/>
                 <include name="commons-io*.jar"/>
                 <include name="commons-lang*.jar"/>
                 <include name="commons-logging*.jar"/>
@@ -799,8 +802,10 @@
                 <include name="commons-collections4*.jar"/>
                 <include name="commons-el*.jar"/>
                 <include name="commons-fileupload*.jar"/>
+                <include name="http2*.jar"/>
                 <include name="httpcore*.jar"/>
                 <include name="httpclient*.jar"/>
+                <include name="jetty*.jar"/>
                 <include name="commons-io*.jar"/>
                 <include name="commons-lang*.jar"/>
                 <include name="commons-logging*.jar"/>
@@ -1089,6 +1094,7 @@
                 <include name="jna*.jar"/>
             </fileset>
             <fileset dir="../lib">
+                <include name="http2*.jar"/>
                 <include name="jetty*.jar"/>
                 <include name="javax.servlet-api*.jar"/>
                 <include name="ecj*.jar"/>
@@ -1288,7 +1294,8 @@
     
     <path id="jetty-lib">
         <fileset dir="../lib">
-            <include name="jetty*.jar"/>
+          <include name="http2*.jar"/>
+          <include name="jetty*.jar"/>
         </fileset>
     </path>
     
diff --git a/framework/core/pom.xml b/framework/core/pom.xml
index aa65229..b1c94e1 100644
--- a/framework/core/pom.xml
+++ b/framework/core/pom.xml
@@ -232,6 +232,18 @@
       <version>${slf4j.version}</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>io.dropwizard.metrics</groupId>
+      <artifactId>metrics-core</artifactId>
+      <version>4.1.12.1</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.xerial.snappy</groupId>
+      <artifactId>snappy-java</artifactId>
+      <version>1.1.7.7</version>
+      <scope>test</scope>
+    </dependency>
 
   </dependencies>
 </project>
diff --git a/framework/core/src/test/java/org/apache/manifoldcf/core/lockmanager/ZooKeeperInstance.java b/framework/core/src/test/java/org/apache/manifoldcf/core/lockmanager/ZooKeeperInstance.java
index 75f99d6..9dd7faf 100644
--- a/framework/core/src/test/java/org/apache/manifoldcf/core/lockmanager/ZooKeeperInstance.java
+++ b/framework/core/src/test/java/org/apache/manifoldcf/core/lockmanager/ZooKeeperInstance.java
@@ -18,31 +18,31 @@
 */
 package org.apache.manifoldcf.core.lockmanager;
 
-import java.util.*;
-import java.io.*;
-import org.apache.zookeeper.server.*;
-import org.apache.zookeeper.server.quorum.*;
+import java.io.File;
+import java.io.IOException;
+import java.util.Properties;
 
-public class ZooKeeperInstance
-{
+import org.apache.zookeeper.server.ServerConfig;
+import org.apache.zookeeper.server.ZooKeeperServerMain;
+import org.apache.zookeeper.server.quorum.QuorumPeerConfig;
+
+public class ZooKeeperInstance {
   protected final int zkPort;
   protected final File tempDir;
-  
+
   protected ZooKeeperThread zookeeperThread = null;
-  
-  public ZooKeeperInstance(int zkPort, File tempDir)
-  {
+
+  public ZooKeeperInstance(final int zkPort, final File tempDir) {
     this.zkPort = zkPort;
     this.tempDir = tempDir;
   }
 
-  public void start()
-    throws Exception
-  {
-    Properties startupProperties = new Properties();
-    startupProperties.setProperty("tickTime","2000");
-    startupProperties.setProperty("dataDir",tempDir.toString());
-    startupProperties.setProperty("clientPort",Integer.toString(zkPort));
+  public void start() throws Exception {
+    final Properties startupProperties = new Properties();
+    startupProperties.setProperty("tickTime", "2000");
+    startupProperties.setProperty("dataDir", tempDir.toString());
+    startupProperties.setProperty("clientPort", Integer.toString(zkPort));
+    startupProperties.setProperty("admin.enableServer", "false");
 
     final QuorumPeerConfig quorumConfiguration = new QuorumPeerConfig();
     quorumConfiguration.parseProperties(startupProperties);
@@ -53,33 +53,26 @@
     zookeeperThread = new ZooKeeperThread(configuration);
     zookeeperThread.start();
     // We have no way of knowing whether zookeeper is alive or not, but the
-    // client is supposed to know about that.  But it doesn't, so wait for 5 seconds
+    // client is supposed to know about that. But it doesn't, so wait for 5 seconds
     Thread.sleep(5000L);
   }
-  
-  public void stop()
-    throws Exception
-  {
-    while (true)
-    {
+
+  public void stop() throws Exception {
+    while (true) {
       if (zookeeperThread == null)
         break;
-      else if (!zookeeperThread.isAlive())
-      {
-        Throwable e = zookeeperThread.finishUp();
-        if (e != null)
-        {
+      else if (!zookeeperThread.isAlive()) {
+        final Throwable e = zookeeperThread.finishUp();
+        if (e != null) {
           if (e instanceof RuntimeException)
-            throw (RuntimeException)e;
+            throw (RuntimeException) e;
           else if (e instanceof Exception)
-            throw (Exception)e;
+            throw (Exception) e;
           else if (e instanceof Error)
-            throw (Error)e;
+            throw (Error) e;
         }
         zookeeperThread = null;
-      }
-      else
-      {
+      } else {
         // This isn't the best way to kill zookeeper but it's the only way
         // we've got.
         zookeeperThread.interrupt();
@@ -87,42 +80,33 @@
       }
     }
   }
-  
-  protected static class ZooKeeperThread extends Thread
-  {
+
+  protected static class ZooKeeperThread extends Thread {
     protected final ServerConfig config;
-    
+
     protected Throwable exception = null;
-    
-    public ZooKeeperThread(ServerConfig config)
-    {
+
+    public ZooKeeperThread(final ServerConfig config) {
       this.config = config;
     }
-    
-    public void run()
-    {
-      try
-      {
-        ZooKeeperServerMain server = new ZooKeeperServerMain();
+
+    @Override
+    public void run() {
+      try {
+        final ZooKeeperServerMain server = new ZooKeeperServerMain();
         server.runFromConfig(config);
-      }
-      catch (IOException e)
-      {
+      } catch (final IOException e) {
         // Ignore IOExceptions, since that seems to be normal when shutting
         // down zookeeper via thread.interrupt()
-      }
-      catch (Throwable e)
-      {
+      } catch (final Throwable e) {
         exception = e;
       }
     }
-    
-    public Throwable finishUp()
-      throws InterruptedException
-    {
+
+    public Throwable finishUp() throws InterruptedException {
       join();
       return exception;
     }
   }
-  
+
 }
diff --git a/pom.xml b/pom.xml
index d48b24a..fdb1040 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,7 +48,7 @@
     <postgresql.version>42.1.3</postgresql.version>
     <mysql.version>5.1.47</mysql.version>
     <hsqldb.version>2.3.2</hsqldb.version>
-    <jetty.version>9.4.25.v20191220</jetty.version>
+    <jetty.version>9.4.48.v20220622</jetty.version>
     <jetty-jsp-jdt.version>2.3.3</jetty-jsp-jdt.version>
     <jetty-schemas.version>3.1.M0</jetty-schemas.version>
     <mail.version>1.6.2</mail.version>
@@ -62,10 +62,10 @@
     <commons-collections.version>3.2.2</commons-collections.version>
     <commons-fileupload.version>1.3.3</commons-fileupload.version>
     <commons-discovery.version>0.5</commons-discovery.version>
-    <httpcomponent.httpclient.version>4.5.3</httpcomponent.httpclient.version>
-    <httpcomponent.httpcore.version>4.4.6</httpcomponent.httpcore.version>
-    <httpcomponent.httpmime.version>4.5.3</httpcomponent.httpmime.version>
-    <solr.version>8.3.1</solr.version>
+    <httpcomponent.httpclient.version>4.5.13</httpcomponent.httpclient.version>
+    <httpcomponent.httpcore.version>4.4.15</httpcomponent.httpcore.version>
+    <httpcomponent.httpmime.version>4.5.13</httpcomponent.httpmime.version>
+    <solr.version>9.1.0</solr.version>
     <noggit.version>0.8</noggit.version>
     <commons-el.version>1.0</commons-el.version>
     <commons-lang.version>2.6</commons-lang.version>
@@ -79,9 +79,9 @@
     <glassfish.version>2.1.v20091210</glassfish.version>
     <log4j.version>2.17.2</log4j.version>
     <velocity-engine-core.version>2.3</velocity-engine-core.version>
-    <slf4j.version>1.7.7</slf4j.version>
+    <slf4j.version>1.7.36</slf4j.version>
     <jaxb.version>2.2.6</jaxb.version>
-    <zookeeper.version>3.4.10</zookeeper.version>
+    <zookeeper.version>3.8.0</zookeeper.version>
     <xmlbeans.version>2.6.0</xmlbeans.version>
     <poi.version>3.17</poi.version>
     <tika.version>1.28.1</tika.version>