Merge pull request #1182 from kinow/fix-ui-tomcat

JENA-2265: Use window.location.pathname for the Fuseki URL, fix Vue p…
diff --git a/jena-fuseki2/jena-fuseki-ui/src/main.js b/jena-fuseki2/jena-fuseki-ui/src/main.js
index 3824c69..0d5e797 100644
--- a/jena-fuseki2/jena-fuseki-ui/src/main.js
+++ b/jena-fuseki2/jena-fuseki-ui/src/main.js
@@ -26,7 +26,7 @@
 
 Vue.config.productionTip = false
 
-const fusekiService = new FusekiService()
+const fusekiService = new FusekiService(window.location)
 Vue.prototype.$fusekiService = fusekiService
 
 new Vue({
diff --git a/jena-fuseki2/jena-fuseki-ui/src/services/fuseki.service.js b/jena-fuseki2/jena-fuseki-ui/src/services/fuseki.service.js
index 4c94e48..e1a215e 100644
--- a/jena-fuseki2/jena-fuseki-ui/src/services/fuseki.service.js
+++ b/jena-fuseki2/jena-fuseki-ui/src/services/fuseki.service.js
@@ -27,19 +27,40 @@
 const DATASET_COUNT_GRAPH_QUERY_2 = 'select ?g (count(*) as ?count) {graph ?g {?s ?p ?o}} group by ?g'
 
 class FusekiService {
-  constructor () {
+  /**
+   * @param {Location} location
+   */
+  constructor (location) {
     this.isOffline = true
+    this.pathname = location.pathname
+  }
+
+  /**
+   * Gets the Fuseki URL, only the pathname onward. The protocol, server, port, etc,
+   * are left to the browser/JS engine & Vue to choose. Previously we were passing
+   * strings such as `/#/ping$`. But this did not work when the application was
+   * deployed on a Tomcat server, for example, where the base URL could be something
+   * like `http://localhost:8080/jena-fuseki/#/`.
+   *
+   * See RC1 vote thread for 4.4.0 for more: https://lists.apache.org/thread/z3gb5w95oc7c4v0g1jpk9jkxm0l4b7lh
+   *
+   * @param {string} url - Vue route URL
+   * @return {string} a new route URL that includes any pathname in the URL
+   */
+  getFusekiUrl (url) {
+    // modified version from: https://stackoverflow.com/a/24381515
+    return `${this.pathname}/${url}`.replace(/(\/)\/+/g, '$1')
   }
 
   async getServerData () {
-    const response = await axios.get('/$/server')
+    const response = await axios.get(this.getFusekiUrl('/$/server'))
     return response.data
   }
 
   async getServerStatus () {
     const startTime = new Date().getTime()
     try {
-      await axios.get('/$/ping')
+      await axios.get(this.getFusekiUrl('/$/ping'))
       // connection reset?
       if (this.isOffline) {
         BUS.$emit('connection:reset')
@@ -57,19 +78,19 @@
   }
 
   async getDatasetStats (datasetName) {
-    const response = await axios.get(`/$/stats/${datasetName}`)
+    const response = await axios.get(this.getFusekiUrl(`/$/stats/${datasetName}`))
     return response.data
   }
 
   async getDatasetSize (datasetName) {
     const promisesResult = await Promise.all([
       axios
-        .get(`/${datasetName}/sparql`, {
+        .get(this.getFusekiUrl(`/${datasetName}/sparql`), {
           params: {
             query: DATASET_SIZE_QUERY_1
           }
         }),
-      axios.get(`/${datasetName}/sparql`, {
+      axios.get(this.getFusekiUrl(`/${datasetName}/sparql`), {
         params: {
           query: DATASET_SIZE_QUERY_2
         }
@@ -86,7 +107,7 @@
   }
 
   async deleteDataset (datasetName) {
-    await axios.delete(`/$/datasets${datasetName}`)
+    await axios.delete(this.getFusekiUrl(`/$/datasets${datasetName}`))
   }
 
   /**
@@ -99,7 +120,7 @@
    * }>}
    */
   async backupDataset (datasetName) {
-    return await axios.post(`/$/backup${datasetName}`)
+    return await axios.post(this.getFusekiUrl(`/$/backup${datasetName}`))
   }
 
   async createDataset (datasetName, datasetType) {
@@ -114,7 +135,7 @@
       'Content-Type': 'application/x-www-form-urlencoded'
     }
     try {
-      await axios.post('/$/datasets', data, {
+      await axios.post(this.getFusekiUrl('/$/datasets'), data, {
         headers
       })
     } catch (error) {
@@ -131,18 +152,18 @@
   }
 
   async getTasks () {
-    return axios.get('/$/tasks')
+    return axios.get(this.getFusekiUrl('/$/tasks'))
   }
 
   async countGraphsTriples (datasetName) {
     const promisesResult = await Promise.all([
       axios
-        .get(`/${datasetName}/sparql`, {
+        .get(this.getFusekiUrl(`/${datasetName}/sparql`), {
           params: {
             query: DATASET_COUNT_GRAPH_QUERY_1
           }
         }),
-      axios.get(`/${datasetName}/sparql`, {
+      axios.get(this.getFusekiUrl(`/${datasetName}/sparql`), {
         params: {
           query: DATASET_COUNT_GRAPH_QUERY_2
         }
@@ -160,7 +181,7 @@
 
   async fetchGraph (datasetName, graphName) {
     return await axios
-      .get(`/${datasetName}`, {
+      .get(this.getFusekiUrl(`/${datasetName}`), {
         params: {
           graph: graphName
         },
@@ -172,7 +193,7 @@
 
   async saveGraph (datasetName, graphName, code) {
     return await axios
-      .put(`/${datasetName}`, code, {
+      .put(this.getFusekiUrl(`/${datasetName}`), code, {
         params: {
           graph: graphName
         },
diff --git a/jena-fuseki2/jena-fuseki-ui/src/views/dataset/Query.vue b/jena-fuseki2/jena-fuseki-ui/src/views/dataset/Query.vue
index a498f4e..637ff95 100644
--- a/jena-fuseki2/jena-fuseki-ui/src/views/dataset/Query.vue
+++ b/jena-fuseki2/jena-fuseki-ui/src/views/dataset/Query.vue
@@ -201,7 +201,7 @@
             showQueryButton: true,
             resizeable: false,
             requestConfig: {
-              endpoint: `/${vm.datasetName}/sparql`
+              endpoint: this.$fusekiService.getFusekiUrl(`/${vm.datasetName}/sparql`)
             }
           }
         )
diff --git a/jena-fuseki2/jena-fuseki-ui/src/views/dataset/Upload.vue b/jena-fuseki2/jena-fuseki-ui/src/views/dataset/Upload.vue
index b49b0fe..827b1dc 100644
--- a/jena-fuseki2/jena-fuseki-ui/src/views/dataset/Upload.vue
+++ b/jena-fuseki2/jena-fuseki-ui/src/views/dataset/Upload.vue
@@ -270,7 +270,7 @@
     },
     postActionUrl () {
       const params = (this.form.datasetGraphName && this.form.datasetGraphName !== '') ? `?graph=${this.form.datasetGraphName}` : ''
-      return `/${this.datasetName}/data${params}`
+      return this.$fusekiService.getFusekiUrl(`/${this.datasetName}/data${params}`)
     }
   },
 
diff --git a/jena-fuseki2/jena-fuseki-ui/tests/unit/services/fuseki.service.spec.js b/jena-fuseki2/jena-fuseki-ui/tests/unit/services/fuseki.service.spec.js
index 9bc03ad..ed8b121 100644
--- a/jena-fuseki2/jena-fuseki-ui/tests/unit/services/fuseki.service.spec.js
+++ b/jena-fuseki2/jena-fuseki-ui/tests/unit/services/fuseki.service.spec.js
@@ -26,7 +26,9 @@
   let fusekiService
   let clock
   beforeEach(async () => {
-    fusekiService = new FusekiService()
+    fusekiService = new FusekiService({
+      pathname: ''
+    })
     // Let's freeze time! So we always get responses with `0ms` in the message.
     clock = sinon.useFakeTimers(new Date().getTime())
   })
diff --git a/jena-fuseki2/jena-fuseki-ui/vue.config.js b/jena-fuseki2/jena-fuseki-ui/vue.config.js
index 81118fc..f7a44e0 100644
--- a/jena-fuseki2/jena-fuseki-ui/vue.config.js
+++ b/jena-fuseki2/jena-fuseki-ui/vue.config.js
@@ -17,6 +17,7 @@
 const path = require('path')
 
 module.exports = {
+  publicPath: '',
   chainWebpack: config => {
     config
       .plugin('html')