GUACAMOLE-1383: Merge avoid double-encoding client identifiers within URLs.

diff --git a/guacamole/pom.xml b/guacamole/pom.xml
index 24cc59e..32f4345 100644
--- a/guacamole/pom.xml
+++ b/guacamole/pom.xml
@@ -154,6 +154,7 @@
                             <excludes>
                                 <exclude>translations/*.json</exclude>
                                 <exclude>index.html</exclude>
+                                <exclude>verifyCachedVersion.js</exclude>
                             </excludes>
                         </resource>
 
@@ -164,6 +165,7 @@
                             <includes>
                                 <include>translations/*.json</include>
                                 <include>index.html</include>
+                                <include>verifyCachedVersion.js</include>
                             </includes>
                         </resource>
 
diff --git a/guacamole/src/main/frontend/src/index.html b/guacamole/src/main/frontend/src/index.html
index 9dab9ba..7f007eb 100644
--- a/guacamole/src/main/frontend/src/index.html
+++ b/guacamole/src/main/frontend/src/index.html
@@ -24,6 +24,7 @@
         <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, target-densitydpi=medium-dpi">
         <meta name="mobile-web-app-capable" content="yes">
         <meta name="apple-mobile-web-app-capable" content="yes">
+        <meta name="build" content="${guacamole.build.identifier}">
         <link rel="icon" type="image/png" href="images/logo-64.png">
         <link rel="icon" type="image/png" sizes="144x144" href="images/logo-144.png">
         <link rel="apple-touch-icon" type="image/png" href="images/logo-144.png">
@@ -34,7 +35,7 @@
         <% } %>
 
         <!-- Extension CSS (must be able to override webapp CSS) -->
-        <link rel="stylesheet" type="text/css" href="app.css?v=${project.version}">
+        <link rel="stylesheet" type="text/css" href="app.css?b=${guacamole.build.identifier}">
 
         <title ng-bind="page.title | translate"></title>
     </head>
@@ -105,7 +106,7 @@
         <script type="text/javascript" src="templates.js"></script>
 
         <!-- Extension JavaScript -->
-        <script type="text/javascript" src="app.js?v=${project.version}"></script>
+        <script type="text/javascript" src="app.js?b=${guacamole.build.identifier}"></script>
 
     </body>
 </html>
diff --git a/guacamole/src/main/frontend/src/verifyCachedVersion.js b/guacamole/src/main/frontend/src/verifyCachedVersion.js
new file mode 100644
index 0000000..db9e144
--- /dev/null
+++ b/guacamole/src/main/frontend/src/verifyCachedVersion.js
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+/**
+ * Automatically reloads the current page and clears relevant browser cache if
+ * the build that produced index.html is different/older than the build that
+ * produced the JavaScript loaded by index.html.
+ *
+ * @private
+ * @param {Location} location
+ *     The Location object representing the URL of the current page.
+ *
+ * @param {Storate} [sessionStorage]
+ *     The Storage object that should optionally be used to avoid reloading the
+ *     current page in a loop if it proves impossible to clear cache.
+ */
+(function verifyCachedVersion(location, sessionStorage) {
+
+    /**
+     * The meta element containing the build identifier of the Guacamole build
+     * that produced index.html.
+     *
+     * @private
+     * @type {HTMLMetaElement}
+     */
+    var buildMeta = document.head.querySelector('meta[name=build]');
+
+    // Verify that index.html came from the same build as this JavaScript file,
+    // forcing a reload if out-of-date
+    if (!buildMeta || buildMeta.content !== '${guacamole.build.identifier}') {
+
+        if (sessionStorage) {
+
+            // Bail out if we have already tried to automatically refresh the
+            // cache but were unsuccessful
+            if (sessionStorage.getItem('reloadedFor') === '${guacamole.build.identifier}') {
+                console.warn('The version of Guacamole cached by your '
+                    + 'browser does not match the version of Guacamole on the '
+                    + 'server. To avoid unexpected errors, please clear your '
+                    + 'browser cache.');
+                return;
+            }
+
+            sessionStorage.setItem('reloadedFor', '${guacamole.build.identifier}');
+
+        }
+
+        // Force refresh of cache by issuing an HTTP request with headers that
+        // request revalidation of cached content
+        var xhr = new XMLHttpRequest();
+        xhr.open('GET', '', true);
+        xhr.setRequestHeader('Cache-Control', 'no-cache');
+        xhr.setRequestHeader('Pragma', 'no-cache');
+
+        xhr.onreadystatechange = function readyStateChanged() {
+
+            // Reload current page when ready (this call to reload MAY be
+            // sufficient in itself to clear cache, but this is not
+            // guaranteed by any standard)
+            if (xhr.readyState === XMLHttpRequest.DONE)
+                location.reload(true);
+
+        };
+
+        xhr.send();
+
+    }
+
+})(window.location, window.sessionStorage);
diff --git a/guacamole/src/main/frontend/webpack.config.js b/guacamole/src/main/frontend/webpack.config.js
index aa337fb..29bb8dd 100644
--- a/guacamole/src/main/frontend/webpack.config.js
+++ b/guacamole/src/main/frontend/webpack.config.js
@@ -121,7 +121,8 @@
             { from: 'fonts/**/*' },
             { from: 'images/**/*' },
             { from: 'layouts/**/*' },
-            { from: 'translations/**/*' }
+            { from: 'translations/**/*' },
+            { from: 'verifyCachedVersion.js' }
         ], {
             context: 'src/'
         }),
diff --git a/guacamole/src/main/java/org/apache/guacamole/CacheRevalidationFilter.java b/guacamole/src/main/java/org/apache/guacamole/CacheRevalidationFilter.java
new file mode 100644
index 0000000..38dd403
--- /dev/null
+++ b/guacamole/src/main/java/org/apache/guacamole/CacheRevalidationFilter.java
@@ -0,0 +1,61 @@
+/*
+ * 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.guacamole;
+
+import java.io.IOException;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Filter that sets the HTTP response headers necessary to request that the
+ * browser always revalidate its cached copy of the response before using that
+ * cached copy.
+ */
+public class CacheRevalidationFilter implements Filter {
+
+    @Override
+    public void init(FilterConfig config) throws ServletException {
+        // No configuration
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response,
+            FilterChain chain) throws IOException, ServletException {
+
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+        httpResponse.addHeader("Cache-Control", "no-cache");
+        httpResponse.addHeader("Pragma", "no-cache");
+        
+        chain.doFilter(request, response);
+        
+    }
+
+    @Override
+    public void destroy() {
+        // Nothing to clean up
+    }
+
+
+}
diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java
index d66d5f4..d5aedb0 100644
--- a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java
+++ b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java
@@ -41,6 +41,7 @@
 import org.apache.guacamole.resource.Resource;
 import org.apache.guacamole.resource.ResourceServlet;
 import org.apache.guacamole.resource.SequenceResource;
+import org.apache.guacamole.resource.WebApplicationResource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -598,6 +599,9 @@
         Collection<Resource> javaScriptResources = new ArrayList<Resource>();
         Collection<Resource> cssResources = new ArrayList<Resource>();
 
+        // Veriffy that the possibly-cached index.html matches the current build
+        javaScriptResources.add(new WebApplicationResource(getServletContext(), "/verifyCachedVersion.js"));
+
         // Load all extensions
         final Set<String> toleratedAuthProviders = getToleratedAuthenticationProviders();
         loadExtensions(javaScriptResources, cssResources, toleratedAuthProviders);
diff --git a/guacamole/src/main/java/org/apache/guacamole/resource/ResourceServlet.java b/guacamole/src/main/java/org/apache/guacamole/resource/ResourceServlet.java
index a6cf973..c61687e 100644
--- a/guacamole/src/main/java/org/apache/guacamole/resource/ResourceServlet.java
+++ b/guacamole/src/main/java/org/apache/guacamole/resource/ResourceServlet.java
@@ -68,6 +68,10 @@
     protected void doHead(HttpServletRequest request, HttpServletResponse response)
             throws ServletException, IOException {
 
+        // Request that the browser revalidate cached data
+        response.addHeader("Cache-Control", "no-cache");
+        response.addHeader("Pragma", "no-cache");
+
         // Set last modified and content type headers
         response.addDateHeader("Last-Modified", resource.getLastModified());
         response.setContentType(resource.getMimeType());
diff --git a/guacamole/src/main/webapp/WEB-INF/web.xml b/guacamole/src/main/webapp/WEB-INF/web.xml
index 52a52a6..ba1f611 100644
--- a/guacamole/src/main/webapp/WEB-INF/web.xml
+++ b/guacamole/src/main/webapp/WEB-INF/web.xml
@@ -28,6 +28,16 @@
         <welcome-file>index.html</welcome-file>
     </welcome-file-list>
 
+    <!-- Request that the browser always revalidate its cached copy of index.html -->
+    <filter>
+        <filter-name>cacheRevalidationFilter</filter-name>
+        <filter-class>org.apache.guacamole.CacheRevalidationFilter</filter-class>
+    </filter>
+    <filter-mapping>
+        <filter-name>cacheRevalidationFilter</filter-name>
+        <url-pattern>/index.html</url-pattern>
+    </filter-mapping>
+
     <!-- Route all requests through Guice -->
     <filter>
         <filter-name>guiceFilter</filter-name>
diff --git a/pom.xml b/pom.xml
index e384ad9..0480d5b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -115,19 +115,37 @@
                 </executions>
             </plugin>
 
-            <!-- Define a "rootlocation" property that can be used to reference
-                the location of the main guacamole-client directory -->
             <plugin>
                 <groupId>org.codehaus.mojo</groupId>
                 <artifactId>build-helper-maven-plugin</artifactId>
                 <version>3.2.0</version>
                 <executions>
+
+                    <!-- Define a "rootlocation" property that can be used to
+                        reference the location of the main guacamole-client
+                        directory -->
                     <execution>
                         <id>define-project-root</id>
                         <goals>
                             <goal>rootlocation</goal>
                         </goals>
                     </execution>
+
+                    <!-- Define a "guacamole.build.identifier" property that
+                        can be used to uniquely identify the current build
+                        relative to previous builds -->
+                    <execution>
+                        <id>define-build-timestamp</id>
+                        <configuration>
+                            <name>guacamole.build.identifier</name>
+                            <timeSource>build</timeSource>
+                            <pattern>yyyyMMddHHmmss</pattern>
+                        </configuration>
+                        <goals>
+                            <goal>timestamp-property</goal>
+                        </goals>
+                    </execution>
+
                 </executions>
             </plugin>