diff --git a/scm-server/pom.xml b/scm-server/pom.xml index abcc6e89f7..e0e85bf818 100644 --- a/scm-server/pom.xml +++ b/scm-server/pom.xml @@ -76,12 +76,6 @@ jsw - - linux-x86-32 - linux-x86-64 - windows-32 - windows-64 - configuration.directory.in.classpath.first diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 40f7fbeb88..ab593f2b9a 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -21,7 +21,7 @@ javax.servlet servlet-api - 2.5 + ${servlet.version} provided @@ -47,6 +47,12 @@ com.sun.jersey jersey-json ${jersey.version} + + + stax-api + stax + + @@ -73,6 +79,12 @@ ${guice.version} + + net.sf.ehcache + ehcache-core + ${ehcache.version} + + @@ -91,8 +103,10 @@ + 2.5 1.4-ea06 2.0 + 2.2.0 diff --git a/scm-webapp/src/main/java/sonia/scm/ContextListener.java b/scm-webapp/src/main/java/sonia/scm/ContextListener.java index fafa0dd144..91c76e4bef 100644 --- a/scm-webapp/src/main/java/sonia/scm/ContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/ContextListener.java @@ -15,6 +15,8 @@ import com.google.inject.servlet.GuiceServletContextListener; import com.google.inject.servlet.ServletModule; import sonia.scm.api.rest.UriExtensionsConfig; +import sonia.scm.filter.GZipFilter; +import sonia.scm.filter.StaticResourceFilter; import sonia.scm.security.Authenticator; import sonia.scm.security.DemoAuthenticator; @@ -36,11 +38,30 @@ public class ContextListener extends GuiceServletContextListener { /** Field description */ - public static final String REST_MAPPING = "/api/rest/*"; + public static final String PATTERN_PAGE = "*.html"; + + /** Field description */ + public static final String PATTERN_RESTAPI = "/api/rest/*"; + + /** Field description */ + public static final String PATTERN_SCRIPT = "*.js"; + + /** Field description */ + public static final String PATTERN_STYLESHEET = "*.css"; /** Field description */ public static final String REST_PACKAGE = "sonia.scm.api.rest"; + /** Field description */ + public static final String[] PATTERN_STATIC_RESOURCES = new String[] { + PATTERN_SCRIPT, + PATTERN_STYLESHEET, "*.jpg", "*.gif", "*.png" }; + + /** Field description */ + public static final String[] PATTERN_COMPRESSABLE = new String[] { + PATTERN_SCRIPT, + PATTERN_STYLESHEET, "*.json", "*.xml", "*.txt" }; + //~--- get methods ---------------------------------------------------------- /** @@ -60,6 +81,12 @@ public class ContextListener extends GuiceServletContextListener bind(Authenticator.class).to(DemoAuthenticator.class); bind(SCMContextProvider.class).toInstance(SCMContext.getContext()); + // filters + filter(PATTERN_PAGE, + PATTERN_STATIC_RESOURCES).through(StaticResourceFilter.class); + filter(PATTERN_PAGE, PATTERN_COMPRESSABLE).through(GZipFilter.class); + + // jersey Map params = new HashMap(); /* @@ -74,7 +101,7 @@ public class ContextListener extends GuiceServletContextListener params.put(ServletContainer.RESOURCE_CONFIG_CLASS, UriExtensionsConfig.class.getName()); params.put(PackagesResourceConfig.PROPERTY_PACKAGES, REST_PACKAGE); - serve(REST_MAPPING).with(GuiceContainer.class, params); + serve(PATTERN_RESTAPI).with(GuiceContainer.class, params); } }); } diff --git a/scm-webapp/src/main/java/sonia/scm/filter/GZipFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/GZipFilter.java new file mode 100644 index 0000000000..29917e6ff1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/filter/GZipFilter.java @@ -0,0 +1,61 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + + + +package sonia.scm.filter; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.inject.Singleton; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * + * @author Sebastian Sdorra + */ +@Singleton +public class GZipFilter extends HttpFilter +{ + + /** + * Method description + * + * + * @param request + * @param response + * @param chain + * + * @throws IOException + * @throws ServletException + */ + @Override + protected void doFilter(HttpServletRequest request, + HttpServletResponse response, FilterChain chain) + throws IOException, ServletException + { + String ae = request.getHeader("accept-encoding"); + + if ((ae != null) && (ae.indexOf("gzip") != -1)) + { + GZipResponseWrapper wrappedResponse = new GZipResponseWrapper(response); + + chain.doFilter(request, wrappedResponse); + wrappedResponse.finishResponse(); + } + else + { + chain.doFilter(request, response); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/filter/GZipResponseStream.java b/scm-webapp/src/main/java/sonia/scm/filter/GZipResponseStream.java new file mode 100644 index 0000000000..f535385088 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/filter/GZipResponseStream.java @@ -0,0 +1,181 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + + + +package sonia.scm.filter; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import java.util.zip.GZIPOutputStream; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; + +/** + * + * @author Sebastian Sdorra + */ +public class GZipResponseStream extends ServletOutputStream +{ + + /** + * Constructs ... + * + * + * @param response + * + * @throws IOException + */ + public GZipResponseStream(HttpServletResponse response) throws IOException + { + super(); + closed = false; + this.response = response; + this.output = response.getOutputStream(); + baos = new ByteArrayOutputStream(); + gzipstream = new GZIPOutputStream(baos); + } + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @throws IOException + */ + @Override + public void close() throws IOException + { + if (closed) + { + throw new IOException("This output stream has already been closed"); + } + + gzipstream.finish(); + + byte[] bytes = baos.toByteArray(); + + response.addHeader("Content-Length", Integer.toString(bytes.length)); + response.addHeader("Content-Encoding", "gzip"); + output.write(bytes); + output.flush(); + output.close(); + closed = true; + } + + /** + * Method description + * + * + * @return + */ + public boolean closed() + { + return (this.closed); + } + + /** + * Method description + * + * + * @throws IOException + */ + @Override + public void flush() throws IOException + { + if (closed) + { + throw new IOException("Cannot flush a closed output stream"); + } + + gzipstream.flush(); + } + + /** + * Method description + * + */ + public void reset() + { + + // noop + } + + /** + * Method description + * + * + * @param b + * + * @throws IOException + */ + @Override + public void write(int b) throws IOException + { + if (closed) + { + throw new IOException("Cannot write to a closed output stream"); + } + + gzipstream.write((byte) b); + } + + /** + * Method description + * + * + * @param b + * + * @throws IOException + */ + @Override + public void write(byte b[]) throws IOException + { + write(b, 0, b.length); + } + + /** + * Method description + * + * + * @param b + * @param off + * @param len + * + * @throws IOException + */ + @Override + public void write(byte b[], int off, int len) throws IOException + { + if (closed) + { + throw new IOException("Cannot write to a closed output stream"); + } + + gzipstream.write(b, off, len); + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + protected ByteArrayOutputStream baos = null; + + /** Field description */ + protected GZIPOutputStream gzipstream = null; + + /** Field description */ + protected boolean closed = false; + + /** Field description */ + protected ServletOutputStream output = null; + + /** Field description */ + protected HttpServletResponse response = null; +} diff --git a/scm-webapp/src/main/java/sonia/scm/filter/GZipResponseWrapper.java b/scm-webapp/src/main/java/sonia/scm/filter/GZipResponseWrapper.java new file mode 100644 index 0000000000..28d09c1ff6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/filter/GZipResponseWrapper.java @@ -0,0 +1,164 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + + + +package sonia.scm.filter; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +/** + * + * @author Sebastian Sdorra + */ +public class GZipResponseWrapper extends HttpServletResponseWrapper +{ + + /** + * Constructs ... + * + * + * @param response + */ + public GZipResponseWrapper(HttpServletResponse response) + { + super(response); + origResponse = response; + } + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @return + * + * @throws IOException + */ + public ServletOutputStream createOutputStream() throws IOException + { + return (new GZipResponseStream(origResponse)); + } + + /** + * Method description + * + */ + public void finishResponse() + { + try + { + if (writer != null) + { + writer.close(); + } + else + { + if (stream != null) + { + stream.close(); + } + } + } + catch (IOException e) {} + } + + /** + * Method description + * + * + * @throws IOException + */ + @Override + public void flushBuffer() throws IOException + { + stream.flush(); + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @return + * + * @throws IOException + */ + @Override + public ServletOutputStream getOutputStream() throws IOException + { + if (writer != null) + { + throw new IllegalStateException("getWriter() has already been called!"); + } + + if (stream == null) + { + stream = createOutputStream(); + } + + return (stream); + } + + /** + * Method description + * + * + * @return + * + * @throws IOException + */ + @Override + public PrintWriter getWriter() throws IOException + { + if (writer != null) + { + return (writer); + } + + if (stream != null) + { + throw new IllegalStateException( + "getOutputStream() has already been called!"); + } + + stream = createOutputStream(); + writer = new PrintWriter(new OutputStreamWriter(stream, "UTF-8")); + + return (writer); + } + + //~--- set methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @param length + */ + @Override + public void setContentLength(int length) {} + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + protected HttpServletResponse origResponse = null; + + /** Field description */ + protected ServletOutputStream stream = null; + + /** Field description */ + protected PrintWriter writer = null; +} diff --git a/scm-webapp/src/main/java/sonia/scm/filter/HttpFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/HttpFilter.java new file mode 100644 index 0000000000..c40623cbf4 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/filter/HttpFilter.java @@ -0,0 +1,99 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + + + +package sonia.scm.filter; + +//~--- JDK imports ------------------------------------------------------------ + +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.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * + * @author Sebastian Sdorra + */ +public abstract class HttpFilter implements Filter +{ + + /** + * Method description + * + * + * @param request + * @param response + * @param chain + * + * @throws IOException + * @throws ServletException + */ + protected abstract void doFilter(HttpServletRequest request, + HttpServletResponse response, + FilterChain chain) + throws IOException, ServletException; + + /** + * Method description + * + */ + @Override + public void destroy() + { + + // do nothing + } + + /** + * Method description + * + * + * @param request + * @param response + * @param chain + * + * @throws IOException + * @throws ServletException + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) + throws IOException, ServletException + { + if ((request instanceof HttpServletRequest) + && (response instanceof HttpServletResponse)) + { + doFilter((HttpServletRequest) request, (HttpServletResponse) response, + chain); + } + else + { + throw new IllegalArgumentException("request is not an http request"); + } + } + + /** + * Method description + * + * + * @param filterConfig + * + * @throws ServletException + */ + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + + // do nothing + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/filter/StaticResourceFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/StaticResourceFilter.java new file mode 100644 index 0000000000..d196a2e9fc --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/filter/StaticResourceFilter.java @@ -0,0 +1,129 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + + + +package sonia.scm.filter; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.inject.Singleton; + +import sonia.scm.util.WebUtil; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.File; +import java.io.IOException; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * + * @author Sebastian Sdorra + */ +@Singleton +public class StaticResourceFilter extends HttpFilter +{ + + /** Field description */ + private static final Logger logger = + Logger.getLogger(StaticResourceFilter.class.getName()); + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param filterConfig + * + * @throws ServletException + */ + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + this.context = filterConfig.getServletContext(); + } + + /** + * Method description + * + * + * @param request + * @param response + * @param chain + * + * @throws IOException + * @throws ServletException + */ + @Override + protected void doFilter(HttpServletRequest request, + HttpServletResponse response, FilterChain chain) + throws IOException, ServletException + { + String uri = request.getRequestURI(); + File resource = getResourceFile(request, uri); + + if (resource.exists()) + { + WebUtil.addETagHeader(response, resource); + WebUtil.addStaticCacheControls(response, WebUtil.TIME_YEAR); + + if (!WebUtil.isModified(request, resource)) + { + if (logger.isLoggable(Level.FINEST)) + { + StringBuilder msg = new StringBuilder("return "); + + msg.append(HttpServletResponse.SC_NOT_MODIFIED); + msg.append(" for ").append(uri); + logger.finest(msg.toString()); + } + + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + } + else + { + chain.doFilter(request, response); + } + } + else + { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + } + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @param request + * @param uri + * + * @return + */ + private File getResourceFile(HttpServletRequest request, String uri) + { + String path = uri.substring(request.getContextPath().length()); + + return new File(context.getRealPath(path)); + } + + //~--- fields --------------------------------------------------------------- + + /** Field description */ + private ServletContext context; +} diff --git a/scm-webapp/src/main/java/sonia/scm/util/WebUtil.java b/scm-webapp/src/main/java/sonia/scm/util/WebUtil.java new file mode 100644 index 0000000000..5355f213bb --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/util/WebUtil.java @@ -0,0 +1,279 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + + + +package sonia.scm.util; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.File; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * + * @author Sebastian Sdorra + */ +public class WebUtil +{ + + /** Field description */ + public static final String CACHE_CONTROL_PREVENT = + "no-cache, must-revalidate"; + + /** Field description */ + public static final String DATE_PREVENT_CACHE = + "Tue, 09 Apr 1985 10:00:00 GMT"; + + /** Field description */ + public static final String HEADER_CACHECONTROL = "Cache-Control"; + + /** Field description */ + public static final String HEADER_ETAG = "Etag"; + + /** Field description */ + public static final String HEADER_EXPIRES = "Expires"; + + /** Field description */ + public static final String HEADER_IFMS = "If-Modified-Since"; + + /** Field description */ + public static final String HEADER_INM = "If-None-Match"; + + /** Field description */ + public static final String HEADER_LASTMODIFIED = "Last-Modified"; + + /** Field description */ + public static final String HEADER_PRAGMA = "Pragma"; + + /** Field description */ + public static final String PRAGMA_NOCACHE = "no-cache"; + + /** Field description */ + public static final long TIME_DAY = 60 * 60 * 24; + + /** Field description */ + public static final long TIME_MONTH = 60 * 60 * 24 * 30; + + /** Field description */ + public static final long TIME_YEAR = 60 * 60 * 24 * 365; + + /** Field description */ + private static final String HTTP_DATE_FORMAT = + "EEE, dd MMM yyyy HH:mm:ss zzz"; + + /** Field description */ + private static final Logger logger = + Logger.getLogger(WebUtil.class.getName()); + + //~--- methods -------------------------------------------------------------- + + /** + * Method description + * + * + * @param response + * @param file + */ + public static void addETagHeader(HttpServletResponse response, File file) + { + response.addHeader(HEADER_ETAG, getETag(file)); + } + + /** + * Method description + * + * + * @param response + */ + public static void addPreventCacheHeaders(HttpServletResponse response) + { + response.addDateHeader(HEADER_LASTMODIFIED, new Date().getTime()); + response.addHeader(HEADER_CACHECONTROL, CACHE_CONTROL_PREVENT); + response.addHeader(HEADER_PRAGMA, PRAGMA_NOCACHE); + response.addHeader(HEADER_EXPIRES, DATE_PREVENT_CACHE); + } + + /** + * Method description + * + * + * @param response + * @param seconds + */ + public static void addStaticCacheControls(HttpServletResponse response, + long seconds) + { + long time = new Date().getTime(); + + response.addDateHeader(HEADER_EXPIRES, time + (seconds * 1000)); + + String cc = "max-age=".concat(Long.toString(seconds)).concat(", public"); + + // use public for https + response.addHeader(HEADER_CACHECONTROL, cc.toString()); + } + + /** + * Method description + * + * + * @param date + * + * @return + */ + public static String formatHttpDate(Date date) + { + return getHttpDateFormat().format(date); + } + + /** + * Method description + * + * + * @param dateString + * + * @return + * + * @throws ParseException + */ + public static Date parseHttpDate(String dateString) throws ParseException + { + return getHttpDateFormat().parse(dateString); + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @param file + * + * @return + */ + public static String getETag(File file) + { + return new StringBuilder("W/\"").append(file.length()).append( + file.lastModified()).append("\"").toString(); + } + + /** + * Method description + * + * + * @return + */ + public static DateFormat getHttpDateFormat() + { + SimpleDateFormat dateFormat = new SimpleDateFormat(HTTP_DATE_FORMAT, + Locale.ENGLISH); + + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + + return dateFormat; + } + + /** + * Method description + * + * + * @param request + * + * @return + */ + public static Date getIfModifiedSinceDate(HttpServletRequest request) + { + Date date = null; + String dateString = request.getHeader(HEADER_IFMS); + + if ((dateString != null) && (dateString.length() > 0)) + { + try + { + date = parseHttpDate(dateString); + } + catch (ParseException ex) + { + if (logger.isLoggable(Level.WARNING)) + { + logger.log(Level.WARNING, null, ex); + } + } + catch (NumberFormatException ex) + { + logger.warning(dateString); + + if (logger.isLoggable(Level.WARNING)) + { + logger.log(Level.WARNING, dateString, ex); + } + } + } + + return date; + } + + /** + * Method description + * + * + * @param request + * + * @return + */ + public static boolean isGzipSupported(HttpServletRequest request) + { + String enc = request.getHeader("Accept-Encoding"); + + return (enc != null) && enc.contains("gzip"); + } + + /** + * Method description + * + * + * @param request + * @param file + * + * @return + */ + public static boolean isModified(HttpServletRequest request, File file) + { + boolean result = true; + Date modifiedSince = getIfModifiedSinceDate(request); + + if ((modifiedSince != null) + && (modifiedSince.getTime() == file.lastModified())) + { + result = false; + } + + if (result) + { + String inmEtag = request.getHeader(HEADER_INM); + + if ((inmEtag != null) && (inmEtag.length() > 0) + && inmEtag.equals(getETag(file))) + { + result = false; + } + } + + return result; + } +} diff --git a/scm-webapp/src/main/webapp/index.html b/scm-webapp/src/main/webapp/index.html index 308063629b..0c09ffaa7f 100644 --- a/scm-webapp/src/main/webapp/index.html +++ b/scm-webapp/src/main/webapp/index.html @@ -17,10 +17,8 @@ - - SCM-WebAPP