[LIVY-468] Reverse proxy support

## What changes were proposed in this pull request?
Implements reverse proxy support, by adding basePath/URL concept, configurable using livy.conf

Resolves: [LIVY-468](https://issues.apache.org/jira/browse/LIVY-468)

## How was this patch tested?
Tested manually with Kong 0.11 and Spark 2.1.0 in cluster mode (using spark dispatcher).

Author: M Wcislo <mwcislo999@gmail.com>

Closes #93 from m-wcislo/LIVY-468_reverse_proxy_support.
diff --git a/conf/livy.conf.template b/conf/livy.conf.template
index 86ca9ab..6f50e2f 100644
--- a/conf/livy.conf.template
+++ b/conf/livy.conf.template
@@ -29,6 +29,10 @@
 # What port to start the server on.
 # livy.server.port = 8998
 
+# What base path ui should work on. By default UI is mounted on "/".
+# E.g.: livy.ui.basePath = /my_livy - result in mounting UI on /my_livy/
+# livy.ui.basePath = ""
+
 # What spark master Livy sessions should use.
 # livy.spark.master = local
 
diff --git a/server/src/main/resources/org/apache/livy/server/ui/static/js/all-sessions.js b/server/src/main/resources/org/apache/livy/server/ui/static/js/all-sessions.js
index 28b08a8..64b06df 100644
--- a/server/src/main/resources/org/apache/livy/server/ui/static/js/all-sessions.js
+++ b/server/src/main/resources/org/apache/livy/server/ui/static/js/all-sessions.js
@@ -48,9 +48,9 @@
 var numBatches = 0;
 
 $(document).ready(function () {
-  var sessionsReq = $.getJSON(location.origin + "/sessions", function(response) {
+  var sessionsReq = $.getJSON(location.origin + prependBasePath("/sessions"), function(response) {
     if (response && response.total > 0) {
-      $("#interactive-sessions").load("/static/html/sessions-table.html .sessions-template", function() {
+      $("#interactive-sessions").load(prependBasePath("/static/html/sessions-table.html .sessions-template"), function() {
         loadSessionsTable(response.sessions);
         $("#interactive-sessions-table").DataTable();
         $('#interactive-sessions [data-toggle="tooltip"]').tooltip();
@@ -59,9 +59,9 @@
     numSessions = response.total;
   });
 
-  var batchesReq = $.getJSON(location.origin + "/batches", function(response) {
+  var batchesReq = $.getJSON(location.origin + prependBasePath("/batches"), function(response) {
     if (response && response.total > 0) {
-      $("#batches").load("/static/html/batches-table.html .sessions-template", function() {
+      $("#batches").load(prependBasePath("/static/html/batches-table.html .sessions-template"), function() {
         loadBatchesTable(response.sessions);
         $("#batches-table").DataTable();
         $('#batches [data-toggle="tooltip"]').tooltip();
diff --git a/server/src/main/resources/org/apache/livy/server/ui/static/js/livy-ui.js b/server/src/main/resources/org/apache/livy/server/ui/static/js/livy-ui.js
index 6eef2de..f2d743a 100644
--- a/server/src/main/resources/org/apache/livy/server/ui/static/js/livy-ui.js
+++ b/server/src/main/resources/org/apache/livy/server/ui/static/js/livy-ui.js
@@ -27,6 +27,8 @@
   '=': '&#x3D;'
 };
 
+var basePath = "";
+
 function escapeHtml(string) {
   return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
     return entityMap[s];
@@ -34,7 +36,7 @@
 }
 
 function uiLink(relativePath, inner) {
-  return anchorLink("/ui/" + relativePath, inner);
+  return anchorLink(prependBasePath("/ui/") + relativePath, inner);
 }
 
 function anchorLink(link, inner) {
@@ -89,10 +91,19 @@
 
 function getPathArray() {
   var pathArr = location.pathname.split("/");
-  pathArr.splice(0, 2);
+  var baseUrlEnd = 2 + (basePath.match(/\//g) || []).length;
+  pathArr.splice(0, baseUrlEnd);
   return pathArr;
 }
 
+function setBasePath(path) {
+  basePath = path;
+}
+
+function prependBasePath(path) {
+  return basePath + path;
+}
+
 $.extend( $.fn.dataTable.defaults, {
   stateSave: true,
 });
diff --git a/server/src/main/resources/org/apache/livy/server/ui/static/js/session-log.js b/server/src/main/resources/org/apache/livy/server/ui/static/js/session-log.js
index a3072f9..b6411a0 100644
--- a/server/src/main/resources/org/apache/livy/server/ui/static/js/session-log.js
+++ b/server/src/main/resources/org/apache/livy/server/ui/static/js/session-log.js
@@ -70,7 +70,7 @@
   var type = pathArr.shift();
   var id = pathArr.shift();
 
-  $.getJSON(location.origin + getLogPath(type, id), {size: -1}, function(response) {
+  $.getJSON(location.origin + prependBasePath(getLogPath(type, id)), {size: -1}, function(response) {
     if (response) {
       $("#session-log").append(parseLog(response.log));
       $('#session-log [data-toggle="tooltip"]').tooltip();
diff --git a/server/src/main/resources/org/apache/livy/server/ui/static/js/session.js b/server/src/main/resources/org/apache/livy/server/ui/static/js/session.js
index 7b85546..9f38719 100644
--- a/server/src/main/resources/org/apache/livy/server/ui/static/js/session.js
+++ b/server/src/main/resources/org/apache/livy/server/ui/static/js/session.js
@@ -80,13 +80,13 @@
 $(document).ready(function () {
   var id = getPathArray().pop();
 
-  $.getJSON(location.origin + "/sessions/" + id, function(response) {
+  $.getJSON(location.origin + prependBasePath("/sessions/") + id, function(response) {
     if (response) {
       appendSummary(response);
 
-      $.getJSON(location.origin + "/sessions/" + id + "/statements", function(statementsRes) {
+      $.getJSON(location.origin + prependBasePath("/sessions/") + id + "/statements", function(statementsRes) {
         if (statementsRes && statementsRes.total_statements > 0) {
-          $("#session-statements").load("/static/html/statements-table.html .statements-template",
+          $("#session-statements").load(prependBasePath("/static/html/statements-table.html .statements-template"),
           function() {
             loadStatementsTable(statementsRes.statements);
             $("#statements-table").DataTable();
diff --git a/server/src/main/scala/org/apache/livy/LivyConf.scala b/server/src/main/scala/org/apache/livy/LivyConf.scala
index 48ea7dd..3f6a86c 100644
--- a/server/src/main/scala/org/apache/livy/LivyConf.scala
+++ b/server/src/main/scala/org/apache/livy/LivyConf.scala
@@ -61,6 +61,7 @@
 
   val SERVER_HOST = Entry("livy.server.host", "0.0.0.0")
   val SERVER_PORT = Entry("livy.server.port", 8998)
+  val SERVER_BASE_PATH = Entry("livy.ui.basePath", "")
 
   val UI_ENABLED = Entry("livy.ui.enabled", true)
 
diff --git a/server/src/main/scala/org/apache/livy/server/LivyServer.scala b/server/src/main/scala/org/apache/livy/server/LivyServer.scala
index f540484..b0b84a2 100644
--- a/server/src/main/scala/org/apache/livy/server/LivyServer.scala
+++ b/server/src/main/scala/org/apache/livy/server/LivyServer.scala
@@ -62,6 +62,7 @@
 
     val host = livyConf.get(SERVER_HOST)
     val port = livyConf.getInt(SERVER_PORT)
+    val basePath = livyConf.get(SERVER_BASE_PATH)
     val multipartConfig = MultipartConfig(
         maxFileSize = Some(livyConf.getLong(LivyConf.FILE_UPLOAD_MAX_SIZE))
       ).toMultipartConfigElement
@@ -198,12 +199,12 @@
             mount(context, batchServlet, "/batches/*")
 
             if (livyConf.getBoolean(UI_ENABLED)) {
-              val uiServlet = new UIServlet
+              val uiServlet = new UIServlet(basePath)
               mount(context, uiServlet, "/ui/*")
               mount(context, staticResourceServlet, "/static/*")
-              mount(context, uiRedirectServlet("/ui/"), "/*")
+              mount(context, uiRedirectServlet(basePath + "/ui/"), "/*")
             } else {
-              mount(context, uiRedirectServlet("/metrics"), "/*")
+              mount(context, uiRedirectServlet(basePath + "/metrics"), "/*")
             }
 
             context.mountMetricsAdminServlet("/metrics")
diff --git a/server/src/main/scala/org/apache/livy/server/ui/UIServlet.scala b/server/src/main/scala/org/apache/livy/server/ui/UIServlet.scala
index 0b0d56a..47d6eae 100644
--- a/server/src/main/scala/org/apache/livy/server/ui/UIServlet.scala
+++ b/server/src/main/scala/org/apache/livy/server/ui/UIServlet.scala
@@ -21,7 +21,7 @@
 
 import org.scalatra.ScalatraServlet
 
-class UIServlet extends ScalatraServlet {
+class UIServlet(val basePath: String) extends ScalatraServlet {
   before() { contentType = "text/html" }
 
   sealed trait Page { val name: String }
@@ -37,14 +37,21 @@
 
   private def getHeader(pageName: String): Seq[Node] =
     <head>
-      <link rel="stylesheet" href="/static/css/bootstrap.min.css" type="text/css"/>
-      <link rel="stylesheet" href="/static/css/dataTables.bootstrap.min.css" type="text/css"/>
-      <link rel="stylesheet" href="/static/css/livy-ui.css" type="text/css"/>
-      <script src="/static/js/jquery-3.2.1.min.js"></script>
-      <script src="/static/js/bootstrap.min.js"></script>
-      <script src="/static/js/jquery.dataTables.min.js"></script>
-      <script src="/static/js/dataTables.bootstrap.min.js"></script>
-      <script src="/static/js/livy-ui.js"></script>
+      <link rel="stylesheet"
+            href={basePath + "/static/css/bootstrap.min.css"}
+            type="text/css"/>
+      <link rel="stylesheet"
+            href={basePath + "/static/css/dataTables.bootstrap.min.css"}
+            type="text/css"/>
+      <link rel="stylesheet" href={basePath + "/static/css/livy-ui.css"} type="text/css"/>
+      <script src={basePath + "/static/js/jquery-3.2.1.min.js"}></script>
+      <script src={basePath + "/static/js/bootstrap.min.js"}></script>
+      <script src={basePath + "/static/js/jquery.dataTables.min.js"}></script>
+      <script src={basePath + "/static/js/dataTables.bootstrap.min.js"}></script>
+      <script src={basePath + "/static/js/livy-ui.js"}></script>
+      <script type="text/javascript">
+        setBasePath({"'" + basePath + "'"});
+      </script>
       <title>Livy - {pageName}</title>
     </head>
 
@@ -52,8 +59,8 @@
     <nav class="navbar navbar-default">
       <div class="container-fluid">
         <div class="navbar-header">
-          <a class="navbar-brand" href="/ui">
-            <img alt="Livy" src="/static/img/livy-mini-logo.png"/>
+          <a class="navbar-brand" href={basePath + "/ui"}>
+            <img alt="Livy" src={basePath + "/static/img/livy-mini-logo.png"}/>
           </a>
         </div>
         <div class="collapse navbar-collapse">
@@ -68,12 +75,14 @@
     val tabs: Seq[Node] = page match {
       case _: AllSessionsPage => <li class="active"><a href="#">Sessions</a></li>
       case sessionPage: SessionPage => {
-        <li><a href="/ui">Sessions</a></li> ++
+        <li><a href={basePath + "/ui"}>Sessions</a></li> ++
           <li class="active"><a href="#">{sessionPage.name}</a></li>
       }
       case logPage: LogPage => {
-        val sessionLink = if (logPage.sessionType == "Session") "/ui/session/" + logPage.id else "#"
-        <li><a href="/ui">Sessions</a></li> ++
+        val sessionLink = if (logPage.sessionType == "Session") {
+          basePath + "/ui/session/" + logPage.id
+        } else "#"
+        <li><a href={basePath + "/ui"}>Sessions</a></li> ++
           <li><a href={sessionLink}>{logPage.sessionName}</a></li> ++
           <li class="active"><a href="#">Log</a></li>
       }
@@ -102,7 +111,7 @@
       <div id="all-sessions">
         <div id="interactive-sessions"></div>
         <div id="batches"></div>
-        <script src="/static/js/all-sessions.js"></script>
+        <script src={basePath + "/static/js/all-sessions.js"}></script>
       </div>
 
     createPage(AllSessionsPage(), content)
@@ -113,7 +122,7 @@
       <div id="session-page">
         <div id="session-summary"></div>
         <div id="session-statements"></div>
-        <script src="/static/js/session.js"></script>
+        <script src={basePath + "/static/js/session.js"}></script>
       </div>
 
     createPage(SessionPage(params("id").toInt), content)
@@ -123,7 +132,7 @@
     val content =
       <div id="log-page">
         <div id="session-log"></div>
-        <script src="/static/js/session-log.js"></script>
+        <script src={basePath + "/static/js/session-log.js"}></script>
       </div>
 
     createPage(page, content)