diff --git a/client/WEB-INF/web.xml b/client/WEB-INF/web.xml
index 1af38e14535..6509a97545a 100644
--- a/client/WEB-INF/web.xml
+++ b/client/WEB-INF/web.xml
@@ -58,10 +58,15 @@
registerCompleteServlet
com.cloud.servlet.RegisterCompleteServlet
-
-
- apiServlet
- /api/*
+
+
+ staticResources
+ com.cloud.servlet.StaticResourceServlet
+
+
+
+ apiServlet
+ /api/*
@@ -73,4 +78,11 @@
registerCompleteServlet
/mycloud/complete
+
+
+ staticResources
+ *.css
+ *.html
+ *.js
+
diff --git a/client/pom.xml b/client/pom.xml
index 5038e662db5..5c56d08a6c4 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -358,6 +358,23 @@
+
+ com.googlecode.todomap
+ maven-jettygzip-plugin
+ 0.0.4
+
+ ${project.build.directory}/generated-webapp
+ ${project.build.directory}/generated-webapp
+
+
+
+ prepare-package
+
+ process
+
+
+
+
org.apache.maven.plugins
maven-war-plugin
diff --git a/server/src/com/cloud/servlet/StaticResourceServlet.java b/server/src/com/cloud/servlet/StaticResourceServlet.java
new file mode 100644
index 00000000000..8e96732e4ff
--- /dev/null
+++ b/server/src/com/cloud/servlet/StaticResourceServlet.java
@@ -0,0 +1,115 @@
+// 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 com.cloud.servlet;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Serves static resources with support for gzip compression and content
+ * caching.
+ */
+public class StaticResourceServlet extends HttpServlet {
+
+ private static final long serialVersionUID = -8833228931973461812L;
+
+ private File getRequestedFile(final HttpServletRequest req) {
+ return new File(getServletContext().getRealPath(req.getServletPath()));
+ }
+
+ @Override
+ protected void doGet(final HttpServletRequest req,
+ final HttpServletResponse resp) throws ServletException,
+ IOException {
+ final File requestedFile = getRequestedFile(req);
+ if (!requestedFile.exists() || !requestedFile.isFile()) {
+ resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ final String etag = getEtag(requestedFile);
+ if (etag.equals(req.getHeader("If-None-Match"))) {
+ resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+ return;
+ }
+ // have to send data, either compressed or the original
+ final File compressedStatic = getCompressedVersion(requestedFile);
+ InputStream fileContent = null;
+ try {
+ resp.setContentType(getContentType(requestedFile.getName()));
+ resp.setHeader("ETag", etag);
+ resp.setStatus(HttpServletResponse.SC_OK);
+ if (isClientCompressionSupported(req) && compressedStatic.exists()) {
+ // gzip compressed
+ resp.setHeader("Content-Encoding", "gzip");
+ resp.setContentLength((int) compressedStatic.length());
+ fileContent = new FileInputStream(compressedStatic);
+ } else {
+ // uncompressed
+ resp.setContentLength((int) requestedFile.length());
+ fileContent = new FileInputStream(requestedFile);
+ }
+ IOUtils.copy(fileContent, resp.getOutputStream());
+ } finally {
+ IOUtils.closeQuietly(fileContent);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ static final Map contentTypes = Collections
+ .unmodifiableMap(new HashMap() {
+ {
+ put("css", "text/css");
+ put("svg", "image/svg+xml");
+ put("js", "application/javascript");
+ put("htm", "text/html");
+ put("html", "text/html");
+ put("txt", "text/plain");
+ put("xml", "text/xml");
+ }
+ });
+
+ static String getContentType(final String fileName) {
+ return contentTypes.get(StringUtils.lowerCase(StringUtils
+ .substringAfterLast(fileName, ".")));
+ }
+
+ static File getCompressedVersion(final File requestedFile) {
+ return new File(requestedFile.getAbsolutePath() + ".gz");
+ }
+
+ static boolean isClientCompressionSupported(final HttpServletRequest req) {
+ return StringUtils.contains(req.getHeader("Accept-Encoding"), "gzip");
+ }
+
+ static String getEtag(final File resource) {
+ return "W/\"" + resource.length() + "-" + resource.lastModified();
+ }
+
+}
diff --git a/server/test/com/cloud/servlet/StaticResourceServletTest.java b/server/test/com/cloud/servlet/StaticResourceServletTest.java
new file mode 100644
index 00000000000..ae5c3847f5b
--- /dev/null
+++ b/server/test/com/cloud/servlet/StaticResourceServletTest.java
@@ -0,0 +1,235 @@
+// 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 com.cloud.servlet;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Matchers;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+public class StaticResourceServletTest {
+
+ File rootDirectory;
+
+ @Before
+ public void setupFiles() throws IOException {
+ rootDirectory = new File("target/tmp");
+ rootDirectory.mkdirs();
+ final File webInf = new File(rootDirectory, "WEB-INF");
+ webInf.mkdirs();
+ final File dir = new File(rootDirectory, "dir");
+ dir.mkdirs();
+ final File indexHtml = new File(rootDirectory, "index.html");
+ indexHtml.createNewFile();
+ FileUtils.writeStringToFile(indexHtml, "index.html");
+ final File defaultCss = new File(rootDirectory, "default.css");
+ defaultCss.createNewFile();
+ final File defaultCssGziped = new File(rootDirectory, "default.css.gz");
+ defaultCssGziped.createNewFile();
+ }
+
+ @After
+ public void cleanupFiles() {
+ FileUtils.deleteQuietly(rootDirectory);
+ }
+
+ // negative tests
+
+ @Test
+ public void testNoSuchFile() throws ServletException, IOException {
+ final StaticResourceServlet servlet = Mockito
+ .mock(StaticResourceServlet.class);
+ Mockito.doCallRealMethod()
+ .when(servlet)
+ .doGet(Matchers.any(HttpServletRequest.class),
+ Matchers.any(HttpServletResponse.class));
+ final ServletContext servletContext = Mockito
+ .mock(ServletContext.class);
+ Mockito.when(servletContext.getRealPath("notexisting.css")).thenReturn(
+ new File(rootDirectory, "notexisting.css").getAbsolutePath());
+ Mockito.when(servlet.getServletContext()).thenReturn(servletContext);
+
+ final HttpServletRequest request = Mockito
+ .mock(HttpServletRequest.class);
+ Mockito.when(request.getServletPath()).thenReturn("notexisting.css");
+ final HttpServletResponse response = Mockito
+ .mock(HttpServletResponse.class);
+ servlet.doGet(request, response);
+ Mockito.verify(response).setStatus(HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ @Test
+ public void testDirectory() throws ServletException, IOException {
+ final HttpServletResponse response = doGetTest("dir");
+ Mockito.verify(response).setStatus(HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ @Test
+ public void testWebInf() throws ServletException, IOException {
+ final HttpServletResponse response = doGetTest("WEB-INF/web.xml");
+ Mockito.verify(response).setStatus(HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ // positive tests
+
+ @Test
+ public void testNotCompressedFile() throws ServletException, IOException {
+ final HttpServletResponse response = doGetTest("index.html");
+ Mockito.verify(response).setStatus(HttpServletResponse.SC_OK);
+ Mockito.verify(response).setContentType("text/html");
+ Mockito.verify(response, Mockito.times(0)).setHeader(
+ "Content-Encoding", "gzip");
+ }
+
+ @Test
+ public void testCompressedFile() throws ServletException, IOException {
+ final HashMap headers = new HashMap();
+ headers.put("Accept-Encoding", "gzip");
+ final HttpServletResponse response = doGetTest("default.css", headers);
+ Mockito.verify(response).setStatus(HttpServletResponse.SC_OK);
+ Mockito.verify(response).setContentType("text/css");
+ Mockito.verify(response, Mockito.times(1)).setHeader(
+ "Content-Encoding", "gzip");
+ }
+
+ @Test
+ public void testCompressedFileWithoutBrowserSupport()
+ throws ServletException, IOException {
+ final HashMap headers = new HashMap();
+ headers.put("Accept-Encoding", "");
+ final HttpServletResponse response = doGetTest("default.css", headers);
+ Mockito.verify(response).setStatus(HttpServletResponse.SC_OK);
+ Mockito.verify(response).setContentType("text/css");
+ Mockito.verify(response, Mockito.times(0)).setHeader(
+ "Content-Encoding", "gzip");
+ }
+
+ @Test
+ public void testWithEtag() throws ServletException, IOException {
+ final HashMap headers = new HashMap();
+ headers.put("If-None-Match", StaticResourceServlet.getEtag(new File(
+ rootDirectory, "default.css")));
+ final HttpServletResponse response = doGetTest("default.css", headers);
+ Mockito.verify(response).setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+ }
+
+ @Test
+ public void testWithEtagOutdated() throws ServletException, IOException {
+ final HashMap headers = new HashMap();
+ headers.put("If-None-Match", "NO-GOOD-ETAG");
+ final HttpServletResponse response = doGetTest("default.css", headers);
+ Mockito.verify(response).setStatus(HttpServletResponse.SC_OK);
+ }
+
+ // utility methods
+
+ @Test
+ public void getEtag() {
+ Assert.assertNotNull(StaticResourceServlet.getEtag(new File(
+ rootDirectory, "index.html")));
+ }
+
+ @Test
+ public void getContentType() {
+ Assert.assertEquals("text/plain",
+ StaticResourceServlet.getContentType("foo.txt"));
+ Assert.assertEquals("text/html",
+ StaticResourceServlet.getContentType("index.html"));
+ Assert.assertEquals("text/plain",
+ StaticResourceServlet.getContentType("README.TXT"));
+ }
+
+ @Test
+ public void isClientCompressionSupported() {
+ final HttpServletRequest request = Mockito
+ .mock(HttpServletRequest.class);
+ Mockito.when(request.getHeader("Accept-Encoding")).thenReturn(
+ "gzip, deflate");
+ Assert.assertTrue(StaticResourceServlet
+ .isClientCompressionSupported(request));
+ }
+
+ @Test
+ public void isClientCompressionSupportedWithoutHeader() {
+ final HttpServletRequest request = Mockito
+ .mock(HttpServletRequest.class);
+ Mockito.when(request.getHeader("Accept-Encoding")).thenReturn(null);
+ Assert.assertFalse(StaticResourceServlet
+ .isClientCompressionSupported(request));
+ }
+
+ // test utilities
+ private HttpServletResponse doGetTest(final String uri)
+ throws ServletException, IOException {
+ return doGetTest(uri, Collections. emptyMap());
+ }
+
+ private HttpServletResponse doGetTest(final String uri,
+ final Map headers) throws ServletException,
+ IOException {
+ final StaticResourceServlet servlet = Mockito
+ .mock(StaticResourceServlet.class);
+ Mockito.doCallRealMethod()
+ .when(servlet)
+ .doGet(Matchers.any(HttpServletRequest.class),
+ Matchers.any(HttpServletResponse.class));
+ final ServletContext servletContext = Mockito
+ .mock(ServletContext.class);
+ Mockito.when(servletContext.getRealPath(uri)).thenReturn(
+ new File(rootDirectory, uri).getAbsolutePath());
+ Mockito.when(servlet.getServletContext()).thenReturn(servletContext);
+
+ final HttpServletRequest request = Mockito
+ .mock(HttpServletRequest.class);
+ Mockito.when(request.getServletPath()).thenReturn(uri);
+ Mockito.when(request.getHeader(Matchers.anyString())).thenAnswer(
+ new Answer() {
+
+ @Override
+ public String answer(final InvocationOnMock invocation)
+ throws Throwable {
+ return headers.get(invocation.getArguments()[0]);
+ }
+ });
+ final HttpServletResponse response = Mockito
+ .mock(HttpServletResponse.class);
+ final ServletOutputStream responseBody = Mockito
+ .mock(ServletOutputStream.class);
+ Mockito.when(response.getOutputStream()).thenReturn(responseBody);
+ servlet.doGet(request, response);
+ return response;
+ }
+
+}