SOLR-14186: Introduce gitattributes to manage EOL
diff --git a/solr/.gitattributes b/solr/.gitattributes
new file mode 100644
index 0000000..b343da9
--- /dev/null
+++ b/solr/.gitattributes
@@ -0,0 +1,45 @@
+# 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.
+
+# Handles all files not treated particularly below
+*          text=auto
+
+
+# -nix specific
+bin/solr        text eol=lf
+bin/init.d/solr text eol=lf
+bin/post        text eol=lf
+*.bash     text eol=lf
+*.sh       text eol=lf
+
+# Windows specific
+*.bat      text eol=crlf
+*.cmd      text eol=crlf
+*.ps1      text eol=crlf
+
+# Graphics - no eol normalization
+*.png      binary
+*.jpg      binary
+*.jpeg     binary
+*.gif      binary
+*.tif      binary
+*.tiff     binary
+*.ico      binary
+*.svg      binary
+*.eps      binary
+
+# Other files to ignore from eol normalization
+licenses/*      -text
+solr-ref-guide/src/fonts/Inconsolata/OFL.txt  -text
diff --git a/solr/bin/solr.in.cmd b/solr/bin/solr.in.cmd
index e462336..20f42fe 100755
--- a/solr/bin/solr.in.cmd
+++ b/solr/bin/solr.in.cmd
@@ -1,177 +1,177 @@
-@REM

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

-@REM  contributor license agreements.  See the NOTICE file distributed with

-@REM  this work for additional information regarding copyright ownership.

-@REM  The ASF licenses this file to You under the Apache License, Version 2.0

-@REM  (the "License"); you may not use this file except in compliance with

-@REM  the License.  You may obtain a copy of the License at

-@REM

-@REM      http://www.apache.org/licenses/LICENSE-2.0

-@REM

-@REM  Unless required by applicable law or agreed to in writing, software

-@REM  distributed under the License is distributed on an "AS IS" BASIS,

-@REM  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

-@REM  See the License for the specific language governing permissions and

-@REM  limitations under the License.

-

-@echo off

-

-REM Settings here will override settings in existing env vars or in bin/solr.  The default shipped state

-REM of this file is completely commented.

-

-REM By default the script will use JAVA_HOME to determine which java

-REM to use, but you can set a specific path for Solr to use without

-REM affecting other Java applications on your server/workstation.

-REM set SOLR_JAVA_HOME=

-

-REM Increase Java Min/Max Heap as needed to support your indexing / query needs

-REM set SOLR_JAVA_MEM=-Xms512m -Xmx512m

-

-REM Configure verbose GC logging:

-REM For Java 8: if this is set, additional params will be added to specify the log file & rotation

-REM For Java 9 or higher: GC_LOG_OPTS is currently not supported. If you set it, the startup script will exit with failure.

-REM set GC_LOG_OPTS=-verbose:gc -XX:+PrintHeapAtGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime

-

-REM Various GC settings have shown to work well for a number of common Solr workloads.

-REM See solr.cmd GC_TUNE for the default list.

-REM set GC_TUNE=-XX:SurvivorRatio=4

-REM set GC_TUNE=%GC_TUNE% -XX:TargetSurvivorRatio=90

-REM set GC_TUNE=%GC_TUNE% -XX:MaxTenuringThreshold=8

-REM set GC_TUNE=%GC_TUNE% -XX:+UseConcMarkSweepGC

-REM set GC_TUNE=%GC_TUNE% -XX:ConcGCThreads=4

-REM set GC_TUNE=%GC_TUNE% -XX:ParallelGCThreads=4

-REM set GC_TUNE=%GC_TUNE% -XX:+CMSScavengeBeforeRemark

-REM set GC_TUNE=%GC_TUNE% -XX:PretenureSizeThreshold=64m

-REM set GC_TUNE=%GC_TUNE% -XX:+UseCMSInitiatingOccupancyOnly

-REM set GC_TUNE=%GC_TUNE% -XX:CMSInitiatingOccupancyFraction=50

-REM set GC_TUNE=%GC_TUNE% -XX:CMSMaxAbortablePrecleanTime=6000

-REM set GC_TUNE=%GC_TUNE% -XX:+CMSParallelRemarkEnabled

-REM set GC_TUNE=%GC_TUNE% -XX:+ParallelRefProcEnabled

-REM set GC_TUNE=%GC_TUNE% -XX:-OmitStackTraceInFastThrow    etc.

-

-REM Set the ZooKeeper connection string if using an external ZooKeeper ensemble

-REM e.g. host1:2181,host2:2181/chroot

-REM Leave empty if not using SolrCloud

-REM set ZK_HOST=

-

-REM Set the ZooKeeper client timeout (for SolrCloud mode)

-REM set ZK_CLIENT_TIMEOUT=15000

-

-REM By default the start script uses "localhost"; override the hostname here

-REM for production SolrCloud environments to control the hostname exposed to cluster state

-REM set SOLR_HOST=192.168.1.1

-

-REM By default Solr will try to connect to Zookeeper with 30 seconds in timeout; override the timeout if needed

-REM set SOLR_WAIT_FOR_ZK=30

-

-REM By default the start script uses UTC; override the timezone if needed

-REM set SOLR_TIMEZONE=UTC

-

-REM Set to true to activate the JMX RMI connector to allow remote JMX client applications

-REM to monitor the JVM hosting Solr; set to "false" to disable that behavior

-REM (false is recommended in production environments)

-REM set ENABLE_REMOTE_JMX_OPTS=false

-

-REM The script will use SOLR_PORT+10000 for the RMI_PORT or you can set it here

-REM set RMI_PORT=18983

-

-REM Anything you add to the SOLR_OPTS variable will be included in the java

-REM start command line as-is, in ADDITION to other options. If you specify the

-REM -a option on start script, those options will be appended as well. Examples:

-REM set SOLR_OPTS=%SOLR_OPTS% -Dsolr.autoSoftCommit.maxTime=3000

-REM set SOLR_OPTS=%SOLR_OPTS% -Dsolr.autoCommit.maxTime=60000

-REM set SOLR_OPTS=%SOLR_OPTS% -Dsolr.clustering.enabled=true

-

-REM Path to a directory for Solr to store cores and their data. By default, Solr will use server\solr

-REM If solr.xml is not stored in ZooKeeper, this directory needs to contain solr.xml

-REM set SOLR_HOME=

-

-REM Path to a directory that Solr will use as root for data folders for each core.

-REM If not set, defaults to <instance_dir>/data. Overridable per core through 'dataDir' core property

-REM set SOLR_DATA_HOME=

-

-REM Changes the logging level. Valid values: ALL, TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF. Default is INFO

-REM This is an alternative to changing the rootLogger in log4j2.xml

-REM set SOLR_LOG_LEVEL=INFO

-

-REM Location where Solr should write logs to. Absolute or relative to solr start dir

-REM set SOLR_LOGS_DIR=logs

-

-REM Enables log rotation before starting Solr. Setting SOLR_LOG_PRESTART_ROTATION=true will let Solr take care of pre

-REM start rotation of logs. This is false by default as log4j2 handles this for us. If you choose to use another log

-REM framework that cannot do startup rotation, you may want to enable this to let Solr rotate logs on startup.

-REM set SOLR_LOG_PRESTART_ROTATION=false

-

-REM Set the host interface to listen on. Jetty will listen on all interfaces (0.0.0.0) by default.

-REM This must be an IPv4 ("a.b.c.d") or bracketed IPv6 ("[x::y]") address, not a hostname!

-REM set SOLR_JETTY_HOST=0.0.0.0

-

-REM Sets the port Solr binds to, default is 8983

-REM set SOLR_PORT=8983

-

-REM Enables HTTPS. It is implictly true if you set SOLR_SSL_KEY_STORE. Use this config

-REM to enable https module with custom jetty configuration.

-REM set SOLR_SSL_ENABLED=true

-REM Uncomment to set SSL-related system properties

-REM Be sure to update the paths to the correct keystore for your environment

-REM set SOLR_SSL_KEY_STORE=etc/solr-ssl.keystore.jks

-REM set SOLR_SSL_KEY_STORE_PASSWORD=secret

-REM set SOLR_SSL_TRUST_STORE=etc/solr-ssl.keystore.jks

-REM set SOLR_SSL_TRUST_STORE_PASSWORD=secret

-REM Require clients to authenticate

-REM set SOLR_SSL_NEED_CLIENT_AUTH=false

-REM Enable clients to authenticate (but not require)

-REM set SOLR_SSL_WANT_CLIENT_AUTH=false

-REM Verify client hostname during SSL handshake

-REM set SOLR_SSL_CLIENT_HOSTNAME_VERIFICATION=false

-REM SSL Certificates contain host/ip "peer name" information that is validated by default. Setting

-REM this to false can be useful to disable these checks when re-using a certificate on many hosts

-REM set SOLR_SSL_CHECK_PEER_NAME=true

-REM Override Key/Trust Store types if necessary

-REM set SOLR_SSL_KEY_STORE_TYPE=JKS

-REM set SOLR_SSL_TRUST_STORE_TYPE=JKS

-

-REM Uncomment if you want to override previously defined SSL values for HTTP client

-REM otherwise keep them commented and the above values will automatically be set for HTTP clients

-REM set SOLR_SSL_CLIENT_KEY_STORE=

-REM set SOLR_SSL_CLIENT_KEY_STORE_PASSWORD=

-REM set SOLR_SSL_CLIENT_TRUST_STORE=

-REM set SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD=

-REM set SOLR_SSL_CLIENT_KEY_STORE_TYPE=

-REM set SOLR_SSL_CLIENT_TRUST_STORE_TYPE=

-

-REM Sets path of Hadoop credential provider (hadoop.security.credential.provider.path property) and

-REM enables usage of credential store.

-REM Credential provider should store the following keys:

-REM * solr.jetty.keystore.password

-REM * solr.jetty.truststore.password

-REM Set the two below if you want to set specific store passwords for HTTP client

-REM * javax.net.ssl.keyStorePassword

-REM * javax.net.ssl.trustStorePassword

-REM More info: https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-common/CredentialProviderAPI.html

-REM set SOLR_HADOOP_CREDENTIAL_PROVIDER_PATH=localjceks://file/home/solr/hadoop-credential-provider.jceks

-REM set SOLR_OPTS=" -Dsolr.ssl.credential.provider.chain=hadoop"

-

-REM Settings for authentication

-REM Please configure only one of SOLR_AUTHENTICATION_CLIENT_BUILDER or SOLR_AUTH_TYPE parameters

-REM set SOLR_AUTHENTICATION_CLIENT_BUILDER=org.apache.solr.client.solrj.impl.PreemptiveBasicAuthClientBuilderFactory

-REM set SOLR_AUTH_TYPE=basic

-REM set SOLR_AUTHENTICATION_OPTS="-Dbasicauth=solr:SolrRocks"

-

-REM Settings for ZK ACL

-REM set SOLR_ZK_CREDS_AND_ACLS=-DzkACLProvider=org.apache.solr.common.cloud.VMParamsAllAndReadonlyDigestZkACLProvider ^

-REM  -DzkCredentialsProvider=org.apache.solr.common.cloud.VMParamsSingleSetCredentialsDigestZkCredentialsProvider ^

-REM  -DzkDigestUsername=admin-user -DzkDigestPassword=CHANGEME-ADMIN-PASSWORD ^

-REM  -DzkDigestReadonlyUsername=readonly-user -DzkDigestReadonlyPassword=CHANGEME-READONLY-PASSWORD

-REM set SOLR_OPTS=%SOLR_OPTS% %SOLR_ZK_CREDS_AND_ACLS%

-

-REM When running Solr in non-cloud mode and if planning to do distributed search (using the "shards" parameter), the

-REM list of hosts needs to be whitelisted or Solr will forbid the request. The whitelist can be configured in solr.xml,

-REM or if you are using the OOTB solr.xml, can be specified using the system property "solr.shardsWhitelist". Alternatively

-REM host checking can be disabled by using the system property "solr.disable.shardsWhitelist"

-REM set SOLR_OPTS="%SOLR_OPTS% -Dsolr.shardsWhitelist=http://localhost:8983,http://localhost:8984"

-

-REM For a visual indication in the Admin UI of what type of environment this cluster is, configure

-REM a -Dsolr.environment property below. Valid values are prod, stage, test, dev, with an optional

-REM label or color, e.g. -Dsolr.environment=test,label=Functional+test,color=brown

-REM SOLR_OPTS="$SOLR_OPTS -Dsolr.environment=prod"

+@REM
+@REM  Licensed to the Apache Software Foundation (ASF) under one or more
+@REM  contributor license agreements.  See the NOTICE file distributed with
+@REM  this work for additional information regarding copyright ownership.
+@REM  The ASF licenses this file to You under the Apache License, Version 2.0
+@REM  (the "License"); you may not use this file except in compliance with
+@REM  the License.  You may obtain a copy of the License at
+@REM
+@REM      http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM  Unless required by applicable law or agreed to in writing, software
+@REM  distributed under the License is distributed on an "AS IS" BASIS,
+@REM  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@REM  See the License for the specific language governing permissions and
+@REM  limitations under the License.
+
+@echo off
+
+REM Settings here will override settings in existing env vars or in bin/solr.  The default shipped state
+REM of this file is completely commented.
+
+REM By default the script will use JAVA_HOME to determine which java
+REM to use, but you can set a specific path for Solr to use without
+REM affecting other Java applications on your server/workstation.
+REM set SOLR_JAVA_HOME=
+
+REM Increase Java Min/Max Heap as needed to support your indexing / query needs
+REM set SOLR_JAVA_MEM=-Xms512m -Xmx512m
+
+REM Configure verbose GC logging:
+REM For Java 8: if this is set, additional params will be added to specify the log file & rotation
+REM For Java 9 or higher: GC_LOG_OPTS is currently not supported. If you set it, the startup script will exit with failure.
+REM set GC_LOG_OPTS=-verbose:gc -XX:+PrintHeapAtGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationStoppedTime
+
+REM Various GC settings have shown to work well for a number of common Solr workloads.
+REM See solr.cmd GC_TUNE for the default list.
+REM set GC_TUNE=-XX:SurvivorRatio=4
+REM set GC_TUNE=%GC_TUNE% -XX:TargetSurvivorRatio=90
+REM set GC_TUNE=%GC_TUNE% -XX:MaxTenuringThreshold=8
+REM set GC_TUNE=%GC_TUNE% -XX:+UseConcMarkSweepGC
+REM set GC_TUNE=%GC_TUNE% -XX:ConcGCThreads=4
+REM set GC_TUNE=%GC_TUNE% -XX:ParallelGCThreads=4
+REM set GC_TUNE=%GC_TUNE% -XX:+CMSScavengeBeforeRemark
+REM set GC_TUNE=%GC_TUNE% -XX:PretenureSizeThreshold=64m
+REM set GC_TUNE=%GC_TUNE% -XX:+UseCMSInitiatingOccupancyOnly
+REM set GC_TUNE=%GC_TUNE% -XX:CMSInitiatingOccupancyFraction=50
+REM set GC_TUNE=%GC_TUNE% -XX:CMSMaxAbortablePrecleanTime=6000
+REM set GC_TUNE=%GC_TUNE% -XX:+CMSParallelRemarkEnabled
+REM set GC_TUNE=%GC_TUNE% -XX:+ParallelRefProcEnabled
+REM set GC_TUNE=%GC_TUNE% -XX:-OmitStackTraceInFastThrow    etc.
+
+REM Set the ZooKeeper connection string if using an external ZooKeeper ensemble
+REM e.g. host1:2181,host2:2181/chroot
+REM Leave empty if not using SolrCloud
+REM set ZK_HOST=
+
+REM Set the ZooKeeper client timeout (for SolrCloud mode)
+REM set ZK_CLIENT_TIMEOUT=15000
+
+REM By default the start script uses "localhost"; override the hostname here
+REM for production SolrCloud environments to control the hostname exposed to cluster state
+REM set SOLR_HOST=192.168.1.1
+
+REM By default Solr will try to connect to Zookeeper with 30 seconds in timeout; override the timeout if needed
+REM set SOLR_WAIT_FOR_ZK=30
+
+REM By default the start script uses UTC; override the timezone if needed
+REM set SOLR_TIMEZONE=UTC
+
+REM Set to true to activate the JMX RMI connector to allow remote JMX client applications
+REM to monitor the JVM hosting Solr; set to "false" to disable that behavior
+REM (false is recommended in production environments)
+REM set ENABLE_REMOTE_JMX_OPTS=false
+
+REM The script will use SOLR_PORT+10000 for the RMI_PORT or you can set it here
+REM set RMI_PORT=18983
+
+REM Anything you add to the SOLR_OPTS variable will be included in the java
+REM start command line as-is, in ADDITION to other options. If you specify the
+REM -a option on start script, those options will be appended as well. Examples:
+REM set SOLR_OPTS=%SOLR_OPTS% -Dsolr.autoSoftCommit.maxTime=3000
+REM set SOLR_OPTS=%SOLR_OPTS% -Dsolr.autoCommit.maxTime=60000
+REM set SOLR_OPTS=%SOLR_OPTS% -Dsolr.clustering.enabled=true
+
+REM Path to a directory for Solr to store cores and their data. By default, Solr will use server\solr
+REM If solr.xml is not stored in ZooKeeper, this directory needs to contain solr.xml
+REM set SOLR_HOME=
+
+REM Path to a directory that Solr will use as root for data folders for each core.
+REM If not set, defaults to <instance_dir>/data. Overridable per core through 'dataDir' core property
+REM set SOLR_DATA_HOME=
+
+REM Changes the logging level. Valid values: ALL, TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF. Default is INFO
+REM This is an alternative to changing the rootLogger in log4j2.xml
+REM set SOLR_LOG_LEVEL=INFO
+
+REM Location where Solr should write logs to. Absolute or relative to solr start dir
+REM set SOLR_LOGS_DIR=logs
+
+REM Enables log rotation before starting Solr. Setting SOLR_LOG_PRESTART_ROTATION=true will let Solr take care of pre
+REM start rotation of logs. This is false by default as log4j2 handles this for us. If you choose to use another log
+REM framework that cannot do startup rotation, you may want to enable this to let Solr rotate logs on startup.
+REM set SOLR_LOG_PRESTART_ROTATION=false
+
+REM Set the host interface to listen on. Jetty will listen on all interfaces (0.0.0.0) by default.
+REM This must be an IPv4 ("a.b.c.d") or bracketed IPv6 ("[x::y]") address, not a hostname!
+REM set SOLR_JETTY_HOST=0.0.0.0
+
+REM Sets the port Solr binds to, default is 8983
+REM set SOLR_PORT=8983
+
+REM Enables HTTPS. It is implictly true if you set SOLR_SSL_KEY_STORE. Use this config
+REM to enable https module with custom jetty configuration.
+REM set SOLR_SSL_ENABLED=true
+REM Uncomment to set SSL-related system properties
+REM Be sure to update the paths to the correct keystore for your environment
+REM set SOLR_SSL_KEY_STORE=etc/solr-ssl.keystore.jks
+REM set SOLR_SSL_KEY_STORE_PASSWORD=secret
+REM set SOLR_SSL_TRUST_STORE=etc/solr-ssl.keystore.jks
+REM set SOLR_SSL_TRUST_STORE_PASSWORD=secret
+REM Require clients to authenticate
+REM set SOLR_SSL_NEED_CLIENT_AUTH=false
+REM Enable clients to authenticate (but not require)
+REM set SOLR_SSL_WANT_CLIENT_AUTH=false
+REM Verify client hostname during SSL handshake
+REM set SOLR_SSL_CLIENT_HOSTNAME_VERIFICATION=false
+REM SSL Certificates contain host/ip "peer name" information that is validated by default. Setting
+REM this to false can be useful to disable these checks when re-using a certificate on many hosts
+REM set SOLR_SSL_CHECK_PEER_NAME=true
+REM Override Key/Trust Store types if necessary
+REM set SOLR_SSL_KEY_STORE_TYPE=JKS
+REM set SOLR_SSL_TRUST_STORE_TYPE=JKS
+
+REM Uncomment if you want to override previously defined SSL values for HTTP client
+REM otherwise keep them commented and the above values will automatically be set for HTTP clients
+REM set SOLR_SSL_CLIENT_KEY_STORE=
+REM set SOLR_SSL_CLIENT_KEY_STORE_PASSWORD=
+REM set SOLR_SSL_CLIENT_TRUST_STORE=
+REM set SOLR_SSL_CLIENT_TRUST_STORE_PASSWORD=
+REM set SOLR_SSL_CLIENT_KEY_STORE_TYPE=
+REM set SOLR_SSL_CLIENT_TRUST_STORE_TYPE=
+
+REM Sets path of Hadoop credential provider (hadoop.security.credential.provider.path property) and
+REM enables usage of credential store.
+REM Credential provider should store the following keys:
+REM * solr.jetty.keystore.password
+REM * solr.jetty.truststore.password
+REM Set the two below if you want to set specific store passwords for HTTP client
+REM * javax.net.ssl.keyStorePassword
+REM * javax.net.ssl.trustStorePassword
+REM More info: https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-common/CredentialProviderAPI.html
+REM set SOLR_HADOOP_CREDENTIAL_PROVIDER_PATH=localjceks://file/home/solr/hadoop-credential-provider.jceks
+REM set SOLR_OPTS=" -Dsolr.ssl.credential.provider.chain=hadoop"
+
+REM Settings for authentication
+REM Please configure only one of SOLR_AUTHENTICATION_CLIENT_BUILDER or SOLR_AUTH_TYPE parameters
+REM set SOLR_AUTHENTICATION_CLIENT_BUILDER=org.apache.solr.client.solrj.impl.PreemptiveBasicAuthClientBuilderFactory
+REM set SOLR_AUTH_TYPE=basic
+REM set SOLR_AUTHENTICATION_OPTS="-Dbasicauth=solr:SolrRocks"
+
+REM Settings for ZK ACL
+REM set SOLR_ZK_CREDS_AND_ACLS=-DzkACLProvider=org.apache.solr.common.cloud.VMParamsAllAndReadonlyDigestZkACLProvider ^
+REM  -DzkCredentialsProvider=org.apache.solr.common.cloud.VMParamsSingleSetCredentialsDigestZkCredentialsProvider ^
+REM  -DzkDigestUsername=admin-user -DzkDigestPassword=CHANGEME-ADMIN-PASSWORD ^
+REM  -DzkDigestReadonlyUsername=readonly-user -DzkDigestReadonlyPassword=CHANGEME-READONLY-PASSWORD
+REM set SOLR_OPTS=%SOLR_OPTS% %SOLR_ZK_CREDS_AND_ACLS%
+
+REM When running Solr in non-cloud mode and if planning to do distributed search (using the "shards" parameter), the
+REM list of hosts needs to be whitelisted or Solr will forbid the request. The whitelist can be configured in solr.xml,
+REM or if you are using the OOTB solr.xml, can be specified using the system property "solr.shardsWhitelist". Alternatively
+REM host checking can be disabled by using the system property "solr.disable.shardsWhitelist"
+REM set SOLR_OPTS="%SOLR_OPTS% -Dsolr.shardsWhitelist=http://localhost:8983,http://localhost:8984"
+
+REM For a visual indication in the Admin UI of what type of environment this cluster is, configure
+REM a -Dsolr.environment property below. Valid values are prod, stage, test, dev, with an optional
+REM label or color, e.g. -Dsolr.environment=test,label=Functional+test,color=brown
+REM SOLR_OPTS="$SOLR_OPTS -Dsolr.environment=prod"
diff --git a/solr/core/src/java/org/apache/solr/cloud/RecoveringCoreTermWatcher.java b/solr/core/src/java/org/apache/solr/cloud/RecoveringCoreTermWatcher.java
index 5d4ec17..71e3fe4 100644
--- a/solr/core/src/java/org/apache/solr/cloud/RecoveringCoreTermWatcher.java
+++ b/solr/core/src/java/org/apache/solr/cloud/RecoveringCoreTermWatcher.java
@@ -1,86 +1,86 @@
-/*

- * 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.solr.cloud;

-

-import java.lang.invoke.MethodHandles;

-import java.util.concurrent.atomic.AtomicLong;

-

-import org.apache.solr.client.solrj.cloud.ShardTerms;

-import org.apache.solr.core.CoreContainer;

-import org.apache.solr.core.CoreDescriptor;

-import org.apache.solr.core.SolrCore;

-import org.slf4j.Logger;

-import org.slf4j.LoggerFactory;

-

-/**

- * Start recovery of a core if its term is less than leader's term

- */

-public class RecoveringCoreTermWatcher implements ZkShardTerms.CoreTermWatcher {

-  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

-  private final CoreDescriptor coreDescriptor;

-  private final CoreContainer coreContainer;

-  // used to prevent the case when term of other replicas get changed, we redo recovery

-  // the idea here is with a specific term of a replica, we only do recovery one

-  private final AtomicLong lastTermDoRecovery;

-

-  RecoveringCoreTermWatcher(CoreDescriptor coreDescriptor, CoreContainer coreContainer) {

-    this.coreDescriptor = coreDescriptor;

-    this.coreContainer = coreContainer;

-    this.lastTermDoRecovery = new AtomicLong(-1);

-  }

-

-  @Override

-  public boolean onTermChanged(ShardTerms terms) {

-    if (coreContainer.isShutDown()) return false;

-

-    try (SolrCore solrCore = coreContainer.getCore(coreDescriptor.getName())) {

-      if (solrCore == null || solrCore.isClosed()) {

-        return false;

-      }

-

-      if (solrCore.getCoreDescriptor() == null || solrCore.getCoreDescriptor().getCloudDescriptor() == null) return true;

-      String coreNodeName = solrCore.getCoreDescriptor().getCloudDescriptor().getCoreNodeName();

-      if (terms.haveHighestTermValue(coreNodeName)) return true;

-      if (lastTermDoRecovery.get() < terms.getTerm(coreNodeName)) {

-        log.info("Start recovery on {} because core's term is less than leader's term", coreNodeName);

-        lastTermDoRecovery.set(terms.getTerm(coreNodeName));

-        solrCore.getUpdateHandler().getSolrCoreState().doRecovery(solrCore.getCoreContainer(), solrCore.getCoreDescriptor());

-      }

-    } catch (Exception e) {

-      log.info("Failed to watch term of core {}", coreDescriptor.getName(), e);

-      return false;

-    }

-

-    return true;

-  }

-

-  @Override

-  public boolean equals(Object o) {

-    if (this == o) return true;

-    if (o == null || getClass() != o.getClass()) return false;

-

-    RecoveringCoreTermWatcher that = (RecoveringCoreTermWatcher) o;

-

-    return coreDescriptor.getName().equals(that.coreDescriptor.getName());

-  }

-

-  @Override

-  public int hashCode() {

-    return coreDescriptor.getName().hashCode();

-  }

-}

+/*
+ * 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.solr.cloud;
+
+import java.lang.invoke.MethodHandles;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.solr.client.solrj.cloud.ShardTerms;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.CoreDescriptor;
+import org.apache.solr.core.SolrCore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Start recovery of a core if its term is less than leader's term
+ */
+public class RecoveringCoreTermWatcher implements ZkShardTerms.CoreTermWatcher {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+  private final CoreDescriptor coreDescriptor;
+  private final CoreContainer coreContainer;
+  // used to prevent the case when term of other replicas get changed, we redo recovery
+  // the idea here is with a specific term of a replica, we only do recovery one
+  private final AtomicLong lastTermDoRecovery;
+
+  RecoveringCoreTermWatcher(CoreDescriptor coreDescriptor, CoreContainer coreContainer) {
+    this.coreDescriptor = coreDescriptor;
+    this.coreContainer = coreContainer;
+    this.lastTermDoRecovery = new AtomicLong(-1);
+  }
+
+  @Override
+  public boolean onTermChanged(ShardTerms terms) {
+    if (coreContainer.isShutDown()) return false;
+
+    try (SolrCore solrCore = coreContainer.getCore(coreDescriptor.getName())) {
+      if (solrCore == null || solrCore.isClosed()) {
+        return false;
+      }
+
+      if (solrCore.getCoreDescriptor() == null || solrCore.getCoreDescriptor().getCloudDescriptor() == null) return true;
+      String coreNodeName = solrCore.getCoreDescriptor().getCloudDescriptor().getCoreNodeName();
+      if (terms.haveHighestTermValue(coreNodeName)) return true;
+      if (lastTermDoRecovery.get() < terms.getTerm(coreNodeName)) {
+        log.info("Start recovery on {} because core's term is less than leader's term", coreNodeName);
+        lastTermDoRecovery.set(terms.getTerm(coreNodeName));
+        solrCore.getUpdateHandler().getSolrCoreState().doRecovery(solrCore.getCoreContainer(), solrCore.getCoreDescriptor());
+      }
+    } catch (Exception e) {
+      log.info("Failed to watch term of core {}", coreDescriptor.getName(), e);
+      return false;
+    }
+
+    return true;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+
+    RecoveringCoreTermWatcher that = (RecoveringCoreTermWatcher) o;
+
+    return coreDescriptor.getName().equals(that.coreDescriptor.getName());
+  }
+
+  @Override
+  public int hashCode() {
+    return coreDescriptor.getName().hashCode();
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/cloud/ZkCollectionTerms.java b/solr/core/src/java/org/apache/solr/cloud/ZkCollectionTerms.java
index b232f9b..629f2bc 100644
--- a/solr/core/src/java/org/apache/solr/cloud/ZkCollectionTerms.java
+++ b/solr/core/src/java/org/apache/solr/cloud/ZkCollectionTerms.java
@@ -1,65 +1,65 @@
-/*

- * 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.solr.cloud;

-

-import java.util.HashMap;

-import java.util.Map;

-

-import org.apache.solr.common.cloud.SolrZkClient;

-import org.apache.solr.common.util.ObjectReleaseTracker;

-import org.apache.solr.core.CoreDescriptor;

-

-/**

- * Used to manage all ZkShardTerms of a collection

- */

-class ZkCollectionTerms implements AutoCloseable {

-  private final String collection;

-  private final Map<String, ZkShardTerms> terms;

-  private final SolrZkClient zkClient;

-

-  ZkCollectionTerms(String collection, SolrZkClient client) {

-    this.collection = collection;

-    this.terms = new HashMap<>();

-    this.zkClient = client;

-    ObjectReleaseTracker.track(this);

-  }

-

-

-  public ZkShardTerms getShard(String shardId) {

-    synchronized (terms) {

-      if (!terms.containsKey(shardId)) terms.put(shardId, new ZkShardTerms(collection, shardId, zkClient));

-      return terms.get(shardId);

-    }

-  }

-

-  public void remove(String shardId, CoreDescriptor coreDescriptor) {

-    synchronized (terms) {

-      if (getShard(shardId).removeTerm(coreDescriptor)) {

-        terms.remove(shardId).close();

-      }

-    }

-  }

-

-  public void close() {

-    synchronized (terms) {

-      terms.values().forEach(ZkShardTerms::close);

-    }

-    ObjectReleaseTracker.release(this);

-  }

-

-}

+/*
+ * 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.solr.cloud;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.solr.common.cloud.SolrZkClient;
+import org.apache.solr.common.util.ObjectReleaseTracker;
+import org.apache.solr.core.CoreDescriptor;
+
+/**
+ * Used to manage all ZkShardTerms of a collection
+ */
+class ZkCollectionTerms implements AutoCloseable {
+  private final String collection;
+  private final Map<String, ZkShardTerms> terms;
+  private final SolrZkClient zkClient;
+
+  ZkCollectionTerms(String collection, SolrZkClient client) {
+    this.collection = collection;
+    this.terms = new HashMap<>();
+    this.zkClient = client;
+    ObjectReleaseTracker.track(this);
+  }
+
+
+  public ZkShardTerms getShard(String shardId) {
+    synchronized (terms) {
+      if (!terms.containsKey(shardId)) terms.put(shardId, new ZkShardTerms(collection, shardId, zkClient));
+      return terms.get(shardId);
+    }
+  }
+
+  public void remove(String shardId, CoreDescriptor coreDescriptor) {
+    synchronized (terms) {
+      if (getShard(shardId).removeTerm(coreDescriptor)) {
+        terms.remove(shardId).close();
+      }
+    }
+  }
+
+  public void close() {
+    synchronized (terms) {
+      terms.values().forEach(ZkShardTerms::close);
+    }
+    ObjectReleaseTracker.release(this);
+  }
+
+}
diff --git a/solr/core/src/java/org/apache/solr/cloud/ZkShardTerms.java b/solr/core/src/java/org/apache/solr/cloud/ZkShardTerms.java
index 2c2a24a..0cab1a8 100644
--- a/solr/core/src/java/org/apache/solr/cloud/ZkShardTerms.java
+++ b/solr/core/src/java/org/apache/solr/cloud/ZkShardTerms.java
@@ -1,425 +1,425 @@
-/*

- * 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.solr.cloud;

-

-import java.lang.invoke.MethodHandles;

-import java.util.HashMap;

-import java.util.HashSet;

-import java.util.Map;

-import java.util.Set;

-import java.util.concurrent.TimeoutException;

-import java.util.concurrent.atomic.AtomicBoolean;

-

-import org.apache.solr.client.solrj.cloud.ShardTerms;

-import org.apache.solr.common.SolrException;

-import org.apache.solr.common.cloud.SolrZkClient;

-import org.apache.solr.common.cloud.ZkStateReader;

-import org.apache.solr.common.util.ObjectReleaseTracker;

-import org.apache.solr.common.util.Utils;

-import org.apache.solr.core.CoreDescriptor;

-import org.apache.zookeeper.CreateMode;

-import org.apache.zookeeper.KeeperException;

-import org.apache.zookeeper.Watcher;

-import org.apache.zookeeper.data.Stat;

-import org.slf4j.Logger;

-import org.slf4j.LoggerFactory;

-

-/**

- * Class used for interact with a ZK term node.

- * Each ZK term node relates to a shard of a collection and have this format (in json)

- * <p>

- * <code>

- * {

- *   "replicaNodeName1" : 1,

- *   "replicaNodeName2" : 2,

- *   ..

- * }

- * </code>

- * <p>

- * The values correspond to replicas are called terms.

- * Only replicas with highest term value are considered up to date and be able to become leader and serve queries.

- * <p>

- * Terms can only updated in two strict ways:

- * <ul>

- * <li>A replica sets its term equals to leader's term

- * <li>The leader increase its term and some other replicas by 1

- * </ul>

- * This class should not be reused after {@link org.apache.zookeeper.Watcher.Event.KeeperState#Expired} event

- */

-public class ZkShardTerms implements AutoCloseable{

-

-  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

-

-  private final Object writingLock = new Object();

-  private final String collection;

-  private final String shard;

-  private final String znodePath;

-  private final SolrZkClient zkClient;

-  private final Set<CoreTermWatcher> listeners = new HashSet<>();

-  private final AtomicBoolean isClosed = new AtomicBoolean(false);

-

-  private ShardTerms terms;

-

-  // Listener of a core for shard's term change events

-  interface CoreTermWatcher {

-    // return true if the listener wanna to be triggered in the next time

-    boolean onTermChanged(ShardTerms terms);

-  }

-

-  public ZkShardTerms(String collection, String shard, SolrZkClient zkClient) {

-    this.znodePath = ZkStateReader.COLLECTIONS_ZKNODE + "/" + collection + "/terms/" + shard;

-    this.collection = collection;

-    this.shard = shard;

-    this.zkClient = zkClient;

-    ensureTermNodeExist();

-    refreshTerms();

-    retryRegisterWatcher();

-    ObjectReleaseTracker.track(this);

-  }

-

-  /**

-   * Ensure that leader's term is higher than some replica's terms

-   * @param leader coreNodeName of leader

-   * @param replicasNeedingRecovery set of replicas in which their terms should be lower than leader's term

-   */

-  public void ensureTermsIsHigher(String leader, Set<String> replicasNeedingRecovery) {

-    if (replicasNeedingRecovery.isEmpty()) return;

-

-    ShardTerms newTerms;

-    while( (newTerms = terms.increaseTerms(leader, replicasNeedingRecovery)) != null) {

-      if (forceSaveTerms(newTerms)) return;

-    }

-  }

-

-  public ShardTerms getShardTerms() {

-    return terms;

-  }

-  /**

-   * Can this replica become leader?

-   * @param coreNodeName of the replica

-   * @return true if this replica can become leader, false if otherwise

-   */

-  public boolean canBecomeLeader(String coreNodeName) {

-    return terms.canBecomeLeader(coreNodeName);

-  }

-

-  /**

-   * Should leader skip sending updates to this replica?

-   * @param coreNodeName of the replica

-   * @return true if this replica has term equals to leader's term, false if otherwise

-   */

-  public boolean skipSendingUpdatesTo(String coreNodeName) {

-    return !terms.haveHighestTermValue(coreNodeName);

-  }

-

-  /**

-   * Did this replica registered its term? This is a sign to check f

-   * @param coreNodeName of the replica

-   * @return true if this replica registered its term, false if otherwise

-   */

-  public boolean registered(String coreNodeName) {

-    return terms.getTerm(coreNodeName) != null;

-  }

-

-  public void close() {

-    // no watcher will be registered

-    isClosed.set(true);

-    synchronized (listeners) {

-      listeners.clear();

-    }

-    ObjectReleaseTracker.release(this);

-  }

-

-  // package private for testing, only used by tests

-  Map<String, Long> getTerms() {

-    synchronized (writingLock) {

-      return terms.getTerms();

-    }

-  }

-

-  /**

-   * Add a listener so the next time the shard's term get updated, listeners will be called

-   */

-  void addListener(CoreTermWatcher listener) {

-    synchronized (listeners) {

-      listeners.add(listener);

-    }

-  }

-

-  /**

-   * Remove the coreNodeName from terms map and also remove any expired listeners

-   * @return Return true if this object should not be reused

-   */

-  boolean removeTerm(CoreDescriptor cd) {

-    int numListeners;

-    synchronized (listeners) {

-      // solrcore already closed

-      listeners.removeIf(coreTermWatcher -> !coreTermWatcher.onTermChanged(terms));

-      numListeners = listeners.size();

-    }

-    return removeTerm(cd.getCloudDescriptor().getCoreNodeName()) || numListeners == 0;

-  }

-

-  // package private for testing, only used by tests

-  // return true if this object should not be reused

-  boolean removeTerm(String coreNodeName) {

-    ShardTerms newTerms;

-    while ( (newTerms = terms.removeTerm(coreNodeName)) != null) {

-      try {

-        if (saveTerms(newTerms)) return false;

-      } catch (KeeperException.NoNodeException e) {

-        return true;

-      }

-    }

-    return true;

-  }

-

-  /**

-   * Register a replica's term (term value will be 0).

-   * If a term is already associate with this replica do nothing

-   * @param coreNodeName of the replica

-   */

-  void registerTerm(String coreNodeName) {

-    ShardTerms newTerms;

-    while ( (newTerms = terms.registerTerm(coreNodeName)) != null) {

-      if (forceSaveTerms(newTerms)) break;

-    }

-  }

-

-  /**

-   * Set a replica's term equals to leader's term, and remove recovering flag of a replica.

-   * This call should only be used by {@link org.apache.solr.common.params.CollectionParams.CollectionAction#FORCELEADER}

-   * @param coreNodeName of the replica

-   */

-  public void setTermEqualsToLeader(String coreNodeName) {

-    ShardTerms newTerms;

-    while ( (newTerms = terms.setTermEqualsToLeader(coreNodeName)) != null) {

-      if (forceSaveTerms(newTerms)) break;

-    }

-  }

-

-  public void setTermToZero(String coreNodeName) {

-    ShardTerms newTerms;

-    while ( (newTerms = terms.setTermToZero(coreNodeName)) != null) {

-      if (forceSaveTerms(newTerms)) break;

-    }

-  }

-

-  /**

-   * Mark {@code coreNodeName} as recovering

-   */

-  public void startRecovering(String coreNodeName) {

-    ShardTerms newTerms;

-    while ( (newTerms = terms.startRecovering(coreNodeName)) != null) {

-      if (forceSaveTerms(newTerms)) break;

-    }

-  }

-

-  /**

-   * Mark {@code coreNodeName} as finished recovering

-   */

-  public void doneRecovering(String coreNodeName) {

-    ShardTerms newTerms;

-    while ( (newTerms = terms.doneRecovering(coreNodeName)) != null) {

-      if (forceSaveTerms(newTerms)) break;

-    }

-  }

-

-  public boolean isRecovering(String name) {

-    return terms.isRecovering(name);

-  }

-

-  /**

-   * When first updates come in, all replicas have some data now,

-   * so we must switch from term 0 (registered) to 1 (have some data)

-   */

-  public void ensureHighestTermsAreNotZero() {

-    ShardTerms newTerms;

-    while ( (newTerms = terms.ensureHighestTermsAreNotZero()) != null) {

-      if (forceSaveTerms(newTerms)) break;

-    }

-  }

-

-  public long getHighestTerm() {

-    return terms.getMaxTerm();

-  }

-

-  public long getTerm(String coreNodeName) {

-    Long term = terms.getTerm(coreNodeName);

-    return term == null? -1 : term;

-  }

-

-  // package private for testing, only used by tests

-  int getNumListeners() {

-    synchronized (listeners) {

-      return listeners.size();

-    }

-  }

-

-  /**

-   * Set new terms to ZK.

-   * In case of correspond ZK term node is not created, create it

-   * @param newTerms to be set

-   * @return true if terms is saved successfully to ZK, false if otherwise

-   */

-  private boolean forceSaveTerms(ShardTerms newTerms) {

-    try {

-      return saveTerms(newTerms);

-    } catch (KeeperException.NoNodeException e) {

-      ensureTermNodeExist();

-      return false;

-    }

-  }

-

-  /**

-   * Set new terms to ZK, the version of new terms must match the current ZK term node

-   * @param newTerms to be set

-   * @return true if terms is saved successfully to ZK, false if otherwise

-   * @throws KeeperException.NoNodeException correspond ZK term node is not created

-   */

-  private boolean saveTerms(ShardTerms newTerms) throws KeeperException.NoNodeException {

-    byte[] znodeData = Utils.toJSON(newTerms);

-    try {

-      Stat stat = zkClient.setData(znodePath, znodeData, newTerms.getVersion(), true);

-      setNewTerms(new ShardTerms(newTerms, stat.getVersion()));

-      log.info("Successful update of terms at {} to {}", znodePath, newTerms);

-      return true;

-    } catch (KeeperException.BadVersionException e) {

-      log.info("Failed to save terms, version is not a match, retrying");

-      refreshTerms();

-    } catch (KeeperException.NoNodeException e) {

-      throw e;

-    } catch (Exception e) {

-      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error while saving shard term for collection: " + collection, e);

-    }

-    return false;

-  }

-

-  /**

-   * Create correspond ZK term node

-   */

-  private void ensureTermNodeExist() {

-    String path = "/collections/" + collection + "/terms";

-    try {

-      path += "/" + shard;

-

-      try {

-        Map<String,Long> initialTerms = new HashMap<>();

-        zkClient.makePath(path, Utils.toJSON(initialTerms), CreateMode.PERSISTENT, true);

-      } catch (KeeperException.NodeExistsException e) {

-        // it's okay if another beats us creating the node

-      }

-

-    } catch (InterruptedException e) {

-      Thread.interrupted();

-      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,

-          "Error creating shard term node in Zookeeper for collection: " + collection, e);

-    } catch (KeeperException e) {

-      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,

-          "Error creating shard term node in Zookeeper for collection: " + collection, e);

-    }

-  }

-

-  /**

-   * Fetch latest terms from ZK

-   */

-  public void refreshTerms() {

-    ShardTerms newTerms;

-    try {

-      Stat stat = new Stat();

-      byte[] data = zkClient.getData(znodePath, null, stat, true);

-      newTerms = new ShardTerms((Map<String, Long>) Utils.fromJSON(data), stat.getVersion());

-    } catch (KeeperException e) {

-      Thread.interrupted();

-      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error updating shard term for collection: " + collection, e);

-    } catch (InterruptedException e) {

-      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error updating shard term for collection: " + collection, e);

-    }

-

-    setNewTerms(newTerms);

-  }

-

-  /**

-   * Retry register a watcher to the correspond ZK term node

-   */

-  private void retryRegisterWatcher() {

-    while (!isClosed.get()) {

-      try {

-        registerWatcher();

-        return;

-      } catch (KeeperException.SessionExpiredException | KeeperException.AuthFailedException e) {

-        isClosed.set(true);

-        log.error("Failed watching shard term for collection: {} due to unrecoverable exception", collection, e);

-        return;

-      } catch (KeeperException e) {

-        log.warn("Failed watching shard term for collection: {}, retrying!", collection, e);

-        try {

-          zkClient.getConnectionManager().waitForConnected(zkClient.getZkClientTimeout());

-        } catch (TimeoutException te) {

-          if (Thread.interrupted()) {

-            throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error watching shard term for collection: " + collection, te);

-          }

-        }

-      }

-    }

-  }

-

-  /**

-   * Register a watcher to the correspond ZK term node

-   */

-  private void registerWatcher() throws KeeperException {

-    Watcher watcher = event -> {

-      // session events are not change events, and do not remove the watcher

-      if (Watcher.Event.EventType.None == event.getType()) {

-        return;

-      }

-      retryRegisterWatcher();

-      // Some events may be missed during register a watcher, so it is safer to refresh terms after registering watcher

-      refreshTerms();

-    };

-    try {

-      // exists operation is faster than getData operation

-      zkClient.exists(znodePath, watcher, true);

-    } catch (InterruptedException e) {

-      Thread.interrupted();

-      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error watching shard term for collection: " + collection, e);

-    }

-  }

-

-

-  /**

-   * Atomically update {@link ZkShardTerms#terms} and call listeners

-   * @param newTerms to be set

-   */

-  private void setNewTerms(ShardTerms newTerms) {

-    boolean isChanged = false;

-    synchronized (writingLock) {

-      if (terms == null || newTerms.getVersion() > terms.getVersion()) {

-        terms = newTerms;

-        isChanged = true;

-      }

-    }

-    if (isChanged) onTermUpdates(newTerms);

-  }

-

-  private void onTermUpdates(ShardTerms newTerms) {

-    synchronized (listeners) {

-      listeners.removeIf(coreTermWatcher -> !coreTermWatcher.onTermChanged(newTerms));

-    }

-  }

-}

+/*
+ * 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.solr.cloud;
+
+import java.lang.invoke.MethodHandles;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.solr.client.solrj.cloud.ShardTerms;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.SolrZkClient;
+import org.apache.solr.common.cloud.ZkStateReader;
+import org.apache.solr.common.util.ObjectReleaseTracker;
+import org.apache.solr.common.util.Utils;
+import org.apache.solr.core.CoreDescriptor;
+import org.apache.zookeeper.CreateMode;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.Watcher;
+import org.apache.zookeeper.data.Stat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class used for interact with a ZK term node.
+ * Each ZK term node relates to a shard of a collection and have this format (in json)
+ * <p>
+ * <code>
+ * {
+ *   "replicaNodeName1" : 1,
+ *   "replicaNodeName2" : 2,
+ *   ..
+ * }
+ * </code>
+ * <p>
+ * The values correspond to replicas are called terms.
+ * Only replicas with highest term value are considered up to date and be able to become leader and serve queries.
+ * <p>
+ * Terms can only updated in two strict ways:
+ * <ul>
+ * <li>A replica sets its term equals to leader's term
+ * <li>The leader increase its term and some other replicas by 1
+ * </ul>
+ * This class should not be reused after {@link org.apache.zookeeper.Watcher.Event.KeeperState#Expired} event
+ */
+public class ZkShardTerms implements AutoCloseable{
+
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private final Object writingLock = new Object();
+  private final String collection;
+  private final String shard;
+  private final String znodePath;
+  private final SolrZkClient zkClient;
+  private final Set<CoreTermWatcher> listeners = new HashSet<>();
+  private final AtomicBoolean isClosed = new AtomicBoolean(false);
+
+  private ShardTerms terms;
+
+  // Listener of a core for shard's term change events
+  interface CoreTermWatcher {
+    // return true if the listener wanna to be triggered in the next time
+    boolean onTermChanged(ShardTerms terms);
+  }
+
+  public ZkShardTerms(String collection, String shard, SolrZkClient zkClient) {
+    this.znodePath = ZkStateReader.COLLECTIONS_ZKNODE + "/" + collection + "/terms/" + shard;
+    this.collection = collection;
+    this.shard = shard;
+    this.zkClient = zkClient;
+    ensureTermNodeExist();
+    refreshTerms();
+    retryRegisterWatcher();
+    ObjectReleaseTracker.track(this);
+  }
+
+  /**
+   * Ensure that leader's term is higher than some replica's terms
+   * @param leader coreNodeName of leader
+   * @param replicasNeedingRecovery set of replicas in which their terms should be lower than leader's term
+   */
+  public void ensureTermsIsHigher(String leader, Set<String> replicasNeedingRecovery) {
+    if (replicasNeedingRecovery.isEmpty()) return;
+
+    ShardTerms newTerms;
+    while( (newTerms = terms.increaseTerms(leader, replicasNeedingRecovery)) != null) {
+      if (forceSaveTerms(newTerms)) return;
+    }
+  }
+
+  public ShardTerms getShardTerms() {
+    return terms;
+  }
+  /**
+   * Can this replica become leader?
+   * @param coreNodeName of the replica
+   * @return true if this replica can become leader, false if otherwise
+   */
+  public boolean canBecomeLeader(String coreNodeName) {
+    return terms.canBecomeLeader(coreNodeName);
+  }
+
+  /**
+   * Should leader skip sending updates to this replica?
+   * @param coreNodeName of the replica
+   * @return true if this replica has term equals to leader's term, false if otherwise
+   */
+  public boolean skipSendingUpdatesTo(String coreNodeName) {
+    return !terms.haveHighestTermValue(coreNodeName);
+  }
+
+  /**
+   * Did this replica registered its term? This is a sign to check f
+   * @param coreNodeName of the replica
+   * @return true if this replica registered its term, false if otherwise
+   */
+  public boolean registered(String coreNodeName) {
+    return terms.getTerm(coreNodeName) != null;
+  }
+
+  public void close() {
+    // no watcher will be registered
+    isClosed.set(true);
+    synchronized (listeners) {
+      listeners.clear();
+    }
+    ObjectReleaseTracker.release(this);
+  }
+
+  // package private for testing, only used by tests
+  Map<String, Long> getTerms() {
+    synchronized (writingLock) {
+      return terms.getTerms();
+    }
+  }
+
+  /**
+   * Add a listener so the next time the shard's term get updated, listeners will be called
+   */
+  void addListener(CoreTermWatcher listener) {
+    synchronized (listeners) {
+      listeners.add(listener);
+    }
+  }
+
+  /**
+   * Remove the coreNodeName from terms map and also remove any expired listeners
+   * @return Return true if this object should not be reused
+   */
+  boolean removeTerm(CoreDescriptor cd) {
+    int numListeners;
+    synchronized (listeners) {
+      // solrcore already closed
+      listeners.removeIf(coreTermWatcher -> !coreTermWatcher.onTermChanged(terms));
+      numListeners = listeners.size();
+    }
+    return removeTerm(cd.getCloudDescriptor().getCoreNodeName()) || numListeners == 0;
+  }
+
+  // package private for testing, only used by tests
+  // return true if this object should not be reused
+  boolean removeTerm(String coreNodeName) {
+    ShardTerms newTerms;
+    while ( (newTerms = terms.removeTerm(coreNodeName)) != null) {
+      try {
+        if (saveTerms(newTerms)) return false;
+      } catch (KeeperException.NoNodeException e) {
+        return true;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Register a replica's term (term value will be 0).
+   * If a term is already associate with this replica do nothing
+   * @param coreNodeName of the replica
+   */
+  void registerTerm(String coreNodeName) {
+    ShardTerms newTerms;
+    while ( (newTerms = terms.registerTerm(coreNodeName)) != null) {
+      if (forceSaveTerms(newTerms)) break;
+    }
+  }
+
+  /**
+   * Set a replica's term equals to leader's term, and remove recovering flag of a replica.
+   * This call should only be used by {@link org.apache.solr.common.params.CollectionParams.CollectionAction#FORCELEADER}
+   * @param coreNodeName of the replica
+   */
+  public void setTermEqualsToLeader(String coreNodeName) {
+    ShardTerms newTerms;
+    while ( (newTerms = terms.setTermEqualsToLeader(coreNodeName)) != null) {
+      if (forceSaveTerms(newTerms)) break;
+    }
+  }
+
+  public void setTermToZero(String coreNodeName) {
+    ShardTerms newTerms;
+    while ( (newTerms = terms.setTermToZero(coreNodeName)) != null) {
+      if (forceSaveTerms(newTerms)) break;
+    }
+  }
+
+  /**
+   * Mark {@code coreNodeName} as recovering
+   */
+  public void startRecovering(String coreNodeName) {
+    ShardTerms newTerms;
+    while ( (newTerms = terms.startRecovering(coreNodeName)) != null) {
+      if (forceSaveTerms(newTerms)) break;
+    }
+  }
+
+  /**
+   * Mark {@code coreNodeName} as finished recovering
+   */
+  public void doneRecovering(String coreNodeName) {
+    ShardTerms newTerms;
+    while ( (newTerms = terms.doneRecovering(coreNodeName)) != null) {
+      if (forceSaveTerms(newTerms)) break;
+    }
+  }
+
+  public boolean isRecovering(String name) {
+    return terms.isRecovering(name);
+  }
+
+  /**
+   * When first updates come in, all replicas have some data now,
+   * so we must switch from term 0 (registered) to 1 (have some data)
+   */
+  public void ensureHighestTermsAreNotZero() {
+    ShardTerms newTerms;
+    while ( (newTerms = terms.ensureHighestTermsAreNotZero()) != null) {
+      if (forceSaveTerms(newTerms)) break;
+    }
+  }
+
+  public long getHighestTerm() {
+    return terms.getMaxTerm();
+  }
+
+  public long getTerm(String coreNodeName) {
+    Long term = terms.getTerm(coreNodeName);
+    return term == null? -1 : term;
+  }
+
+  // package private for testing, only used by tests
+  int getNumListeners() {
+    synchronized (listeners) {
+      return listeners.size();
+    }
+  }
+
+  /**
+   * Set new terms to ZK.
+   * In case of correspond ZK term node is not created, create it
+   * @param newTerms to be set
+   * @return true if terms is saved successfully to ZK, false if otherwise
+   */
+  private boolean forceSaveTerms(ShardTerms newTerms) {
+    try {
+      return saveTerms(newTerms);
+    } catch (KeeperException.NoNodeException e) {
+      ensureTermNodeExist();
+      return false;
+    }
+  }
+
+  /**
+   * Set new terms to ZK, the version of new terms must match the current ZK term node
+   * @param newTerms to be set
+   * @return true if terms is saved successfully to ZK, false if otherwise
+   * @throws KeeperException.NoNodeException correspond ZK term node is not created
+   */
+  private boolean saveTerms(ShardTerms newTerms) throws KeeperException.NoNodeException {
+    byte[] znodeData = Utils.toJSON(newTerms);
+    try {
+      Stat stat = zkClient.setData(znodePath, znodeData, newTerms.getVersion(), true);
+      setNewTerms(new ShardTerms(newTerms, stat.getVersion()));
+      log.info("Successful update of terms at {} to {}", znodePath, newTerms);
+      return true;
+    } catch (KeeperException.BadVersionException e) {
+      log.info("Failed to save terms, version is not a match, retrying");
+      refreshTerms();
+    } catch (KeeperException.NoNodeException e) {
+      throw e;
+    } catch (Exception e) {
+      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error while saving shard term for collection: " + collection, e);
+    }
+    return false;
+  }
+
+  /**
+   * Create correspond ZK term node
+   */
+  private void ensureTermNodeExist() {
+    String path = "/collections/" + collection + "/terms";
+    try {
+      path += "/" + shard;
+
+      try {
+        Map<String,Long> initialTerms = new HashMap<>();
+        zkClient.makePath(path, Utils.toJSON(initialTerms), CreateMode.PERSISTENT, true);
+      } catch (KeeperException.NodeExistsException e) {
+        // it's okay if another beats us creating the node
+      }
+
+    } catch (InterruptedException e) {
+      Thread.interrupted();
+      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
+          "Error creating shard term node in Zookeeper for collection: " + collection, e);
+    } catch (KeeperException e) {
+      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
+          "Error creating shard term node in Zookeeper for collection: " + collection, e);
+    }
+  }
+
+  /**
+   * Fetch latest terms from ZK
+   */
+  public void refreshTerms() {
+    ShardTerms newTerms;
+    try {
+      Stat stat = new Stat();
+      byte[] data = zkClient.getData(znodePath, null, stat, true);
+      newTerms = new ShardTerms((Map<String, Long>) Utils.fromJSON(data), stat.getVersion());
+    } catch (KeeperException e) {
+      Thread.interrupted();
+      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error updating shard term for collection: " + collection, e);
+    } catch (InterruptedException e) {
+      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error updating shard term for collection: " + collection, e);
+    }
+
+    setNewTerms(newTerms);
+  }
+
+  /**
+   * Retry register a watcher to the correspond ZK term node
+   */
+  private void retryRegisterWatcher() {
+    while (!isClosed.get()) {
+      try {
+        registerWatcher();
+        return;
+      } catch (KeeperException.SessionExpiredException | KeeperException.AuthFailedException e) {
+        isClosed.set(true);
+        log.error("Failed watching shard term for collection: {} due to unrecoverable exception", collection, e);
+        return;
+      } catch (KeeperException e) {
+        log.warn("Failed watching shard term for collection: {}, retrying!", collection, e);
+        try {
+          zkClient.getConnectionManager().waitForConnected(zkClient.getZkClientTimeout());
+        } catch (TimeoutException te) {
+          if (Thread.interrupted()) {
+            throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error watching shard term for collection: " + collection, te);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Register a watcher to the correspond ZK term node
+   */
+  private void registerWatcher() throws KeeperException {
+    Watcher watcher = event -> {
+      // session events are not change events, and do not remove the watcher
+      if (Watcher.Event.EventType.None == event.getType()) {
+        return;
+      }
+      retryRegisterWatcher();
+      // Some events may be missed during register a watcher, so it is safer to refresh terms after registering watcher
+      refreshTerms();
+    };
+    try {
+      // exists operation is faster than getData operation
+      zkClient.exists(znodePath, watcher, true);
+    } catch (InterruptedException e) {
+      Thread.interrupted();
+      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error watching shard term for collection: " + collection, e);
+    }
+  }
+
+
+  /**
+   * Atomically update {@link ZkShardTerms#terms} and call listeners
+   * @param newTerms to be set
+   */
+  private void setNewTerms(ShardTerms newTerms) {
+    boolean isChanged = false;
+    synchronized (writingLock) {
+      if (terms == null || newTerms.getVersion() > terms.getVersion()) {
+        terms = newTerms;
+        isChanged = true;
+      }
+    }
+    if (isChanged) onTermUpdates(newTerms);
+  }
+
+  private void onTermUpdates(ShardTerms newTerms) {
+    synchronized (listeners) {
+      listeners.removeIf(coreTermWatcher -> !coreTermWatcher.onTermChanged(newTerms));
+    }
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/highlight/LuceneRegexFragmenter.java b/solr/core/src/java/org/apache/solr/highlight/LuceneRegexFragmenter.java
index 0dc3340..61b6cff 100644
--- a/solr/core/src/java/org/apache/solr/highlight/LuceneRegexFragmenter.java
+++ b/solr/core/src/java/org/apache/solr/highlight/LuceneRegexFragmenter.java
@@ -1,217 +1,217 @@
-/*

- * 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.solr.highlight;

-

-import java.util.ArrayList;

-import java.util.Arrays;

-import java.util.regex.Matcher;

-import java.util.regex.Pattern;

-

-import org.apache.lucene.analysis.TokenStream;

-import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;

-import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;

-import org.apache.lucene.search.highlight.Fragmenter;

-

-/**

- * Fragmenter that tries to produce snippets that "look" like a regular 

- * expression.

- *

- * NOTE: the default for <code>maxAnalyzedChars</code> is much lower for this 

- * fragmenter.  After this limit is exhausted, fragments are produced in the

- * same way as <code>GapFragmenter</code>

- */

-class LuceneRegexFragmenter implements Fragmenter

-{

-  // ** defaults

-  public static final int DEFAULT_FRAGMENT_SIZE = 70;

-  public static final int DEFAULT_INCREMENT_GAP = 50;

-  public static final float DEFAULT_SLOP = 0.6f;

-  public static final int DEFAULT_MAX_ANALYZED_CHARS = 10000;

-

-  // ** settings

-

-  // desired length of fragments, in characters

-  protected int targetFragChars;

-  // increment gap which indicates a new fragment should occur 

-  // (often due to multi-valued fields)

-  protected int incrementGapThreshold;

-  // factor by which we are allowed to bend the frag size (larger or smaller)

-  protected float slop;

-  // analysis limit (ensures we don't waste too much time on long fields)

-  protected int maxAnalyzedChars;

-  // default desirable pattern for text fragments.

-  protected Pattern textRE;

-  

-

-  // ** state

-  protected int currentNumFrags;

-  protected int currentOffset;

-  protected int targetOffset;

-  protected int[] hotspots;

-

-  private PositionIncrementAttribute posIncAtt;

-  private OffsetAttribute offsetAtt;

-

-  // ** other

-  // note: could dynamically change size of sentences extracted to match

-  // target frag size

-  public static final String 

-    DEFAULT_PATTERN_RAW = "[-\\w ,\\n\"']{20,200}";

-  public static final Pattern 

-    DEFAULT_PATTERN = Pattern.compile(DEFAULT_PATTERN_RAW);

-

-

-  public LuceneRegexFragmenter() {

-    this(DEFAULT_FRAGMENT_SIZE, 

-         DEFAULT_INCREMENT_GAP,

-         DEFAULT_SLOP,

-         DEFAULT_MAX_ANALYZED_CHARS);

-  }

-  public LuceneRegexFragmenter(int targetFragChars) {

-    this(targetFragChars, 

-         DEFAULT_INCREMENT_GAP,

-         DEFAULT_SLOP,

-         DEFAULT_MAX_ANALYZED_CHARS);

-  }

-

-  public LuceneRegexFragmenter(int targetFragChars, 

-                               int incrementGapThreshold,

-                               float slop,

-                               int maxAnalyzedChars ) {

-    this(targetFragChars, incrementGapThreshold, slop, maxAnalyzedChars,

-         DEFAULT_PATTERN);

-         

-  }

-

-  public LuceneRegexFragmenter(int targetFragChars, 

-                               int incrementGapThreshold,

-                               float slop,

-                               int maxAnalyzedChars,

-                               Pattern targetPattern) {

-    this.targetFragChars = targetFragChars;

-    this.incrementGapThreshold = incrementGapThreshold;    

-    this.slop = slop;

-    this.maxAnalyzedChars = maxAnalyzedChars;

-    this.textRE = targetPattern;

-  }

-  

-

-  /* (non-Javadoc)

-   * @see org.apache.lucene.search.highlight.TextFragmenter#start(java.lang.String)

-   */

-  @Override

-  public void start(String originalText, TokenStream tokenStream) {

-    currentNumFrags = 1;

-    currentOffset = 0;

-    addHotSpots(originalText);

-    posIncAtt = tokenStream.getAttribute(PositionIncrementAttribute.class);

-    offsetAtt = tokenStream.getAttribute(OffsetAttribute.class);

-  }

-

-  ////////////////////////////////////

-  // pre-analysis

-  ////////////////////////////////////

-

-  protected void addHotSpots(String text) {

-    //System.out.println("hot spotting");

-    ArrayList<Integer> temphs = new ArrayList<>(

-                              text.length() / targetFragChars);

-    Matcher match = textRE.matcher(text);

-    int cur = 0;

-    while(match.find() && cur < maxAnalyzedChars) {

-      int start=match.start(), end=match.end();

-      temphs.add(start);

-      temphs.add(end);

-      cur = end;

-      //System.out.println("Matched " + match.group());

-    }    

-    hotspots = new int[temphs.size()];

-    for(int i = 0; i < temphs.size(); i++) {

-      hotspots[i] = temphs.get(i);

-    }

-    // perhaps not necessary--I don't know if re matches are non-overlapping

-    Arrays.sort(hotspots);

-  }

-

-  ////////////////////////////////////

-  // fragmenting

-  ////////////////////////////////////

-

-  /* (non-Javadoc)

-   * @see org.apache.lucene.search.highlight.TextFragmenter#isNewFragment(org.apache.lucene.analysis.Token)

-   */

-  @Override

-  public boolean isNewFragment()

-  {

-    boolean isNewFrag = false;

-    int minFragLen = (int)((1.0f - slop)*targetFragChars);

-    int endOffset = offsetAtt.endOffset();

-    

-    // ** determin isNewFrag

-    if(posIncAtt.getPositionIncrement() > incrementGapThreshold) {

-      // large position gaps always imply new fragments

-      isNewFrag = true;

-

-    } else if(endOffset - currentOffset < minFragLen) {

-      // we're not in our range of flexibility

-      isNewFrag = false;

-

-    } else if(targetOffset > 0) {

-      // we've already decided on a target

-      isNewFrag = endOffset > targetOffset;

-

-    } else {

-      // we might be able to do something

-      int minOffset = currentOffset + minFragLen;

-      int maxOffset = (int)(currentOffset + (1.0f + slop)*targetFragChars);

-      int hotIndex;

-

-      // look for a close hotspot

-      hotIndex = Arrays.binarySearch(hotspots, endOffset);

-      if(hotIndex < 0) hotIndex = -hotIndex;

-      if(hotIndex >= hotspots.length) {

-        // no more hotspots in this input stream

-        targetOffset = currentOffset + targetFragChars;

-

-      } else if(hotspots[hotIndex] > maxOffset) {

-        // no hotspots within slop

-        targetOffset = currentOffset + targetFragChars;

-

-      } else {

-        // try to find hotspot in slop

-        int goal = hotspots[hotIndex];

-        while(goal < minOffset && hotIndex < hotspots.length) {

-          hotIndex++;

-          goal = hotspots[hotIndex];

-        }        

-        targetOffset = goal <= maxOffset ? goal : currentOffset + targetFragChars;

-      }

-

-      isNewFrag = endOffset > targetOffset;

-    }      

-      

-    // ** operate on isNewFrag

-    if(isNewFrag) {

-        currentNumFrags++;

-        currentOffset = endOffset;

-        targetOffset = -1;

-    }

-    return isNewFrag;

-  }

-  

+/*
+ * 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.solr.highlight;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
+import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
+import org.apache.lucene.search.highlight.Fragmenter;
+
+/**
+ * Fragmenter that tries to produce snippets that "look" like a regular 
+ * expression.
+ *
+ * NOTE: the default for <code>maxAnalyzedChars</code> is much lower for this 
+ * fragmenter.  After this limit is exhausted, fragments are produced in the
+ * same way as <code>GapFragmenter</code>
+ */
+class LuceneRegexFragmenter implements Fragmenter
+{
+  // ** defaults
+  public static final int DEFAULT_FRAGMENT_SIZE = 70;
+  public static final int DEFAULT_INCREMENT_GAP = 50;
+  public static final float DEFAULT_SLOP = 0.6f;
+  public static final int DEFAULT_MAX_ANALYZED_CHARS = 10000;
+
+  // ** settings
+
+  // desired length of fragments, in characters
+  protected int targetFragChars;
+  // increment gap which indicates a new fragment should occur 
+  // (often due to multi-valued fields)
+  protected int incrementGapThreshold;
+  // factor by which we are allowed to bend the frag size (larger or smaller)
+  protected float slop;
+  // analysis limit (ensures we don't waste too much time on long fields)
+  protected int maxAnalyzedChars;
+  // default desirable pattern for text fragments.
+  protected Pattern textRE;
+  
+
+  // ** state
+  protected int currentNumFrags;
+  protected int currentOffset;
+  protected int targetOffset;
+  protected int[] hotspots;
+
+  private PositionIncrementAttribute posIncAtt;
+  private OffsetAttribute offsetAtt;
+
+  // ** other
+  // note: could dynamically change size of sentences extracted to match
+  // target frag size
+  public static final String 
+    DEFAULT_PATTERN_RAW = "[-\\w ,\\n\"']{20,200}";
+  public static final Pattern 
+    DEFAULT_PATTERN = Pattern.compile(DEFAULT_PATTERN_RAW);
+
+
+  public LuceneRegexFragmenter() {
+    this(DEFAULT_FRAGMENT_SIZE, 
+         DEFAULT_INCREMENT_GAP,
+         DEFAULT_SLOP,
+         DEFAULT_MAX_ANALYZED_CHARS);
+  }
+  public LuceneRegexFragmenter(int targetFragChars) {
+    this(targetFragChars, 
+         DEFAULT_INCREMENT_GAP,
+         DEFAULT_SLOP,
+         DEFAULT_MAX_ANALYZED_CHARS);
+  }
+
+  public LuceneRegexFragmenter(int targetFragChars, 
+                               int incrementGapThreshold,
+                               float slop,
+                               int maxAnalyzedChars ) {
+    this(targetFragChars, incrementGapThreshold, slop, maxAnalyzedChars,
+         DEFAULT_PATTERN);
+         
+  }
+
+  public LuceneRegexFragmenter(int targetFragChars, 
+                               int incrementGapThreshold,
+                               float slop,
+                               int maxAnalyzedChars,
+                               Pattern targetPattern) {
+    this.targetFragChars = targetFragChars;
+    this.incrementGapThreshold = incrementGapThreshold;    
+    this.slop = slop;
+    this.maxAnalyzedChars = maxAnalyzedChars;
+    this.textRE = targetPattern;
+  }
+  
+
+  /* (non-Javadoc)
+   * @see org.apache.lucene.search.highlight.TextFragmenter#start(java.lang.String)
+   */
+  @Override
+  public void start(String originalText, TokenStream tokenStream) {
+    currentNumFrags = 1;
+    currentOffset = 0;
+    addHotSpots(originalText);
+    posIncAtt = tokenStream.getAttribute(PositionIncrementAttribute.class);
+    offsetAtt = tokenStream.getAttribute(OffsetAttribute.class);
+  }
+
+  ////////////////////////////////////
+  // pre-analysis
+  ////////////////////////////////////
+
+  protected void addHotSpots(String text) {
+    //System.out.println("hot spotting");
+    ArrayList<Integer> temphs = new ArrayList<>(
+                              text.length() / targetFragChars);
+    Matcher match = textRE.matcher(text);
+    int cur = 0;
+    while(match.find() && cur < maxAnalyzedChars) {
+      int start=match.start(), end=match.end();
+      temphs.add(start);
+      temphs.add(end);
+      cur = end;
+      //System.out.println("Matched " + match.group());
+    }    
+    hotspots = new int[temphs.size()];
+    for(int i = 0; i < temphs.size(); i++) {
+      hotspots[i] = temphs.get(i);
+    }
+    // perhaps not necessary--I don't know if re matches are non-overlapping
+    Arrays.sort(hotspots);
+  }
+
+  ////////////////////////////////////
+  // fragmenting
+  ////////////////////////////////////
+
+  /* (non-Javadoc)
+   * @see org.apache.lucene.search.highlight.TextFragmenter#isNewFragment(org.apache.lucene.analysis.Token)
+   */
+  @Override
+  public boolean isNewFragment()
+  {
+    boolean isNewFrag = false;
+    int minFragLen = (int)((1.0f - slop)*targetFragChars);
+    int endOffset = offsetAtt.endOffset();
+    
+    // ** determin isNewFrag
+    if(posIncAtt.getPositionIncrement() > incrementGapThreshold) {
+      // large position gaps always imply new fragments
+      isNewFrag = true;
+
+    } else if(endOffset - currentOffset < minFragLen) {
+      // we're not in our range of flexibility
+      isNewFrag = false;
+
+    } else if(targetOffset > 0) {
+      // we've already decided on a target
+      isNewFrag = endOffset > targetOffset;
+
+    } else {
+      // we might be able to do something
+      int minOffset = currentOffset + minFragLen;
+      int maxOffset = (int)(currentOffset + (1.0f + slop)*targetFragChars);
+      int hotIndex;
+
+      // look for a close hotspot
+      hotIndex = Arrays.binarySearch(hotspots, endOffset);
+      if(hotIndex < 0) hotIndex = -hotIndex;
+      if(hotIndex >= hotspots.length) {
+        // no more hotspots in this input stream
+        targetOffset = currentOffset + targetFragChars;
+
+      } else if(hotspots[hotIndex] > maxOffset) {
+        // no hotspots within slop
+        targetOffset = currentOffset + targetFragChars;
+
+      } else {
+        // try to find hotspot in slop
+        int goal = hotspots[hotIndex];
+        while(goal < minOffset && hotIndex < hotspots.length) {
+          hotIndex++;
+          goal = hotspots[hotIndex];
+        }        
+        targetOffset = goal <= maxOffset ? goal : currentOffset + targetFragChars;
+      }
+
+      isNewFrag = endOffset > targetOffset;
+    }      
+      
+    // ** operate on isNewFrag
+    if(isNewFrag) {
+        currentNumFrags++;
+        currentOffset = endOffset;
+        targetOffset = -1;
+    }
+    return isNewFrag;
+  }
+  
 }
\ No newline at end of file
diff --git a/solr/server/scripts/cloud-scripts/zkcli.bat b/solr/server/scripts/cloud-scripts/zkcli.bat
index 8b10b19..0dcf817 100644
--- a/solr/server/scripts/cloud-scripts/zkcli.bat
+++ b/solr/server/scripts/cloud-scripts/zkcli.bat
@@ -1,21 +1,21 @@
-@echo off

-REM You can override pass the following parameters to this script:

-REM 

-

-set JVM=java

-

-REM  Find location of this script

-

-set SDIR=%~dp0

-if "%SDIR:~-1%"=="\" set SDIR=%SDIR:~0,-1%

-

-set "LOG4J_CONFIG=file:///%SDIR%\..\..\resources\log4j2-console.xml"

-

-REM Settings for ZK ACL

-REM set SOLR_ZK_CREDS_AND_ACLS=-DzkACLProvider=org.apache.solr.common.cloud.VMParamsAllAndReadonlyDigestZkACLProvider ^

-REM  -DzkCredentialsProvider=org.apache.solr.common.cloud.VMParamsSingleSetCredentialsDigestZkCredentialsProvider ^

-REM  -DzkDigestUsername=admin-user -DzkDigestPassword=CHANGEME-ADMIN-PASSWORD ^

-REM  -DzkDigestReadonlyUsername=readonly-user -DzkDigestReadonlyPassword=CHANGEME-READONLY-PASSWORD

-

-"%JVM%" %SOLR_ZK_CREDS_AND_ACLS% %ZKCLI_JVM_FLAGS% -Dlog4j.configurationFile="%LOG4J_CONFIG%" ^

--classpath "%SDIR%\..\..\solr-webapp\webapp\WEB-INF\lib\*;%SDIR%\..\..\lib\ext\*;%SDIR%\..\..\lib\*" org.apache.solr.cloud.ZkCLI %*

+@echo off
+REM You can override pass the following parameters to this script:
+REM 
+
+set JVM=java
+
+REM  Find location of this script
+
+set SDIR=%~dp0
+if "%SDIR:~-1%"=="\" set SDIR=%SDIR:~0,-1%
+
+set "LOG4J_CONFIG=file:///%SDIR%\..\..\resources\log4j2-console.xml"
+
+REM Settings for ZK ACL
+REM set SOLR_ZK_CREDS_AND_ACLS=-DzkACLProvider=org.apache.solr.common.cloud.VMParamsAllAndReadonlyDigestZkACLProvider ^
+REM  -DzkCredentialsProvider=org.apache.solr.common.cloud.VMParamsSingleSetCredentialsDigestZkCredentialsProvider ^
+REM  -DzkDigestUsername=admin-user -DzkDigestPassword=CHANGEME-ADMIN-PASSWORD ^
+REM  -DzkDigestReadonlyUsername=readonly-user -DzkDigestReadonlyPassword=CHANGEME-READONLY-PASSWORD
+
+"%JVM%" %SOLR_ZK_CREDS_AND_ACLS% %ZKCLI_JVM_FLAGS% -Dlog4j.configurationFile="%LOG4J_CONFIG%" ^
+-classpath "%SDIR%\..\..\solr-webapp\webapp\WEB-INF\lib\*;%SDIR%\..\..\lib\ext\*;%SDIR%\..\..\lib\*" org.apache.solr.cloud.ZkCLI %*