/*
* Copyright (c) 2008. All rights reserved.
*/
package ro.isdc.wro.model.resource.locator.support;
import static org.apache.commons.lang3.Validate.notNull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.isdc.wro.config.jmx.WroConfiguration;
import ro.isdc.wro.http.support.RedirectedStreamServletResponseWrapper;
import ro.isdc.wro.model.resource.locator.UriLocator;
import ro.isdc.wro.model.resource.locator.UrlUriLocator;
import ro.isdc.wro.util.WroUtil;
/**
* Responsible to locate a context relative resource. Attempts to locate the resource using {@link RequestDispatcher} .
* If the dispatcher fails, it will fallback resource retrieval to a http call using {@link UrlUriLocator}.
*
* @author Alex Objelean
*/
public class DispatcherStreamLocator {
private static final Logger LOG = LoggerFactory.getLogger(DispatcherStreamLocator.class);
/**
* Attribute indicating that the request is included from within a wro request cycle. This is required to prevent
* {@link StackOverflowError}.
*
* @VisibleForTesting
*/
public static final String ATTRIBUTE_INCLUDED_BY_DISPATCHER = DispatcherStreamLocator.class.getName()
+ ".included_with_dispatcher";
private int timeout = WroConfiguration.DEFAULT_CONNECTION_TIMEOUT;
/**
* /** When using JBoss Portal and it has some funny quirks...actually a portal application have several small web
* application behind it. So when it intercepts a requests for portal then it start bombing the the application behind
* the portal with multiple threads (web requests) that are combined with threads for wro4j.
*
* @param location
* a path to a request relative resource to locate.
* @return a valid stream for required location. This method will never return a null.
* @throws IOException
* if the stream cannot be located at the specified location.
*/
public InputStream getInputStream(final HttpServletRequest request, final HttpServletResponse response,
final String location)
throws IOException {
if (request == null || response == null || location == null) {
throw new IOException("Cannot get stream for location: " + location
+ " because either request, response or location is not available");
}
// where to write the bytes of the stream
final ByteArrayOutputStream os = new ByteArrayOutputStream();
boolean warnOnEmptyStream = false;
try {
final RequestDispatcher dispatcher = request.getRequestDispatcher(location);
if (dispatcher != null) {
// Wrap request
final ServletRequest servletRequest = getWrappedServletRequest(request, location);
// Wrap response
final ServletResponse servletResponse = new RedirectedStreamServletResponseWrapper(os, response);
LOG.debug("dispatching request to location: {}", location);
// use dispatcher
dispatcher.include(servletRequest, servletResponse);
warnOnEmptyStream = true;
// force flushing - the content will be written to BytArrayOutputStream.
// Otherwise exactly 32K of data will be written.
servletResponse.getWriter().flush();
os.close();
}
} catch (final Exception e) {
LOG.debug("Could not dispatch request for location {}", location);
// Not only servletException can be thrown, also dispatch.include can throw NPE when the scheduler runs outside
// of the request cycle, thus connection is unavailable. This is caused mostly when invalid resources are
// included.
return locateExternal(request, location);
}
try {
// fallback to external resource locator if the dispatcher is empty
if (os.size() == 0) {
return locateExternal(request, location);
}
} finally {
if (warnOnEmptyStream && os.size() == 0) {
LOG.debug("Wrong or empty resource with location: {}", location);
}
}
return new ByteArrayInputStream(os.toByteArray());
}
public InputStream locateExternal(final HttpServletRequest request, final String location)
throws IOException {
final String servletContextPath = request.getRequestURL().toString().replace(request.getServletPath(), "");
final String absolutePath = servletContextPath + location;
LOG.debug("locateExternalUri: {}", absolutePath);
return createExternalResourceLocator().locate(absolutePath);
}
/**
* @return the {@link UriLocator} responsible for locating resources when dispatcher fails. No wildcard handling is
* required.
* @VisibleForTesting
*/
UriLocator createExternalResourceLocator() {
final UrlUriLocator locator = new UrlUriLocator();
locator.setEnableWildcards(false);
locator.setTimeout(timeout);
return locator;
}
/**
* Build a wrapped servlet request which will be used for dispatching.
*/
private ServletRequest getWrappedServletRequest(final HttpServletRequest request, final String location) {
final HttpServletRequest wrappedRequest = new HttpServletRequestWrapper(request) {
@Override
public String getRequestURI() {
return getContextPath() + location;
}
@Override
public String getPathInfo() {
return WroUtil.getPathInfoFromLocation(this, location);
}
@Override
public String getServletPath() {
return WroUtil.getServletPathFromLocation(this, location);
}
};
// add an attribute to mark this request as included from wro
wrappedRequest.setAttribute(ATTRIBUTE_INCLUDED_BY_DISPATCHER, Boolean.TRUE);
return wrappedRequest;
}
/**
* @return true if the request is included from within wro request cycle.
*/
public static boolean isIncludedRequest(final HttpServletRequest request) {
notNull(request);
return request.getAttribute(ATTRIBUTE_INCLUDED_BY_DISPATCHER) != null;
}
/**
* @param timeout
* time (in millis) used when an external url is requested for both: connection & read.
*/
public void setTimeout(final int timeout) {
this.timeout = timeout;
}
/**
* @return the configured timeout for this instance of locator.
*/
public int getTimeout() {
return timeout;
}
}