Skip to content

Instantly share code, notes, and snippets.

@mufumbo
Last active December 21, 2015 15:19
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mufumbo/6325670 to your computer and use it in GitHub Desktop.
Save mufumbo/6325670 to your computer and use it in GitHub Desktop.
proof of concept for simulating the appengine dispatch.xml while in development mode
package com.yumyumlabs.web.filter;
import com.google.appengine.api.labs.modules.ModulesService;
import com.google.appengine.api.labs.modules.ModulesServiceFactory;
import com.google.appengine.api.utils.SystemProperty;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
public class AppengineDispatchFilter implements Filter {
final static Logger logger = Logger.getLogger(AppengineDispatchFilter.class.getName());
private final static boolean IS_PROD = SystemProperty.environment.value() == SystemProperty.Environment.Value.Production;
final static Map<String, String> urlModuleMap = new HashMap<>();
@Override
public void init(FilterConfig filterConfig) throws ServletException {
if (!IS_PROD) {
logger.info("fake dispatcher initialized with dispatch.xml config:");
mapConfigurationDispatch();
}
}
// TODO: instead of changing this by hand, read from dispatch.xml and figure it out
// TODO: https://code.google.com/p/googleappengine/source/browse/trunk/java/src/main/com/google/apphosting/utils/config/DispatchXmlReader.java
private void mapConfigurationDispatch() {
urlModuleMap.put("*/admin*", "webadmin");
urlModuleMap.put("*/api/recipe/list-by-path*", "searcher");
urlModuleMap.put("*/api/recipe/search*", "searcher");
urlModuleMap.put("*/c/i/*", "content");
// TODO: this shouldn't be here if TaskOptions header host was working.
urlModuleMap.put("*/tasks*", "tasks");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (IS_PROD) {
chain.doFilter(request, response);
}
else {
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 modulesApi = ModulesServiceFactory.getModulesService();
String moduleHost = modulesApi.getModuleHostname(module, modulesApi.getDefaultVersion(module));
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); // TODO: handle redirects passing through the redirect header to this request.
}
else if ("POST".equals(method)) {
connection.setDoInput(true);
OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream());
Enumeration<String> params = req.getParameterNames();
int i = 0;
while (params != null && params.hasMoreElements()) {
String param = params.nextElement();
String[] values = req.getParameterValues(param);
if (values != null) {
for (String value : values) {
if (i++ > 0) {
writer.write("&");
}
param = URLEncoder.encode(param, "utf-8");
value = URLEncoder.encode(value, "utf-8");
writer.write(param);
writer.write("=");
writer.write(value);
}
}
}
writer.flush();
writer.close();
}
connection.connect();
int status = connection.getResponseCode();
resp.setStatus(status);
if (status != HttpURLConnection.HTTP_OK) {
logger.info("dispatch of " + url + " with status " + status);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] tmp = new byte[4096];
InputStream in = connection.getInputStream();
int len = 0;
while ((len = in.read(tmp)) > 0) {
out.write(tmp, 0, len);
}
in.close();
out.close();
byte[] content = out.toByteArray();
response.getOutputStream().write(content);
}
else {
chain.doFilter(request, response);
}
}
}
@Override
public void destroy() {
if (!IS_PROD)
logger.info("destroy");
}
}
@mufumbo
Copy link
Author

mufumbo commented Aug 24, 2013

in web.xml:

<filter>
    <filter-name>appengineDispatch</filter-name>
    <filter-class>com.yumyumlabs.web.filter.AppengineDispatchFilter</filter-class>
</filter>

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

@codedance
Copy link

Here is an updated version that supports parsing the dispatch.xml file. It also should work with POST methods with non-form style payloads (e.g. a JSON payload) :

https://gist.github.com/codedance/9340965

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