App Engine Java API call hooks

In a previous post, we discussed API call hooks for Python. It's possible to hook and modify RPC calls in Java, too. In this post, we'll demonstrate how.

All API calls in Java are handled by the class com.google.apphosting.api.ApiProxy. This class behaves similarly to the ApiProxyStubMap in the Python SDK, having makeSyncCall and makeAsyncCall functions that take care of invoking the relevant API calls. Unlike the Python runtime, however, all Java API calls are handled by a single delegate class, defined by the interface ApiProxy.Delegate. The active delegate for an App Engine app can be retrieved with ApiProxy.getDelegate, and set with ApiProxy.setDelegate.

Another difference from the Python API is that API calls are serialized into byte arrays before being passed to the ApiProxy, and responses are likewise deserialized by the caller. As a result, the makeSyncCall method takes a byte array as an argument, and returns a byte array.

Because all calls are routed to a single Delegate, the granularity of hooks supported is restricted to hooking all API calls, or none. To make things easier, we'll define a Delegate implementation that dispatches API calls to other Delegate subclasses based on the API being called:

import java.util.HashMap;
import java.util.Map;

import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.ApiProxyException;
import com.google.apphosting.api.ApiProxy.Delegate;
import com.google.apphosting.api.ApiProxy.Environment;
import com.google.apphosting.api.ApiProxy.LogRecord;

public class ApiProxyHook implements Delegate {
	private Delegate baseDelegate;
	private Map hooks = new HashMap();
	
	public ApiProxyHook(Delegate base) {
		this.baseDelegate = base;
	}
	
	public void log(Environment environment, LogRecord record) {
		this.baseDelegate.log(environment, record);
	}

	public byte[] makeSyncCall(Environment environment, String packageName,
			String methodName, byte[] request) throws ApiProxyException {
		Delegate hook = this.hooks.get(packageName);
		if(hook != null) {
			return hook.makeSyncCall(environment, packageName, methodName, request);
		} else {
			return this.baseDelegate.makeSyncCall(environment, packageName, methodName, request);
		}
	}

	public Delegate getBaseDelegate() {
		return baseDelegate;
	}

	public Map getHooks() {
		return hooks;
	}
}

To install this class, we fetch the existing delegate, pass it to the constructor of our ApiProxyHook class, and set the resulting instance as the new delegate, like this:

ApiProxy.setDelegate(new ApiProxyHook(ApiProxy.getDelegate();

Typically, you'd execute this code in a static block, or elsewhere in a location that gets executed only once per runtime.

To demonstrate the code in action, let's define a class that implements the multi-tenancy behaviour we demonstrated for Python in the original article:

import com.google.apphosting.api.ApiProxy.ApiProxyException;
import com.google.apphosting.api.ApiProxy.Delegate;
import com.google.apphosting.api.ApiProxy.Environment;
import com.google.apphosting.api.ApiProxy.LogRecord;
import com.google.apphosting.api.DatastorePb.PutRequest;
import com.google.apphosting.api.DatastorePb.GetResponse;
import com.google.apphosting.api.DatastorePb.Query;
import com.google.apphosting.api.DatastorePb.GetResponse.Entity;
import com.google.apphosting.api.DatastorePb.Query.Filter;
import com.google.apphosting.api.DatastorePb.Query.Filter.Operator;
import com.google.storage.onestore.v3.OnestoreEntity.EntityProto;
import com.google.storage.onestore.v3.OnestoreEntity.Property;

public class MultiTenantHook implements Delegate {
	public class InvalidDomainException extends RuntimeException {
	}
	
	private static final String DOMAIN_PROPERTY_NAME = "_domain";

	private Delegate baseDelegate;

	public MultiTenantHook(Delegate base) {
		this.baseDelegate = base;
	}
	
	public void log(Environment arg0, LogRecord arg1) {
	}

	public byte[] makeSyncCall(Environment env, String packageName,
			String methodName, byte[] request) throws ApiProxyException {
		if(methodName.equals("Put")) {
			return this.handlePut(env, request);
		} else if(methodName.equals("Get")) {
			return this.handleGet(env, request);
		} else if(methodName.equals("RunQuery")) {
			return this.handleQuery(env, request);
		} else {
			return this.baseDelegate.makeSyncCall(env, packageName,
					methodName, request);
		}
	}
	
	private String getDomain(Environment env) {
		return (String)env.getAttributes().get("net.notdot.current_domain");
	}
	
	private String getDomainProperty(EntityProto entity) {
		for(Property property : entity.propertys()) {
			if(property.getName().equals(DOMAIN_PROPERTY_NAME)) {
				return property.getValue().getStringValue();
			}
		}
		return null;
	}
	
	private void setDomainProperty(EntityProto entity, String domain) {
		for(Property property : entity.mutablePropertys()) {
			if(property.getName().equals(DOMAIN_PROPERTY_NAME)) {
				property.getMutableValue().setStringValue(domain);
				return;
			}
		}
		
		Property property = entity.addProperty();
		property.setName(DOMAIN_PROPERTY_NAME);
		property.setMultiple(false);
		property.getMutableValue().setStringValue(domain);
	}

	private byte[] handleQuery(Environment env, byte[] requestData) {
		Query query = new Query();
		query.mergeFrom(requestData);
		
		Filter domain_filter = query.addFilter();
		domain_filter.setOp(Operator.EQUAL);
		Property property = domain_filter.addProperty();
		property.setName(DOMAIN_PROPERTY_NAME);
		property.getMutableValue().setStringValue(this.getDomain(env));
		
		return this.baseDelegate.makeSyncCall(env, "datastore_v3", "RunQuery",
				query.toByteArray());
	}

	private byte[] handleGet(Environment env, byte[] requestData) {
		GetResponse response = new GetResponse();
		response.mergeFrom(this.baseDelegate.makeSyncCall(env, "datastore_v3",
				"Get", requestData));
		
		String domain = this.getDomain(env);
		for(Entity e : response.entitys()) {
			EntityProto entity = e.getEntity();
			if(!this.getDomainProperty(entity).equals(domain)) {
				throw new InvalidDomainException();
			}
		}
		
		return response.toByteArray();
	}

	private byte[] handlePut(Environment env, byte[] requestData) {
		PutRequest request = new PutRequest();
		request.mergeFrom(requestData);
		
		String domain = this.getDomain(env);
		for(EntityProto entity : request.mutableEntitys()) {
			this.setDomainProperty(entity, domain);
		}
		
		return this.baseDelegate.makeSyncCall(env, "datastore_v3", "Put",
				request.toByteArray());
	}

}

The code above bears many similarities to the Python example. The Protocol Buffer library behaves very similarly to the Python one, so only syntactic changes are necessary, along with the changes required because of the different hook system.

This code demonstrates another feature of the ApiProxy class - the Environment. Environment is a class that provides some details required by the App Engine environment, such as the current App ID, version, and the credentials of the currently logged in user (if any). It also provides an attribute dictionary, which we're using in the above snippet to retrieve the hostname for the current request - eliminating the need to pass the ServletRequest object to API calls.

There's one further requirement, then: A Filter class to set the 'net.notdot.current_domain' property to the domain the current request was made to. Here's a basic implementation:

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import com.google.apphosting.api.ApiProxy;

public class MultiTenantFilter implements Filter {
	static {
		ApiProxyHook hook = new ApiProxyHook(ApiProxy.getDelegate());
		hook.getHooks().put("datastore_v3", new MultiTenantHook(hook.getBaseDelegate()));
		ApiProxy.setDelegate(hook);
	}

	public void destroy() {
		// TODO Auto-generated method stub

	}

	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		ApiProxy.getCurrentEnvironment().getAttributes().put(
				"net.notdot.current_domain", request.getServerName());
		chain.doFilter(request, response);
	}

	public void init(FilterConfig arg0) throws ServletException {
		// TODO Auto-generated method stub

	}

}

Note the static initializer for this class sets up the API stubs as needed. Installing this Filter class for all requests ensures the environment is configured in the way required by our stub, and our Java code can now benefit from the same isolation we previously implemented in Python.

Comments

blog comments powered by Disqus