/* 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.riotfamily.common.web.performance;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.riotfamily.common.web.cache.AbstractCacheableController;
import org.riotfamily.common.web.cache.controller.Compressible;
import org.riotfamily.common.web.support.CapturingResponseWrapper;
import org.riotfamily.common.web.support.ServletUtils;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.LastModified;
/**
* Controller that concatenates resources and optionally compresses or
* obfuscates them. The result is cached by Cachius and gzipped when possible.
* <p>
* The controller uses a {@link RequestDispatcher} to request the resources
* and captures them using a {@link CapturingResponseWrapper}, which allows us
* to include dynamic resources, too.
* <p>
* Note: This will not work if an included controller uses
* <code>request.getRequestURI()</code> to look up a resource. Please use
* {@link ServletUtils#getRequestUri(HttpServletRequest)} instead.
*
* @author Felix Gnass [fgnass at neteye dot de]
* @since 6.5
*/
public abstract class AbstractMinifyController extends AbstractCacheableController
implements LastModified, Compressible {
private boolean reloadable;
private long startUpTime = System.currentTimeMillis();
/**
* If set to <code>true</code>, the output will not be cached,
* not compressed or obfuscated and no expires header will be sent.
*/
public void setReloadable(boolean reloadable) {
this.reloadable = reloadable;
}
/**
* Returns the server start-up time, or the current time if running in
* {@link #setReloadable(boolean) development mode}.
*/
@Override
public long getLastModified(HttpServletRequest request) {
return reloadable ? System.currentTimeMillis() : startUpTime;
}
/**
* Adds the query-string to the cache-key.
*/
@Override
protected void appendCacheKey(StringBuffer key, HttpServletRequest request) {
key.append('?').append(request.getQueryString());
}
/**
* Returns <code>CACHE_ETERNALLY</code>.
*/
@Override
public long getTimeToLive() {
return CACHE_ETERNALLY;
}
/**
* Always returns <code>true</code>.
*/
public boolean gzipResponse(HttpServletRequest request) {
return true;
}
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
String s = request.getParameter("files");
if (s != null) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
String[] a = StringUtils.commaDelimitedListToStringArray(s);
String lastAbsoultePath = null;
for (int i = 0; i < a.length; i++) {
String path = StringUtils.cleanPath(a[i]);
if (path.startsWith("/")) {
lastAbsoultePath = path;
}
else if (lastAbsoultePath != null) {
path = StringUtils.applyRelativePath(lastAbsoultePath, path);
}
if (path.indexOf("/WEB-INF/") != -1) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return null;
}
capture(path, buffer, request, response);
}
String contentType = getContentType();
if (contentType != null) {
response.setContentType(contentType);
}
ServletUtils.setFarFutureExpiresHeader(response);
Compressor compressor = getCompressor();
if (compressor != null) {
Reader in = new InputStreamReader(new ByteArrayInputStream(
buffer.toByteArray()), response.getCharacterEncoding());
compressor.compress(in, response.getWriter());
return null;
}
FileCopyUtils.copy(buffer.toByteArray(), response.getOutputStream());
}
return null;
}
protected void capture(String path, ByteArrayOutputStream buffer,
HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
CapturingResponseWrapper wrapper = new CapturingResponseWrapper(response, buffer);
request.getRequestDispatcher(path).include(new IgnoreIfModifiedSinceRequestWrapper(request), wrapper);
wrapper.flush();
buffer.write('\n');
}
protected abstract String getContentType();
protected abstract Compressor getCompressor();
private static class IgnoreIfModifiedSinceRequestWrapper extends HttpServletRequestWrapper {
private static final String HEADER_IFMODSINCE = "If-Modified-Since";
public IgnoreIfModifiedSinceRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public long getDateHeader(String name) {
if (HEADER_IFMODSINCE.equals(name)) {
return -1L;
}
return super.getDateHeader(name);
}
}
}