Index: solr/core/src/test/org/apache/solr/cloud/ZkControllerTest.java
===================================================================
--- solr/core/src/test/org/apache/solr/cloud/ZkControllerTest.java	(revision 1419710)
+++ solr/core/src/test/org/apache/solr/cloud/ZkControllerTest.java	(working copy)
@@ -49,6 +49,62 @@
     initCore();
   }
 
+  public void testNodeNameUrlConversion() throws Exception {
+
+    // nodeName from parts
+    assertEquals("localhost:8888_solr",
+                 ZkController.generateNodeName("localhost", "8888", "solr"));
+    assertEquals("localhost:8888_", // root context
+                 ZkController.generateNodeName("localhost", "8888", ""));
+    assertEquals("foo-bar:77_solr%2Fsub_dir",
+                 ZkController.generateNodeName("foo-bar", "77", "solr/sub_dir"));
+
+    // setup a SolrZkClient to do some getBaseUrlForNodeName testing
+    String zkDir = dataDir.getAbsolutePath() + File.separator
+        + "zookeeper/server1/data";
+
+    ZkTestServer server = new ZkTestServer(zkDir);
+    try {
+      server.run();
+
+      AbstractZkTestCase.tryCleanSolrZkNode(server.getZkHost());
+      AbstractZkTestCase.makeSolrZkNode(server.getZkHost());
+
+      SolrZkClient zkClient = new SolrZkClient(server.getZkAddress(), TIMEOUT);
+      try {
+
+        // getBaseUrlForNodeName
+        assertEquals("http://zzz.xxx:1234/solr",
+                     zkClient.getBaseUrlForNodeName("zzz.xxx:1234_solr"));
+        assertEquals("http://xxx:99/",
+                     zkClient.getBaseUrlForNodeName("xxx:99_"));
+        assertEquals("http://foo-bar.baz.org:9999/some_dir",
+                     zkClient.getBaseUrlForNodeName("foo-bar.baz.org:9999_some_dir"));
+        assertEquals("http://foo-bar.baz.org:9999/solr/sub_dir",
+                     zkClient.getBaseUrlForNodeName("foo-bar.baz.org:9999_solr%2Fsub_dir"));
+        
+        
+        // generateNodeName + getBaseUrlForNodeName
+        assertEquals("http://foo:9876/solr",
+                     zkClient.getBaseUrlForNodeName
+                     (ZkController.generateNodeName("foo","9876","solr")));
+        assertEquals("http://foo.bar.com:9876/solr/sub_dir",
+                     zkClient.getBaseUrlForNodeName
+                     (ZkController.generateNodeName("foo.bar.com","9876","solr/sub_dir")));
+        assertEquals("http://foo-bar:9876/",
+                     zkClient.getBaseUrlForNodeName
+                     (ZkController.generateNodeName("foo-bar","9876","")));
+        assertEquals("http://foo-bar.com:80/some_dir",
+                     zkClient.getBaseUrlForNodeName
+                     (ZkController.generateNodeName("foo-bar.com","80","some_dir")));
+      } finally {
+        zkClient.close();
+      }
+    } finally {
+      server.shutdown();
+    }
+  }
+
   @Test
   public void testReadConfigName() throws Exception {
     String zkDir = dataDir.getAbsolutePath() + File.separator
Index: solr/core/src/test-files/solr/solr.xml
===================================================================
--- solr/core/src/test-files/solr/solr.xml	(revision 1419710)
+++ solr/core/src/test-files/solr/solr.xml	(working copy)
@@ -29,7 +29,7 @@
     If 'null' (or absent), cores will not be manageable via request handler
   -->
   <cores adminPath="/admin/cores" defaultCoreName="collection1" host="127.0.0.1" hostPort="${hostPort:8983}" 
-         hostContext="solr" zkClientTimeout="8000" numShards="${numShards:3}" shareSchema="${shareSchema:false}">
+         hostContext="${hostContext:solr}" zkClientTimeout="8000" numShards="${numShards:3}" shareSchema="${shareSchema:false}">
     <core name="collection1" instanceDir="collection1" shard="${shard:}" collection="${collection:collection1}" config="${solrconfig:solrconfig.xml}" schema="${schema:schema.xml}"/>
   </cores>
 </solr>
Index: solr/core/src/java/org/apache/solr/cloud/OverseerCollectionProcessor.java
===================================================================
--- solr/core/src/java/org/apache/solr/cloud/OverseerCollectionProcessor.java	(revision 1419710)
+++ solr/core/src/java/org/apache/solr/cloud/OverseerCollectionProcessor.java	(working copy)
@@ -256,9 +256,8 @@
           ShardRequest sreq = new ShardRequest();
           params.set("qt", adminPath);
           sreq.purpose = 1;
-          // TODO: this does not work if original url had _ in it
-          // We should have a master list
-          String replica = nodeName.replaceAll("_", "/");
+          String replica = zkStateReader.getZkClient()
+            .getBaseUrlForNodeName(nodeName);
           if (replica.startsWith("http://")) replica = replica.substring(7);
           sreq.shards = new String[] {replica};
           sreq.actualShards = sreq.shards;
Index: solr/core/src/java/org/apache/solr/cloud/ZkController.java
===================================================================
--- solr/core/src/java/org/apache/solr/cloud/ZkController.java	(revision 1419710)
+++ solr/core/src/java/org/apache/solr/cloud/ZkController.java	(working copy)
@@ -19,7 +19,9 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.UnsupportedEncodingException;
 import java.net.InetAddress;
+import java.net.URLEncoder;
 import java.net.NetworkInterface;
 import java.util.Collections;
 import java.util.Enumeration;
@@ -145,18 +147,30 @@
       TimeoutException, IOException {
     if (cc == null) throw new IllegalArgumentException("CoreContainer cannot be null.");
     this.cc = cc;
-    if (localHostContext.contains("/")) {
-      throw new IllegalArgumentException("localHostContext ("
-          + localHostContext + ") should not contain a /");
+    if (localHostContext.startsWith("/")) {
+      // be forgiving and strip this off
+      // this allows us to support users specifying hostContext="/" in 
+      // solr.xml to indicate the root context, instead of hostContext="" 
+      // which means the default of "solr"
+      localHostContext = localHostContext.substring(1);
     }
+    if (localHostContext.endsWith("/")) {
+      // be extra nice
+      localHostContext = localHostContext.substring(0,localHostContext.length()-1);
+    }
     
+    
     this.zkServerAddress = zkServerAddress;
     this.localHostPort = locaHostPort;
     this.localHostContext = localHostContext;
     this.localHost = getHostAddress(localHost);
+    this.baseURL = this.localHost + ":" + this.localHostPort + "/" + this.localHostContext;
+
     this.hostName = getHostNameFromAddress(this.localHost);
-    this.nodeName = this.hostName + ':' + this.localHostPort + '_' + this.localHostContext;
-    this.baseURL = this.localHost + ":" + this.localHostPort + "/" + this.localHostContext;
+    this.nodeName = generateNodeName(this.hostName, 
+                                     this.localHostPort, 
+                                     this.localHostContext);
+
     this.leaderVoteWait = leaderVoteWait;
     this.clientTimeout = zkClientTimeout;
     zkClient = new SolrZkClient(zkServerAddress, zkClientTimeout, zkClientConnectTimeout,
@@ -1243,4 +1257,23 @@
     return cmdDistribExecutor;
   }
 
+  /**
+   * Returns the nodeName that should be used based on the specified properties.
+   *
+   * @param hostName - must not be the empty string
+   * @param hostPort - must consist only of digits, must not be the empty string
+   * @param hostContext - should not begin or end with a slash, may be the empty string to denote the root context
+   * @lucene.experimental
+   * @see SolrZkClient#getBaseUrlForNodeName
+   */
+  static String generateNodeName(final String hostName,
+                                 final String hostPort,
+                                 final String hostContext) {
+    try {
+      return hostName + ':' + hostPort + '_' + 
+        URLEncoder.encode(hostContext, "UTF-8");
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalStateException("JVM Does not seem to support UTF-8", e);
+    }
+  }
 }
Index: solr/solrj/src/test-files/solrj/solr/solr.xml
===================================================================
--- solr/solrj/src/test-files/solrj/solr/solr.xml	(revision 1419710)
+++ solr/solrj/src/test-files/solrj/solr/solr.xml	(working copy)
@@ -28,7 +28,7 @@
   adminPath: RequestHandler path to manage cores.  
     If 'null' (or absent), cores will not be manageable via request handler
   -->
-  <cores adminPath="/admin/cores" defaultCoreName="collection1" host="127.0.0.1" hostPort="${hostPort:8983}" hostContext="solr" zkClientTimeout="8000" numShards="${numShards:3}">
+  <cores adminPath="/admin/cores" defaultCoreName="collection1" host="127.0.0.1" hostPort="${hostPort:8983}" hostContext="${hostContext:}" zkClientTimeout="8000" numShards="${numShards:3}">
     <core name="collection1" instanceDir="collection1" shard="${shard:}" collection="${collection:collection1}" config="${solrconfig:solrconfig.xml}" schema="${schema:schema.xml}"/>
   </cores>
 </solr>
Index: solr/solrj/src/test-files/solrj/solr/shared/solr.xml
===================================================================
--- solr/solrj/src/test-files/solrj/solr/shared/solr.xml	(revision 1419710)
+++ solr/solrj/src/test-files/solrj/solr/shared/solr.xml	(working copy)
@@ -30,7 +30,7 @@
   adminPath: RequestHandler path to manage cores.  
     If 'null' (or absent), cores will not be manageable via REST
   -->
-  <cores adminPath="/admin/cores" defaultCoreName="core0" host="127.0.0.1" hostPort="${hostPort:8983}" hostContext="solr" zkClientTimeout="8000">
+  <cores adminPath="/admin/cores" defaultCoreName="core0" host="127.0.0.1" hostPort="${hostPort:8983}" hostContext="${hostContext:}" zkClientTimeout="8000">
     <core name="collection1" instanceDir="." />
     <core name="core0" instanceDir="${theInstanceDir:./}" collection="${collection:acollection}">
       <property name="version" value="3.5"/>
Index: solr/solrj/src/java/org/apache/solr/common/cloud/SolrZkClient.java
===================================================================
--- solr/solrj/src/java/org/apache/solr/common/cloud/SolrZkClient.java	(revision 1419710)
+++ solr/solrj/src/java/org/apache/solr/common/cloud/SolrZkClient.java	(working copy)
@@ -22,6 +22,7 @@
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
 import java.util.List;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicLong;
@@ -461,6 +462,28 @@
   }
 
   /**
+   * Returns the baseURL corrisponding to a given node's nodeName -- 
+   * NOTE: does not (currently) imply that the nodeName (or resulting 
+   * baseURL) exists in the cluster.
+   * @lucene.experimental
+   */
+  public String getBaseUrlForNodeName(final String nodeName) {
+    final int _offset = nodeName.indexOf("_");
+    if (_offset < 0) {
+      throw new IllegalArgumentException("nodeName does not contain expected '_' seperator: " + nodeName);
+    }
+    final String hostAndPort = nodeName.substring(0,_offset);
+    try {
+      final String path = URLDecoder.decode(nodeName.substring(1+_offset),
+                                            "UTF-8");
+      return "http://" + hostAndPort + "/" + path;
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalStateException("JVM Does not seem to support UTF-8", e);
+    }
+  }
+
+
+  /**
    * Fills string with printout of current ZooKeeper layout.
    */
   public void printLayout(String path, int indent, StringBuilder string)
Index: solr/test-framework/src/java/org/apache/solr/BaseDistributedSearchTestCase.java
===================================================================
--- solr/test-framework/src/java/org/apache/solr/BaseDistributedSearchTestCase.java	(revision 1419710)
+++ solr/test-framework/src/java/org/apache/solr/BaseDistributedSearchTestCase.java	(working copy)
@@ -33,6 +33,7 @@
 import junit.framework.Assert;
 
 import org.apache.lucene.search.FieldCache;
+import org.apache.lucene.util. _TestUtil;
 import org.apache.lucene.util.Constants;
 import org.apache.solr.client.solrj.SolrServer;
 import org.apache.solr.client.solrj.SolrServerException;
@@ -50,6 +51,7 @@
 import org.apache.solr.schema.TrieDateField;
 import org.apache.solr.util.AbstractSolrTestCase;
 import org.junit.BeforeClass;
+import org.junit.AfterClass;
 import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -68,7 +70,101 @@
     assumeFalse("SOLR-4147: ibm 64bit has jvm bugs!", Constants.JRE_IS_64BIT && Constants.JAVA_VENDOR.startsWith("IBM"));
     r = new Random(random().nextLong());
   }
+  
+  /**
+   * Set's the value of the "hostContext" system property to a random path 
+   * like string (which may or may not contain sub-paths).  This is used 
+   * in the default constructor for this test to help ensure no code paths have
+   * hardcoded assumptions about the servlet context used to run solr.
+   * <p>
+   * Test configs may use the <code>${hostContext}</code> variable to access 
+   * this system property.
+   * </p>
+   * @see #BaseDistributedSearchTestCase()
+   * @see #clearHostContext
+   */
+  @BeforeClass
+  public static void initHostContext() {
+    // Can't use randomRealisticUnicodeString because unescaped unicode is 
+    // not allowed in URL paths
+    // Can't use URLEncoder.encode(randomRealisticUnicodeString) because
+    // Jetty freaks out and returns 404's when the context uses escapes
 
+    StringBuilder hostContext = new StringBuilder("/");
+    if (random().nextBoolean()) {
+      // half the time we use the root context, the other half...
+
+      // Remember: randomSimpleString might be the empty string
+      hostContext.append(_TestUtil.randomSimpleString(random(), 2));
+      if (random().nextBoolean()) {
+        hostContext.append("_");
+      }
+      hostContext.append(_TestUtil.randomSimpleString(random(), 3));
+      if ( ! "/".equals(hostContext)) {
+        // if our random string is empty, this might add a trailing slash, 
+        // but our code should be ok with that
+        hostContext.append("/").append(_TestUtil.randomSimpleString(random(), 2));
+      } else {
+        // we got 'lucky' and still just have the root context,
+        // NOOP: don't try to add a subdir to nothing (ie "//" is bad)
+      }
+    }
+
+    log.info("Setting hostContext system property: " + hostContext.toString());
+    System.setProperty("hostContext", hostContext.toString());
+  }
+
+  /**
+   * Clears the "hostContext" system property
+   * @see #initHostContext
+   */
+  @AfterClass
+  public static void clearHostContext() throws Exception {
+    System.clearProperty("hostContext");
+  }
+
+  private static String getHostContextSuitableForServletContext() {
+    String ctx = System.getProperty("hostContext","/solr");
+    if ("".equals(ctx)) ctx = "/solr";
+    if (ctx.endsWith("/")) ctx = ctx.substring(0,ctx.length()-1);;
+    if (!ctx.startsWith("/")) ctx = "/" + ctx;
+    return ctx;
+  }
+
+  /**
+   * Constructs a test in which the jetty+solr instances as well as the 
+   * solr clients all use the value of the "hostContext" system property.
+   * <p>
+   * If the system property is not set, or is set to the empty string 
+   * (neither of which should normally happen unless a subclass explicitly 
+   * modifies the property set by {@link #initHostContext} prior to calling 
+   * this constructor) a servlet context of "/solr" is used. (this is for 
+   * consistency with the default behavior of solr.xml parsing when using 
+   * <code>hostContext="${hostContext:}"</code>
+   * </p>
+   * <p>
+   * If the system property is set to a value which does not begin with a 
+   * "/" (which should normally happen unless a subclass explicitly 
+   * modifies the property set by {@link #initHostContext} prior to calling 
+   * this constructor) a leading "/" will be prepended.
+   * </p>
+   *
+   * @see #initHostContext
+   */
+  protected BaseDistributedSearchTestCase() {
+    this(getHostContextSuitableForServletContext());
+  }
+
+  /**
+   * @param context explicit servlet context path to use (eg: "/solr")
+   */
+  protected BaseDistributedSearchTestCase(final String context) {
+    this.context = context;
+    this.deadServers = new String[] {"[ff01::114]:33332" + context, 
+                                     "[ff01::083]:33332" + context, 
+                                     "[ff01::213]:33332" + context};
+  }
+
   protected int shardCount = 4;      // the actual number of solr cores that will be created in the cluster
 
   /**
@@ -84,12 +180,10 @@
   protected List<SolrServer> clients = new ArrayList<SolrServer>();
   protected List<JettySolrRunner> jettys = new ArrayList<JettySolrRunner>();
   
-  protected String context = "/solr";
+  protected String context;
+  protected String[] deadServers;
   protected String shards;
   protected String[] shardsArr;
-  // Some ISPs redirect to their own web site for domains that don't exist, causing this to fail
-  // protected String[] deadServers = {"does_not_exist_54321.com:33331/solr","127.0.0.1:33332/solr"};
-  protected String[] deadServers = {"[ff01::114]:33332/solr", "[ff01::083]:33332/solr", "[ff01::213]:33332/solr"};
   protected File testDir;
   protected SolrServer controlClient;
 
@@ -258,7 +352,7 @@
   
   public JettySolrRunner createJetty(File solrHome, String dataDir, String shardList, String solrConfigOverride, String schemaOverride) throws Exception {
 
-    JettySolrRunner jetty = new JettySolrRunner(solrHome.getAbsolutePath(), "/solr", 0, solrConfigOverride, schemaOverride);
+    JettySolrRunner jetty = new JettySolrRunner(solrHome.getAbsolutePath(), context, 0, solrConfigOverride, schemaOverride);
     jetty.setShards(shardList);
     jetty.setDataDir(dataDir);
     jetty.start();
Index: solr/test-framework/src/java/org/apache/solr/cloud/AbstractFullDistribZkTestBase.java
===================================================================
--- solr/test-framework/src/java/org/apache/solr/cloud/AbstractFullDistribZkTestBase.java	(revision 1419710)
+++ solr/test-framework/src/java/org/apache/solr/cloud/AbstractFullDistribZkTestBase.java	(working copy)
@@ -359,7 +359,7 @@
   public JettySolrRunner createJetty(String dataDir, String shardList,
       String solrConfigOverride) throws Exception {
     
-    JettySolrRunner jetty = new JettySolrRunner(getSolrHome(), "/solr", 0,
+    JettySolrRunner jetty = new JettySolrRunner(getSolrHome(), context, 0,
         solrConfigOverride, null, false);
     jetty.setShards(shardList);
     jetty.setDataDir(dataDir);
