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
  • Select an option

  • Save codedance/9340965 to your computer and use it in GitHub Desktop.

Select an option

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
Copy Markdown
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
Copy Markdown

mufumbo commented Mar 4, 2014

looks great chris, thanks for the fork!

@aleemstreak
Copy link
Copy Markdown

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

@elpd
Copy link
Copy Markdown

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