/* * Copyright 2013 cruxframework.org. * * Licensed 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.cruxframework.crux.core.server.offline; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.nio.charset.Charset; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; import org.apache.commons.lang.StringUtils; import org.cruxframework.crux.core.server.http.GZIPResponseWrapper; import org.cruxframework.crux.core.server.rest.core.CacheControl; import org.cruxframework.crux.core.server.rest.util.DateUtil; import org.cruxframework.crux.core.server.rest.util.HttpHeaderNames; import org.cruxframework.crux.core.server.rest.util.HttpResponseCodes; import org.cruxframework.crux.core.server.rest.util.header.CacheControlHeaderParser; import org.cruxframework.crux.core.utils.RegexpPatterns; import org.cruxframework.crux.core.utils.StreamUtils; /** * @author Thiago da Rosa de Bustamante * */ public class AppcacheFilter implements Filter { private Map<String, Long> lastModifiedDates = Collections.synchronizedMap(new HashMap<String, Long>()); private FilterConfig filterConfig; private long startTime; @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String ifModifiedHeader = request.getHeader(HttpHeaderNames.IF_MODIFIED_SINCE); long dateModified = getDateModified(request); if ((ifModifiedHeader == null) || isFileModified(ifModifiedHeader, dateModified)) { sendRequestedFile(chain, request, response, dateModified); } else { response.setStatus(HttpResponseCodes.SC_NOT_MODIFIED); } } private boolean isFileModified(String ifModifiedHeader, long dateModified) { boolean result = true; try { if (dateModified > 0) { Date date = DateUtil.parseDate(ifModifiedHeader); if (date.getTime() >= dateModified) { result = false; } } } catch (Exception e) { result = true; } return result; } private long getDateModified(HttpServletRequest request) throws IOException { Long result; try { String file = request.getRequestURI(); result = lastModifiedDates.get(file); if (result == null) { String contextPath = filterConfig.getServletContext().getContextPath(); if (StringUtils.isNotBlank(contextPath) && file.startsWith(contextPath)) { file = StringUtils.removeStart(file, contextPath); } InputStream stream = filterConfig.getServletContext().getResourceAsStream(file); if (stream != null) { String content = StreamUtils.readAsUTF8(stream); int indexStart = content.indexOf("# Build Time ["); int indexEnd = content.indexOf("]", indexStart); if (indexStart > 0 && indexEnd > 0) { String dateStr = content.substring(indexStart + 14, indexEnd); result = Long.parseLong(dateStr); lastModifiedDates.put(file, result); } else { result = 0l; } } else { result = 0l; } } } catch (Exception e) { result = 0l; } return result; } private void sendRequestedFile(FilterChain chain, HttpServletRequest request, HttpServletResponse response, long dateModified) throws IOException, ServletException { String ae = request.getHeader(HttpHeaderNames.ACCEPT_ENCODING); if (ae != null && ae.indexOf("gzip") != -1) { response = new GZIPResponseWrapper(response); } response = new ResponseWrapper(response, request.getContextPath()); response.setContentType("text/cache-manifest"); response.setCharacterEncoding("UTF-8"); CacheControl cache = new CacheControl(); cache.setNoCache(true); response.addHeader(HttpHeaderNames.CACHE_CONTROL, CacheControlHeaderParser.toString(cache)); response.addDateHeader(HttpHeaderNames.EXPIRES, System.currentTimeMillis()); if (dateModified > 0) { response.addDateHeader(HttpHeaderNames.LAST_MODIFIED, dateModified); } chain.doFilter(request, response); ((ResponseWrapper)response).finishResponse(); } protected long getStartTime() { return startTime; } @Override public void init(FilterConfig filterConfig) throws ServletException { this.filterConfig = filterConfig; this.startTime = new Date().getTime(); } @Override public void destroy() { } /** * * @author Thiago da Rosa de Bustamante * */ private static class ResponseWrapper extends HttpServletResponseWrapper { private PrintWriter fWriter; private ModifiedOutputStream fOutputStream; private boolean fWriterReturned; private boolean fOutputStreamReturned; private final String context; public ResponseWrapper(HttpServletResponse response, String context) throws IOException { super(response); this.context = context; fOutputStream = new ModifiedOutputStream(response.getOutputStream()); fWriter = new PrintWriter(new OutputStreamWriter(fOutputStream, "UTF-8")); } public final ServletOutputStream getOutputStream() { if (fWriterReturned) { throw new IllegalStateException(); } fOutputStreamReturned = true; return fOutputStream; } public final PrintWriter getWriter() { if (fOutputStreamReturned) { throw new IllegalStateException(); } fWriterReturned = true; return fWriter; } @Override public void flushBuffer() throws IOException { fOutputStream.flush(); } public void finishResponse() { try { if (fWriter != null) { fWriter.close(); } else { if (fOutputStream != null) { fOutputStream.close(); } } } catch (IOException e) { // } } private byte[] modifyResponse(String input) { int indexContext = input.indexOf("/{context}"); if (indexContext >= 0) { input = RegexpPatterns.REGEXP_CONTEXT.matcher(input).replaceAll(context); // input = input.replace("/{context}", this.context); } return input.getBytes(Charset.forName("UTF-8")); } private class ModifiedOutputStream extends ServletOutputStream { private ServletOutputStream fServletOutputStream; private ByteArrayOutputStream fBuffer; private boolean fIsClosed; public ModifiedOutputStream(ServletOutputStream aOutputStream) { fServletOutputStream = aOutputStream; fBuffer = new ByteArrayOutputStream(); } public void write(int aByte) throws IOException { fBuffer.write(aByte); if (aByte == '\n') { processStream(); fBuffer.reset(); } } public void close() throws IOException { if (!fIsClosed) { processStream(); fServletOutputStream.close(); fIsClosed = true; } } public void flush() throws IOException { if (fBuffer.size() != 0) { if (!fIsClosed) { processStream(); fBuffer.reset(); } } } public void processStream() throws IOException { fServletOutputStream.write(modifyResponse(fBuffer.toString())); } } } }