changeset 229:8e7b37db2ede

Allow semantic versioning on API version numbers This patch implements a web filter on all endpoints (except commands service, which uses an API version unlike the others - '/v1') to allow semantic versioning (http://semver.org/), should we choose to go that route. If a client of the web API requests a version with the same major (first) and minor (second) numbers, and an optional patch (third) number that is lower or the same as the current implemented version, then the filter will pass the request on, rewriting the version number to the current implemented number. If the API version requested is too high, then the request fails with a '404 - API version not implemented'. So if we have an implemented version 0.0.1 of the /systems API, then: /systems/0.0.1 /systems/0.0.0 /systems/0.0 will pass, while /systems/ /systems/0 /systems/1 /systems/0.1 /systems/0.0.2
author Simon Tooke <stooke@redhat.com>
date Wed, 16 Aug 2017 09:52:58 -0400
parents 53215157ca8f
children e18145dc173b
files common/core/src/main/java/com/redhat/thermostat/gateway/common/core/servlet/ServiceVersionFilter.java common/core/src/test/java/com/redhat/thermostat/gateway/common/core/servlet/ServiceVersionFilterTest.java services/jvm-gc/src/main/webapp/WEB-INF/web.xml services/jvm-memory/src/main/webapp/WEB-INF/web.xml services/jvms/src/main/webapp/WEB-INF/web.xml services/system-cpu/src/main/webapp/WEB-INF/web.xml services/system-memory/src/main/webapp/WEB-INF/web.xml services/system-network/src/main/webapp/WEB-INF/web.xml services/systems/src/main/webapp/WEB-INF/web.xml services/white-pages/src/main/webapp/WEB-INF/web.xml tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/jvm/gc/JvmGcServiceIntegrationTest.java tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/jvm/memory/JvmMemoryServiceIntegrationTest.java tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/jvms/JvmsServiceIntegrationTest.java tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/system/cpu/SystemCPUIntegrationTest.java tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/system/memory/SystemMemoryIntegrationTest.java tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/system/network/SystemNetworkIntegrationTest.java tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/systems/SystemInfoIntegrationTest.java tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/tests/integration/IntegrationTest.java tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/tests/integration/VersionTestUtil.java tests/test-utils/src/main/java/com/redhat/thermostat/gateway/tests/utils/HttpTestUtil.java
diffstat 20 files changed, 822 insertions(+), 17 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/core/src/main/java/com/redhat/thermostat/gateway/common/core/servlet/ServiceVersionFilter.java	Wed Aug 16 09:52:58 2017 -0400
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.gateway.common.core.servlet;
+
+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.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/*
+ * This filter takes an init parameter 'version', which represents the implemented version of the API.
+ * Incoming requests are checked against that version.
+ * If there is no incoming version - then error
+ * If there is a version, then it must be compatible (via semantic versioning).
+ *
+ * The URI is passed on to the servlet without the version number.
+ *
+ * The web.xml would look like
+ *
+     <servlet>
+        <servlet-name>SystemInfoServlet</servlet-name>
+        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
+        ...
+    </servlet>
+
+    <filter>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <filter-class>com.redhat.thermostat.gateway.common.core.servlet.ServiceVersionFilter</filter-class>
+        <init-param>
+            <param-name>version</param-name>
+            <param-value>0.0.1</param-value>
+        </init-param>
+    </filter>
+
+    <filter-mapping>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
+    <servlet-mapping>
+        <servlet-name>SystemInfoServlet</servlet-name>
+        <url-pattern>/*</url-pattern>
+    </servlet-mapping>
+ */
+public class ServiceVersionFilter implements Filter {
+
+    private int[] implVersion;
+    private String outVersion;
+    private static final Pattern versionRegex = Pattern.compile("^(/[^/]+)/(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|\\d{1,3}\\.\\d{1,3})(/.*)*$");
+
+    @Override
+    public void init(FilterConfig config) throws ServletException {
+        outVersion = '/' + config.getInitParameter("version");
+        implVersion = extractVersion(config.getInitParameter("version"));
+    }
+
+    // separated out for testing purposes
+    Matcher getMatcher(final String uri) {
+        return versionRegex.matcher(uri);
+    }
+
+    @Override
+    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws ServletException, IOException {
+        final HttpServletRequest request = (HttpServletRequest) req;
+        final String requestURI = request.getRequestURI();
+        final Matcher matcher = getMatcher(requestURI);
+
+        if (matcher.find()) {
+
+            final String requestedVersionStr = matcher.group(2); // API version requested
+            final String group3 = matcher.group(3);
+            final String requestedURIPath = group3 != null ? group3 : ""; // rest of URI
+
+            final int[] inVersion;
+            try {
+                inVersion = extractVersion(requestedVersionStr);
+            } catch (NumberFormatException ignored) {
+                HttpServletResponse response = (HttpServletResponse)(res);
+                response.sendError(404, "API version " + requestedVersionStr + " is invalid");
+                return;
+            }
+
+            // assumptions:
+            // all request URLS start with /apiname/version/path
+            // version is an integer, optionally followed with sub version and sub-subversion ('4.8', '5.3', or '0.0.2')
+
+            if (!matchingVersion(inVersion, implVersion)) {
+                HttpServletResponse response = (HttpServletResponse)(res);
+                response.sendError(404, "API version " + requestedVersionStr + " is not implemented");
+                return;
+            }
+
+            // The outURL mustn't contain the prefix, even though it's in the input URL
+            // The version is verified and consumed in this filter
+            final String filteredURI = outVersion + requestedURIPath;
+            // avoid loops if filter didn't change anything - this is never the case since the redesign
+            if (requestedURIPath.equals(filteredURI)) {
+                chain.doFilter(req, res);
+            } else {
+                req.getRequestDispatcher(filteredURI).forward(req, res);
+            }
+        } else {
+            // if the incoming URL doesn't match the regext patterm, it has no version number.
+            HttpServletResponse response = (HttpServletResponse)(res);
+            response.sendError(404, "URI is missing API version");
+        }
+    }
+
+    @Override
+    public void destroy() {
+        //
+    }
+
+    static boolean matchingVersion(final int[] in, final int[] impl) {
+        if (in.length > impl.length)
+            return false;
+        if (in.length < 2)
+            return false;
+        for (int i = 0; i < in.length; i++) {
+            if (i < (in.length - 1)) {
+                if (in[i] != impl[i])
+                    return false;
+            } else {
+                if (in[i] > impl[i])
+                    return false;
+            }
+        }
+        return true;
+    }
+
+    static int[] extractVersion(final String v) {
+        final String[] vsa = v.split("[.]");
+        final int[] va = new int[vsa.length];
+        for (int i = 0; i < vsa.length; i++) {
+            va[i] = Integer.parseInt(vsa[i]);
+        }
+        return va;
+    }
+
+    // for testing
+    int[] getImplVersion() {
+        return implVersion;
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/core/src/test/java/com/redhat/thermostat/gateway/common/core/servlet/ServiceVersionFilterTest.java	Wed Aug 16 09:52:58 2017 -0400
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.gateway.common.core.servlet;
+
+import org.junit.Test;
+
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.regex.Matcher;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class ServiceVersionFilterTest {
+
+    @Test
+    public void testValidFilterInit() throws ServletException {
+
+        final ServiceVersionFilter filter = createFilter("11.22.33");
+
+        final int[] va = filter.getImplVersion();
+
+        assertNotNull(va);
+        assertEquals(3, va.length);
+
+        assertEquals(11, va[0]);
+        assertEquals(22, va[1]);
+        assertEquals(33, va[2]);
+    }
+
+    private void testExtract(final String in, final int o1, final int o2, final int o3) {
+        final int[] oa = ServiceVersionFilter.extractVersion(in);
+        assertEquals(3, oa.length);
+        assertEquals(o1, oa[0]);
+        assertEquals(o2, oa[1]);
+        assertEquals(o3, oa[2]);
+    }
+
+    @Test
+    public void testExtractor() {
+        testExtract("00.0.0", 0, 0, 0);
+        testExtract("11.22.33", 11, 22, 33);
+
+        final int[] oa2 = ServiceVersionFilter.extractVersion("9.4");
+        assertEquals(2, oa2.length);
+        assertEquals(9, oa2[0]);
+        assertEquals(4, oa2[1]);
+
+        final int[] oa1 = ServiceVersionFilter.extractVersion("2345");
+        assertEquals(1, oa1.length);
+        assertEquals(2345, oa1[0]);
+    }
+
+    @Test
+    public void testBadExtract() {
+        try {
+            ServiceVersionFilter.extractVersion("junk");
+            fail("should have caught the syntax error");
+        } catch (NumberFormatException ignored) {
+        }
+    }
+
+    @Test
+    public void testValidNumericMatches() throws ServletException {
+        assertNumericMatch("0.0.5", "0.0.5");
+        assertNumericMatch("0.0.5", "0.0.4");
+        assertNumericMatch("0.0.5", "0.0.0");
+        assertNumericMatch("0.0.5", "0.0");
+        assertNumericMatch("6.3.5", "6.3.5");
+        assertNumericMatch("6.3.5", "6.3.4");
+        assertNumericMatch("6.3.005", "6.003.4");
+        assertNumericMatch("6.3.5", "6.3.0");
+        assertNumericMatch("6.3.5", "6.3");
+    }
+
+    @Test
+    public void testInvalidNumericMatches() throws ServletException {
+        assertNumericMismatch("0.0.5", "0.0.6");
+        assertNumericMismatch("0.0.5", "0.0.100");
+        assertNumericMismatch("0.0.5", "0.0.50");
+        assertNumericMismatch("0.0.5", "0.1");
+        assertNumericMismatch("6.3.5", "6.3.6");
+        assertNumericMismatch("6.3.5", "6.4.4");
+        assertNumericMismatch("6.3.5", "7.0.0");
+        assertNumericMismatch("6.3.5", "6.010");
+    }
+
+    @Test
+    public void testRegExNoEmpty() throws ServletException {
+
+        // these test that at least two numbers are specified
+        assertRegExMismatch("6.3.");
+        assertRegExMismatch("6.");
+        assertRegExMismatch("0.0.");
+        assertRegExMismatch("0.");
+        assertRegExMismatch("..0");
+        assertRegExMismatch("0..0");
+        assertRegExMismatch(".0.0");
+        assertRegExMismatch("0.1.");
+        assertRegExMismatch("1.");
+        assertRegExMismatch("1.1.");
+        assertRegExMismatch("6.4.");
+        assertRegExMismatch("6..0");
+        assertRegExMismatch("010.");
+        assertRegExMismatch(".");
+        assertRegExMismatch("..");
+        assertRegExMismatch("...");
+        assertRegExMismatch(".");
+    }
+
+    @Test
+    public void testRegEx1To3Digits() throws ServletException {
+        assertRegExMismatch("0000.0.5");
+        assertRegExMismatch("0.0000.4");
+        assertRegExMismatch("0.0.0000");
+        assertRegExMismatch("0000.0000");
+        assertRegExMismatch("6.3.0005");
+        assertRegExMismatch("6.0003.4");
+        assertRegExMismatch("0006.003.4");
+        assertRegExMismatch("6.00000000003");
+    }
+
+    @Test
+    public void testAtLeast2Matches() throws ServletException {
+        // these test that at least two numbers are specified
+        assertRegExMismatch("6");
+        assertRegExMismatch("0");
+        assertRegExMismatch("1");
+        assertRegExMismatch("1.");
+        assertRegExMismatch("7");
+    }
+
+    @Test
+    public void testValid() throws ServletException, IOException {
+        ensureValidRewrite("3.4.5", "/api/3.4", "/bar?q=44.55.66");
+        ensureValidRewrite("3.4.5", "/api/3.4.2", "/foo");
+        ensureValidRewrite("3.4.5", "/api/3.4.2", "");
+        ensureValidRewrite("3.4.5", "/api/3.4", "");
+
+
+        ensureValidNoRewrite("3.5.5", "/api/3.5.5", "");
+        ensureValidNoRewrite("3.7.5", "/api/3.7.5", "/");
+        ensureValidNoRewrite("3.7.5", "/api/3.7.5", "/path");
+        ensureValidNoRewrite("3.7.5", "/api/3.7.5", "/newpath?query=/bar/5.6.7");
+    }
+
+    @Test
+    public void testInvalid() throws ServletException, IOException {
+        ensureInvalid("3.5.5", "/api/3");
+        ensureInvalid("3.4.5", "/api/3/");
+
+        ensureInvalid("3.4.5", "/");
+        ensureInvalid("3.4.5", "/fred");
+
+        ensureInvalid("3.4.5", "/api/3.4.6");
+        ensureInvalid("3.4.5", "/api/3.5");
+        ensureInvalid("3.6.5", "/api/4");
+        ensureInvalid("3.4.5", "/api/4.4.2/foo");
+        ensureInvalid("3.4.5", "/api/3.5/bar?q=44.55.66");
+        ensureInvalid("3.4.5", "/api/030/");
+    }
+
+
+    private void assertNumericMatch(final String impl, final String req) throws ServletException {
+        final int[] implV = ServiceVersionFilter.extractVersion(impl);
+        final int[] reqV = ServiceVersionFilter.extractVersion(req);
+        assertTrue(ServiceVersionFilter.matchingVersion(reqV, implV));
+        assertRegExMatch(req);
+    }
+
+    private void assertNumericMismatch(final String impl, final String req) throws ServletException {
+        final int[] implV = ServiceVersionFilter.extractVersion(impl);
+        final int[] reqV = ServiceVersionFilter.extractVersion(req);
+        assertFalse(ServiceVersionFilter.matchingVersion(reqV, implV));
+    }
+
+    private void assertRegExMatch(final String req) throws ServletException {
+        final ServiceVersionFilter filter = createFilter("0.0.0");
+        final Matcher matcher = filter.getMatcher("/thing/" + req);
+        assertTrue(matcher.find());
+    }
+
+    private void assertRegExMismatch(final String req) throws ServletException {
+        final ServiceVersionFilter filter = createFilter("0.0.0");
+        final Matcher matcher = filter.getMatcher("/thing/" + req);
+        assertFalse(matcher.find());
+    }
+
+    // passes if the version number was changed
+    private void ensureValidRewrite(final String implVersion, final String uri, final String path) throws ServletException, IOException {
+        final ServiceVersionFilter filter = createFilter(implVersion);
+        final HttpServletRequest req = mock(HttpServletRequest.class);
+        when(req.getRequestURI()).thenReturn(uri + path);
+        final RequestDispatcher rd = mock(RequestDispatcher.class);
+        when(req.getRequestDispatcher(anyString())).thenReturn(rd);
+        final HttpServletResponse resp = mock(HttpServletResponse.class);
+        final FilterChain chain = mock(FilterChain.class);
+        filter.doFilter(req, resp, chain);
+        verify(req).getRequestDispatcher("/" + implVersion + path);
+        verify(rd).forward(any(ServletRequest.class), any(ServletResponse.class));
+    }
+
+    // passes if the version number was valid and unchanged.
+    // currently we always rewrite; so these pass even if the URL was rewritten
+    private void ensureValidNoRewrite(final String implVersion, final String uri, final String path) throws ServletException, IOException {
+        ensureValidRewrite(implVersion, uri, path);
+    }
+
+    // passes if the version number was invalid
+    private void ensureInvalid(final String implVersion, final String uri) throws ServletException, IOException {
+        final ServiceVersionFilter filter = createFilter(implVersion);
+        HttpServletRequest req = mock(HttpServletRequest.class);
+        when(req.getRequestURI()).thenReturn(uri);
+        HttpServletResponse resp = mock(HttpServletResponse.class);
+        FilterChain chain = mock(FilterChain.class);
+        filter.doFilter(req, resp, chain);
+        verify(resp).sendError(eq(404), anyString());
+    }
+
+    private ServiceVersionFilter createFilter(final String impl) throws ServletException {
+        final FilterConfig cfg = mock(FilterConfig.class);
+        when(cfg.getInitParameter("version")).thenReturn(impl);
+        final ServiceVersionFilter filter = new ServiceVersionFilter();
+        filter.init(cfg);
+        return filter;
+    }
+}
--- a/services/jvm-gc/src/main/webapp/WEB-INF/web.xml	Thu Aug 10 11:56:46 2017 -0400
+++ b/services/jvm-gc/src/main/webapp/WEB-INF/web.xml	Wed Aug 16 09:52:58 2017 -0400
@@ -52,6 +52,18 @@
             </param-value>
         </init-param>
     </servlet>
+    <filter>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <filter-class>com.redhat.thermostat.gateway.common.core.servlet.ServiceVersionFilter</filter-class>
+        <init-param>
+            <param-name>version</param-name>
+            <param-value>0.0.2</param-value>
+        </init-param>
+    </filter>
+    <filter-mapping>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
     <servlet-mapping>
         <servlet-name>JvmGcServlet</servlet-name>
         <url-pattern>/0.0.2/*</url-pattern>
--- a/services/jvm-memory/src/main/webapp/WEB-INF/web.xml	Thu Aug 10 11:56:46 2017 -0400
+++ b/services/jvm-memory/src/main/webapp/WEB-INF/web.xml	Wed Aug 16 09:52:58 2017 -0400
@@ -52,6 +52,18 @@
             </param-value>
         </init-param>
     </servlet>
+    <filter>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <filter-class>com.redhat.thermostat.gateway.common.core.servlet.ServiceVersionFilter</filter-class>
+        <init-param>
+            <param-name>version</param-name>
+            <param-value>0.0.2</param-value>
+        </init-param>
+    </filter>
+    <filter-mapping>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
     <servlet-mapping>
         <servlet-name>JvmMemoryServlet</servlet-name>
         <url-pattern>/0.0.2/*</url-pattern>
--- a/services/jvms/src/main/webapp/WEB-INF/web.xml	Thu Aug 10 11:56:46 2017 -0400
+++ b/services/jvms/src/main/webapp/WEB-INF/web.xml	Wed Aug 16 09:52:58 2017 -0400
@@ -52,6 +52,18 @@
             </param-value>
         </init-param>
     </servlet>
+    <filter>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <filter-class>com.redhat.thermostat.gateway.common.core.servlet.ServiceVersionFilter</filter-class>
+        <init-param>
+            <param-name>version</param-name>
+            <param-value>0.0.1</param-value>
+        </init-param>
+    </filter>
+    <filter-mapping>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
     <servlet-mapping>
         <servlet-name>JvmsServlet</servlet-name>
         <url-pattern>/0.0.1/*</url-pattern>
--- a/services/system-cpu/src/main/webapp/WEB-INF/web.xml	Thu Aug 10 11:56:46 2017 -0400
+++ b/services/system-cpu/src/main/webapp/WEB-INF/web.xml	Wed Aug 16 09:52:58 2017 -0400
@@ -52,6 +52,18 @@
             </param-value>
         </init-param>
     </servlet>
+    <filter>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <filter-class>com.redhat.thermostat.gateway.common.core.servlet.ServiceVersionFilter</filter-class>
+        <init-param>
+            <param-name>version</param-name>
+            <param-value>0.0.1</param-value>
+        </init-param>
+    </filter>
+    <filter-mapping>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
     <servlet-mapping>
         <servlet-name>SystemCPUServlet</servlet-name>
         <url-pattern>/0.0.1/*</url-pattern>
--- a/services/system-memory/src/main/webapp/WEB-INF/web.xml	Thu Aug 10 11:56:46 2017 -0400
+++ b/services/system-memory/src/main/webapp/WEB-INF/web.xml	Wed Aug 16 09:52:58 2017 -0400
@@ -52,6 +52,18 @@
             </param-value>
         </init-param>
     </servlet>
+    <filter>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <filter-class>com.redhat.thermostat.gateway.common.core.servlet.ServiceVersionFilter</filter-class>
+        <init-param>
+            <param-name>version</param-name>
+            <param-value>0.0.1</param-value>
+        </init-param>
+    </filter>
+    <filter-mapping>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
     <servlet-mapping>
         <servlet-name>SystemMemoryServlet</servlet-name>
         <url-pattern>/0.0.1/*</url-pattern>
--- a/services/system-network/src/main/webapp/WEB-INF/web.xml	Thu Aug 10 11:56:46 2017 -0400
+++ b/services/system-network/src/main/webapp/WEB-INF/web.xml	Wed Aug 16 09:52:58 2017 -0400
@@ -52,6 +52,18 @@
             </param-value>
         </init-param>
     </servlet>
+    <filter>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <filter-class>com.redhat.thermostat.gateway.common.core.servlet.ServiceVersionFilter</filter-class>
+        <init-param>
+            <param-name>version</param-name>
+            <param-value>0.0.1</param-value>
+        </init-param>
+    </filter>
+    <filter-mapping>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
     <servlet-mapping>
         <servlet-name>SystemNetworkServlet</servlet-name>
         <url-pattern>/0.0.1/*</url-pattern>
--- a/services/systems/src/main/webapp/WEB-INF/web.xml	Thu Aug 10 11:56:46 2017 -0400
+++ b/services/systems/src/main/webapp/WEB-INF/web.xml	Wed Aug 16 09:52:58 2017 -0400
@@ -52,6 +52,18 @@
             </param-value>
         </init-param>
     </servlet>
+    <filter>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <filter-class>com.redhat.thermostat.gateway.common.core.servlet.ServiceVersionFilter</filter-class>
+        <init-param>
+            <param-name>version</param-name>
+            <param-value>0.0.1</param-value>
+        </init-param>
+    </filter>
+    <filter-mapping>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
     <servlet-mapping>
         <servlet-name>SystemInfoServlet</servlet-name>
         <url-pattern>/0.0.1/*</url-pattern>
--- a/services/white-pages/src/main/webapp/WEB-INF/web.xml	Thu Aug 10 11:56:46 2017 -0400
+++ b/services/white-pages/src/main/webapp/WEB-INF/web.xml	Wed Aug 16 09:52:58 2017 -0400
@@ -54,6 +54,18 @@
             </param-value>
         </init-param>
     </servlet>
+    <filter>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <filter-class>com.redhat.thermostat.gateway.common.core.servlet.ServiceVersionFilter</filter-class>
+        <init-param>
+            <param-name>version</param-name>
+            <param-value>0.0.1</param-value>
+        </init-param>
+    </filter>
+    <filter-mapping>
+        <filter-name>ServiceVersionFilter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
     <servlet-mapping>
         <servlet-name>WhitePagesHttpHandler</servlet-name>
         <url-pattern>/0.0.1/*</url-pattern>
--- a/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/jvm/gc/JvmGcServiceIntegrationTest.java	Thu Aug 10 11:56:46 2017 -0400
+++ b/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/jvm/gc/JvmGcServiceIntegrationTest.java	Wed Aug 16 09:52:58 2017 -0400
@@ -205,6 +205,29 @@
     }
 
     @Test
+    public void testGetNoPatchNumber() throws InterruptedException, TimeoutException, ExecutionException {
+        String data ="[{\"a\":\"test\",\"b\":\"test1\",\"c\":\"test2\"}, {\"b\":\"test1\"},"+
+                "{\"e\":\"test4\",\"b\":\"test1\"}]";
+
+        // {"response":["{\"a\":\"test\",\"b\":\"test1\",\"c\":\"test2\"}"],
+        //"metaData":{"payloadCount":1,"count":3,
+        //"next":"http://127.0.0.1:30000/jvm-gc/0.0.2?o=1&l=1&q===test1&m=true"}}
+        String expectedResponse ="{\"response\":[{\"a\":\"test\",\"b\":\"test1\",\"c\":\"test2\"}],"+
+                "\"metaData\":{\"payloadCount\":1,\"count\":3,"+
+                "\"next\":\"" + gcUrl + "?" + OFFSET_PREFIX + "\\u003d1\\u0026" + LIMIT_PREFIX + "\\u003d1\\u0026" + QUERY_PREFIX + "\\u003db\\u003d\\u003dtest1\\u0026" + METADATA_PREFIX  +"\\u003dtrue\"}}";
+
+        makeHttpMethodRequest(HttpMethod.POST,"", data,"application/json","", 200);
+
+        final String url1 =  baseUrl + "/" + serviceName + "/" + "0.0";;
+        makeHttpGetRequest(url1 + '?' + QUERY_PREFIX + "=b==test1&" + METADATA_PREFIX + "=true&" + LIMIT_PREFIX + "=1", expectedResponse, 200);
+        final String url2 =  baseUrl + "/" + serviceName + "/" + "0.0.1";;
+        makeHttpGetRequest(url2 + '?' + QUERY_PREFIX + "=b==test1&" + METADATA_PREFIX + "=true&" + LIMIT_PREFIX + "=1", expectedResponse, 200);
+        final String url3 =  baseUrl + "/" + serviceName + "/" + "0.0.3";;
+        makeHttpGetRequest(url3 + '?' + QUERY_PREFIX + "=b==test1&" + METADATA_PREFIX + "=true&" + LIMIT_PREFIX + "=1", "", 404);
+
+    }
+
+    @Test
     public void testGetWithMetaDataAndOffset() throws InterruptedException, TimeoutException, ExecutionException {
         String data ="[{\"a\":\"test\",\"b\":\"test1\",\"c\":\"test2\"}, {\"b\":\"test1\"},"+
                 "{\"e\":\"test4\",\"b\":\"test1\"}]";
--- a/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/jvm/memory/JvmMemoryServiceIntegrationTest.java	Thu Aug 10 11:56:46 2017 -0400
+++ b/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/jvm/memory/JvmMemoryServiceIntegrationTest.java	Wed Aug 16 09:52:58 2017 -0400
@@ -266,6 +266,24 @@
     }
 
     @Test
+    public void testGetWithVersions() throws InterruptedException, TimeoutException, ExecutionException {
+        String data = "[{\"a\":\"test\",\"b\":\"test1\",\"c\":\"test2\"}, {\"b\":\"test1\"}," +
+                "{\"e\":\"test4\",\"b\":\"test1\"}]";
+
+        String expectedResponse = "{\"response\":[{\"a\":\"test\",\"b\":\"test1\"," +
+                "\"c\":\"test2\"}],\"metaData\":{\"payloadCount\":1,\"count\":3," +
+                "\"next\":\"" + returnedUrl + "?" + OFFSET_PREFIX + "\\u003d1\\u0026" + LIMIT_PREFIX + "\\u003d1\\u0026" + QUERY_PREFIX + "\\u003db\\u003d\\u003dtest1\\u0026" + METADATA_PREFIX + "\\u003dtrue\"}}";
+
+        HttpTestUtil.addRecords(client, resourceUrl, data);
+        final String url1 = baseUrl + "/jvm-memory/0.0";
+        HttpTestUtil.testContentlessResponse(client, HttpMethod.GET, url1 + "?" + QUERY_PREFIX + "=b==test1&" + METADATA_PREFIX + "=true&" + LIMIT_PREFIX + "=1", 200, expectedResponse);
+        final String url2 = baseUrl + "/jvm-memory/0.0.1";
+        HttpTestUtil.testContentlessResponse(client, HttpMethod.GET, url2 + "?" + QUERY_PREFIX + "=b==test1&" + METADATA_PREFIX + "=true&" + LIMIT_PREFIX + "=1", 200, expectedResponse);
+        final String url3 = baseUrl + "/jvm-memory/0.0.3";
+        HttpTestUtil.testContentlessResponse(client, HttpMethod.GET, url3 + "?" + QUERY_PREFIX + "=b==test1&" + METADATA_PREFIX + "=true&" + LIMIT_PREFIX + "=1", 404, null);
+    }
+
+    @Test
     public void testGetWithMetaDataAndOffset() throws InterruptedException, TimeoutException, ExecutionException {
         String data = "[{\"a\":\"test\",\"b\":\"test1\",\"c\":\"test2\"}, {\"b\":\"test1\"}," +
                 "{\"e\":\"test4\",\"b\":\"test1\"}]";
--- a/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/jvms/JvmsServiceIntegrationTest.java	Thu Aug 10 11:56:46 2017 -0400
+++ b/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/jvms/JvmsServiceIntegrationTest.java	Wed Aug 16 09:52:58 2017 -0400
@@ -704,7 +704,7 @@
     }
 
     @Test
-    public void testTreeAllOffset() throws InterruptedException, TimeoutException, ExecutionException {
+    public void testTreeVersions() throws InterruptedException, TimeoutException, ExecutionException {
         String postUrl = jvmsUrl + "/systems/1";
         String treeUrl = jvmsUrl + "/tree";
 
@@ -713,8 +713,7 @@
         assertEquals(200, postResponse.getStatus());
 
         String query = "?aliveOnly=false&offset=1";
-        ContentResponse response = client.newRequest(treeUrl + query).method(HttpMethod.GET).send();
-        assertEquals(200, response.getStatus());
+
         String expected = "{ \"response\" : [{\"systemId\":\"1\", " +
                 "\"jvms\":[{ \"agentId\" : \"aid\", \"jvmId\" : \"jid2\", \"jvmPid\" : 2, " +
                 "\"startTime\" : { \"$numberLong\" : \"1495727607481\" }, " +
@@ -735,6 +734,20 @@
                 "{ \"key\" : \"CYGWIN_MODE\", \"value\" : \"0\" }, { \"key\" : \"COLORTERM\", \"value\" : \"truecolor\" }, " +
                 "{ \"key\" : \"_\", \"value\" : \"/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.131-1.b12.fc24.x86_64/jre/../bin/java\" }], " +
                 "\"uid\" : 1000, \"username\" : \"user\", \"systemId\" : \"1\", \"isAlive\" : false }]}]}";
+
+
+        final String url1 = baseUrl + "/jvms/0.0/tree";
+        ContentResponse response = client.newRequest(url1 + query).method(HttpMethod.GET).send();
+        assertEquals(200, response.getStatus());
         assertEquals(expected, response.getContentAsString());
+
+        final String url2 = baseUrl + "/jvms/0.0.0/tree";
+        response = client.newRequest(url2 + query).method(HttpMethod.GET).send();
+        assertEquals(200, response.getStatus());
+        assertEquals(expected, response.getContentAsString());
+
+        final String url3 = baseUrl + "/jvms/0.0.5/tree";
+        response = client.newRequest(url3 + query).method(HttpMethod.GET).send();
+        assertEquals(404, response.getStatus());
     }
 }
--- a/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/system/cpu/SystemCPUIntegrationTest.java	Thu Aug 10 11:56:46 2017 -0400
+++ b/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/system/cpu/SystemCPUIntegrationTest.java	Wed Aug 16 09:52:58 2017 -0400
@@ -42,12 +42,12 @@
 import com.google.gson.JsonParser;
 
 import com.redhat.thermostat.gateway.tests.integration.MongoIntegrationTest;
+import com.redhat.thermostat.gateway.tests.integration.VersionTestUtil;
 import org.eclipse.jetty.client.api.ContentResponse;
 import org.eclipse.jetty.client.api.Request;
 import org.eclipse.jetty.client.util.StringContentProvider;
 import org.eclipse.jetty.http.HttpHeader;
 import org.eclipse.jetty.http.HttpMethod;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -62,7 +62,8 @@
 public class SystemCPUIntegrationTest extends MongoIntegrationTest {
 
     private static final String collectionName = "cpu-info";
-    private static final String serviceURL = baseUrl + "/system-cpu/0.0.1";
+    private static final String versionString = "0.0.1";
+    private static final String serviceURL = baseUrl + "/system-cpu/" + versionString;
     private static final int HTTP_200_OK = 200;
     private static final int HTTP_404_NOTFOUND = 404;
     private static final String TIMESTAMP_TOKEN = "$TIMESTAMP$";
@@ -188,6 +189,13 @@
         getUnknown(systemid);
     }
 
+    @Test
+    public void testVersions() throws Exception {
+        final String systemid = getRandomSystemId();
+        post(systemid);
+        VersionTestUtil.testAllVersions(baseUrl + "/system-cpu", versionString, "/systems/" + systemid);
+    }
+
     private ContentResponse post(final String systemid) throws InterruptedException, ExecutionException, TimeoutException {
         final Request request = client.newRequest(serviceURL + "/systems/" + systemid);
         request.header(HttpHeader.CONTENT_TYPE, "application/json");
--- a/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/system/memory/SystemMemoryIntegrationTest.java	Thu Aug 10 11:56:46 2017 -0400
+++ b/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/system/memory/SystemMemoryIntegrationTest.java	Wed Aug 16 09:52:58 2017 -0400
@@ -42,12 +42,12 @@
 import com.google.gson.JsonParser;
 
 import com.redhat.thermostat.gateway.tests.integration.MongoIntegrationTest;
+import com.redhat.thermostat.gateway.tests.integration.VersionTestUtil;
 import org.eclipse.jetty.client.api.ContentResponse;
 import org.eclipse.jetty.client.api.Request;
 import org.eclipse.jetty.client.util.StringContentProvider;
 import org.eclipse.jetty.http.HttpHeader;
 import org.eclipse.jetty.http.HttpMethod;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -63,7 +63,8 @@
 public class SystemMemoryIntegrationTest extends MongoIntegrationTest {
     
     private static final String collectionName = "memory-info";
-    private static final String serviceURL = baseUrl + "/system-memory/0.0.1";
+    private static final String versionString = "0.0.1";
+    private static final String serviceURL = baseUrl + "/system-memory/" + versionString;
     private static final int HTTP_200_OK = 200;
     private static final int HTTP_404_NOTFOUND = 404;
     private static final String TIMESTAMP_TOKEN = "$TIMESTAMP$";
@@ -191,6 +192,13 @@
         getUnknown(systemid);
     }
 
+    @Test
+    public void testVersions() throws Exception {
+        final String systemid = getRandomSystemId();
+        post(systemid);
+        VersionTestUtil.testAllVersions(baseUrl + "/system-memory", versionString, "/systems/" + systemid);
+    }
+
     private ContentResponse post(final String systemid) throws InterruptedException, ExecutionException, TimeoutException {
         final Request request = client.newRequest(serviceURL + "/systems/" + systemid);
         request.header(HttpHeader.CONTENT_TYPE, "application/json");
--- a/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/system/network/SystemNetworkIntegrationTest.java	Thu Aug 10 11:56:46 2017 -0400
+++ b/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/system/network/SystemNetworkIntegrationTest.java	Wed Aug 16 09:52:58 2017 -0400
@@ -41,6 +41,7 @@
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
 import com.redhat.thermostat.gateway.tests.integration.MongoIntegrationTest;
+import com.redhat.thermostat.gateway.tests.integration.VersionTestUtil;
 import org.eclipse.jetty.client.api.ContentResponse;
 import org.eclipse.jetty.client.api.Request;
 import org.eclipse.jetty.client.util.StringContentProvider;
@@ -60,7 +61,8 @@
 public class SystemNetworkIntegrationTest extends MongoIntegrationTest {
 
     private static final String collectionName = "network-info";
-    private static final String serviceURL = baseUrl + "/system-network/0.0.1";
+    private static final String versionString = "0.0.1";
+    private static final String serviceURL = baseUrl + "/system-network/" + versionString;
     private static final int HTTP_200_OK = 200;
     private static final int HTTP_404_NOTFOUND = 404;
     private static final String TIMESTAMP_TOKEN = "$TIMESTAMP$";
@@ -187,6 +189,14 @@
         getUnknown(systemid);
     }
 
+
+    @Test
+    public void testVersions() throws Exception {
+        final String systemid = getRandomSystemId();
+        post(systemid);
+        VersionTestUtil.testAllVersions(baseUrl + "/system-network", versionString, "/systems/" + systemid);
+    }
+
     private ContentResponse post(final String systemid) throws InterruptedException, ExecutionException, TimeoutException {
         final Request request = client.newRequest(serviceURL + "/systems/" + systemid);
         request.header(HttpHeader.CONTENT_TYPE, "application/json");
--- a/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/systems/SystemInfoIntegrationTest.java	Thu Aug 10 11:56:46 2017 -0400
+++ b/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/service/systems/SystemInfoIntegrationTest.java	Wed Aug 16 09:52:58 2017 -0400
@@ -41,11 +41,11 @@
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
 import com.redhat.thermostat.gateway.tests.integration.MongoIntegrationTest;
+import com.redhat.thermostat.gateway.tests.integration.VersionTestUtil;
 import org.eclipse.jetty.client.api.ContentResponse;
 import org.eclipse.jetty.client.api.Request;
 import org.eclipse.jetty.client.util.StringContentProvider;
 import org.eclipse.jetty.http.HttpMethod;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -60,7 +60,7 @@
 public class SystemInfoIntegrationTest extends MongoIntegrationTest {
 
     private static final String serviceName = "system-info";
-    private static final String versionNumber = "0.0.1";
+    private static final String versionString = "0.0.1";
     private static final int HTTP_200_OK = 200;
     private static final String CPU_STRING1 = "Intel";
     private static final String CPU_STRING2 = "AMD";
@@ -93,7 +93,7 @@
     }
 
     public SystemInfoIntegrationTest() {
-        super("systems/" + versionNumber, serviceName);
+        super("systems/" + versionString, serviceName);
     }
 
     @Test
@@ -191,6 +191,13 @@
         getUnknownSystemInfo(systemid);
     }
 
+    @Test
+    public void testVersions() throws Exception {
+        final String systemid = getRandomSystemId();
+        postSystemInfo(systemid);
+        VersionTestUtil.testAllVersions(baseUrl + "/systems", versionString, "/systems/" + systemid);
+    }
+
     private ContentResponse postSystemInfo(final String systemid) throws InterruptedException, ExecutionException, TimeoutException {
         final Request request = client.newRequest(resourceUrl + "/systems/" + systemid);
         request.header("Content-Type", "application/json");
--- a/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/tests/integration/IntegrationTest.java	Thu Aug 10 11:56:46 2017 -0400
+++ b/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/tests/integration/IntegrationTest.java	Wed Aug 16 09:52:58 2017 -0400
@@ -87,16 +87,21 @@
 
     @BeforeClass
     public static void beforeClassIntegrationTest() throws Exception {
+        client = createAndStartHttpClient();
+        startServer();
+    }
+
+    public static HttpClient createAndStartHttpClient() throws Exception {
+        final HttpClient theclient;
         if (isTLSEnabled()) {
             SslContextFactory sslFactory = new SslContextFactory();
             sslFactory.setTrustAll(true);
-            client = new HttpClient(sslFactory);
+            theclient = new HttpClient(sslFactory);
         } else {
-            client = new HttpClient();
+            theclient = new HttpClient();
         }
-        client.start();
-
-        startServer();
+        theclient.start();
+        return theclient;
     }
 
     private static boolean isTLSEnabled() {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/integration-tests/src/test/java/com/redhat/thermostat/gateway/tests/integration/VersionTestUtil.java	Wed Aug 16 09:52:58 2017 -0400
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.gateway.tests.integration;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.http.HttpMethod;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class VersionTestUtil {
+
+    public static void testAllVersions(final String url, final String implVersioStr, final String path) throws Exception {
+
+        final int[] implVersion = extractVersion(implVersioStr);
+
+        // must be a string of the form NNN.NNN.NNN
+        assertEquals(3, implVersion.length);
+
+        // no vesrion can be greater than 3 digits
+        assertTrue(implVersion[0] >= 0 && implVersion[0] < 1000);
+        assertTrue(implVersion[1] >= 0 && implVersion[1] < 1000);
+        assertTrue(implVersion[2] >= 0 && implVersion[2] < 1000);
+
+        // test with query string
+        HttpClient client = IntegrationTest.createAndStartHttpClient();
+
+        List<String> goodVersions = makeGoodVersions(implVersion);
+        for (final String v : goodVersions) {
+            testGoodVersion(client, url + '/' + v + path);
+        }
+
+        // both bad URLs and mismatched versions will give a 404,
+        // but the mismatched version will also give a body saying
+        // the API versuion is invalid (syntax error) or unimplemented (usually too high)
+
+        List<String> badVersions = makeBadVersions(implVersion);
+        for (final String v : badVersions) {
+            testInvalidVersion(client, url + '/' + v + path);
+        }
+
+        List<String> highVersions = makeHighVersions(implVersion);
+        for (final String v : highVersions) {
+            testTooHighVersion(client, url + '/' + v + path);
+        }
+
+        client.stop();
+    }
+
+    private static void testInvalidVersion(final HttpClient client, final String url) throws Exception {
+        ContentResponse response = client.newRequest(url).method(HttpMethod.GET).send();
+        assertEquals(response.getStatus(), 404);
+    }
+
+    private static void testTooHighVersion(final HttpClient client, final String url) throws Exception {
+        ContentResponse response = client.newRequest(url).method(HttpMethod.GET).send();
+        assertEquals(response.getStatus(), 404);
+        assertTrue(response.getReason().contains("not implemented"));
+    }
+
+    private static void testGoodVersion(final HttpClient client, final String url) throws Exception {
+        ContentResponse response = client.newRequest(url).method(HttpMethod.GET).send();
+        assertEquals(response.getStatus(), 200);
+    }
+
+    private static List<String> makeBadVersions(final int[] implVersion) {
+        final ArrayList<String> list = new ArrayList<>();
+        list.add("");
+        list.add(".");
+        list.add("..");
+        list.add("...");
+        list.add("0");
+        list.add("000" + implVersion[0] + "." + implVersion[1] + "." + implVersion[2]);
+        list.add("000" + implVersion[0] + ".000" + implVersion[1] + "." + implVersion[2]);
+        list.add("000" + implVersion[0] + "." + implVersion[1] + ".000" + implVersion[2]);
+        list.add("000" + implVersion[0] + ".000" + implVersion[1] + "000." + implVersion[2]);
+        list.add("000" + implVersion[0] + ".");
+        list.add("000" + implVersion[0] + ".000");
+        return list;
+    }
+
+    private static List<String> makeHighVersions(final int[] implVersion) {
+        final ArrayList<String> list = new ArrayList<>();
+        list.add("" + implVersion[0] + "." + implVersion[1] + "." + (implVersion[2] + 1));
+        list.add("" + implVersion[0] + "." + (implVersion[1] + 1));
+        list.add("" + (implVersion[0] + 1) + "." + implVersion[1]);
+        list.add("0.0." + (implVersion[2] + 1));
+        return list;
+    }
+
+    private static List<String> makeGoodVersions(final int[] implVersion) {
+        final ArrayList<String> list = new ArrayList<>();
+        list.add("" + implVersion[0] + "." + implVersion[1] + "." + implVersion[2]);
+        if (implVersion[2] > 0) {
+            list.add("" + implVersion[0] + "." + implVersion[1] + "." + (implVersion[2] - 1));
+        }
+        list.add("" + implVersion[0] + "." + implVersion[1] + ".0");
+        list.add("" + implVersion[0] + "." + implVersion[1] + ".00");
+        list.add("" + implVersion[0] + "." + implVersion[1] + ".000");
+        list.add("" + implVersion[0] + "." + implVersion[1]);
+        return list;
+    }
+
+    private static int[] extractVersion(final String v) {
+        final String[] vsa = v.split("[.]");
+        final int[] va = new int[vsa.length];
+        for (int i = 0; i < vsa.length; i++) {
+            va[i] = Integer.parseInt(vsa[i]);
+        }
+        return va;
+    }
+}
--- a/tests/test-utils/src/main/java/com/redhat/thermostat/gateway/tests/utils/HttpTestUtil.java	Thu Aug 10 11:56:46 2017 -0400
+++ b/tests/test-utils/src/main/java/com/redhat/thermostat/gateway/tests/utils/HttpTestUtil.java	Wed Aug 16 09:52:58 2017 -0400
@@ -76,7 +76,9 @@
             throws InterruptedException, TimeoutException, ExecutionException {
         ContentResponse response = client.newRequest(url).method(httpMethod).send();
         assertEquals(expectedResponseStatus, response.getStatus());
-        assertEquals(expectedResponseContent, response.getContentAsString());
+        if (expectedResponseContent != null) {
+            assertEquals(expectedResponseContent, response.getContentAsString());
+        }
     }
 
     public static void testContentResponse(HttpClient client,