Skip to content

Instantly share code, notes, and snippets.

@codedance
Forked from mufumbo/gist:6325670
Last active October 30, 2015 00:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save codedance/9340965 to your computer and use it in GitHub Desktop.
Save codedance/9340965 to your computer and use it in GitHub Desktop.
package com.papercut.gae.utils;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import com.google.appengine.api.modules.ModulesService;
import com.google.appengine.api.modules.ModulesServiceFactory;
import com.google.appengine.api.utils.SystemProperty;
import com.google.common.io.ByteStreams;
/**
* This Filter is a hack to duplicate the dispatch.xml module routing
* functionality in the Google App Engine development server.
*
* Based on: https://gist.github.com/mufumbo/6325670
*
* @author chris
*
*/
public class AppengineDispatchFilterHack implements Filter {
private final static Logger logger = Logger.getLogger(AppengineDispatchFilterHack.class.getName());
private final static boolean IS_PROD =
(SystemProperty.environment.value() == SystemProperty.Environment.Value.Production);
private final static Map<String, String> urlModuleMap = new HashMap<>();
@Override
public void init(FilterConfig filterConfig) throws ServletException {
if (!IS_PROD) {
logger.info("Loading fake dispatcher filter");
try {
mapConfigurationDispatch(filterConfig.getServletContext());
} catch (SAXException | IOException | ParserConfigurationException e) {
throw new ServletException(e);
}
}
}
private void mapConfigurationDispatch(ServletContext servletContext)
throws SAXException, IOException, ParserConfigurationException {
InputStream dispatchStream = servletContext.getResourceAsStream("/WEB-INF/dispatch.xml");
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = docBuilder.parse(dispatchStream);
dispatchStream.close();
NodeList nodeList = doc.getElementsByTagName("dispatch");
for (int i = 0; i < nodeList.getLength(); i++) {
Element elem = (Element) nodeList.item(i);
String url = elem.getElementsByTagName("url").item(0).getTextContent();
String module = elem.getElementsByTagName("module").item(0).getTextContent();
logger.info("Fake dispatcher with URL: " + url + " Module: " + module);
urlModuleMap.put(url, module);
}
}
@SuppressWarnings("rawtypes")
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// Don't do anything in production.
if (IS_PROD) {
chain.doFilter(request, response);
return;
}
HttpServletResponse resp = (HttpServletResponse) response;
HttpServletRequest req = ((HttpServletRequest) request);
String uri = req.getRequestURI().toString();
String queryString = req.getQueryString();
String method = req.getMethod();
String module = null;
for (String originalRule : urlModuleMap.keySet()) {
String rule = originalRule;
// we're not supporting this type of rule
if (rule.startsWith("*")) {
rule = rule.substring(1, rule.length());
}
if (rule.endsWith("*")) {
rule = rule.substring(0, rule.length() - 1);
if (uri.startsWith(rule)) {
module = urlModuleMap.get(originalRule);
logger.info("Found rule[" + originalRule + "] module[" + module + "] on uri[" + uri + "]");
break;
}
}
if (rule.equals(uri)) {
module = urlModuleMap.get(originalRule);
logger.info("Found exact match[" + originalRule + "] module[" + module + "]");
break;
}
}
if (module != null) {
ModulesService moduleService = ModulesServiceFactory.getModulesService();
String currentVersion = moduleService.getCurrentVersion();
String moduleHost = moduleService.getVersionHostname(module, currentVersion);
String url = "http://" + moduleHost + uri;
if (queryString != null) {
url += "?" + queryString;
}
URL u = new URL(url);
HttpURLConnection connection = (HttpURLConnection) u.openConnection();
connection.setRequestMethod(method);
connection.setConnectTimeout(30000);
connection.setReadTimeout(30000);
connection.setDoOutput(true);
// Simply passing headers through. TODO: improve this for multi value
StringBuilder headerDbg = new StringBuilder();
Enumeration headers = req.getHeaderNames();
while (headers != null && headers.hasMoreElements()) {
Object name = headers.nextElement();
Enumeration values = req.getHeaders(name.toString());
while (values != null && values.hasMoreElements()) {
Object value = values.nextElement();
if (value != null) {
String key = name.toString();
if ("host".equals(key.toLowerCase())) {
logger.info("dispatch OldHost[" + value.toString() + "] NewHost[" + moduleHost + "]");
} else {
if (headerDbg.length() > 0) {
headerDbg.append("&");
}
headerDbg.append(key).append("={").append(value.toString()).append("}");
}
connection.setRequestProperty(key, value.toString());
}
}
}
logger.info("dispatching[" + module + "] " + method + " to " + url + " with headrs:["
+ headerDbg.toString() + "]");
if ("GET".equals(method)) {
connection.setInstanceFollowRedirects(true);
} else if ("POST".equals(method)) {
connection.setDoInput(true);
// Copy through the POST payload
try (
InputStream in = req.getInputStream();
OutputStream out = connection.getOutputStream();
) {
ByteStreams.copy(in, out);
}
}
connection.connect();
int status = connection.getResponseCode();
resp.setStatus(status);
if (status != HttpURLConnection.HTTP_OK) {
logger.info("dispatch of " + url + " with status " + status);
}
// Copy the connection's output through on the response
try (
InputStream in = connection.getInputStream();
OutputStream out = response.getOutputStream();
) {
ByteStreams.copy(in, out);
}
} else {
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
}
@codedance
Copy link
Author

<filter>
    <filter-name>AppengineDispatchHack</filter-name>
    <filter-class>com.papercut.gae.utils.AppengineDispatchFilterHack</filter-class>
</filter>

<filter-mapping>
    <filter-name>AppengineDispatchHack</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

@mufumbo
Copy link

mufumbo commented Mar 4, 2014

looks great chris, thanks for the fork!

@aleemstreak
Copy link

man - this is so good. thanks so much for posting this.

@elpd
Copy link

elpd commented Nov 18, 2014

Excellent. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment