package io.apiman.plugins.jsonp_policy;
import io.apiman.gateway.engine.beans.ApiRequest;
import io.apiman.gateway.engine.beans.ApiResponse;
import io.apiman.gateway.engine.components.IBufferFactoryComponent;
import io.apiman.gateway.engine.io.AbstractStream;
import io.apiman.gateway.engine.io.IApimanBuffer;
import io.apiman.gateway.engine.io.IReadWriteStream;
import io.apiman.gateway.engine.policies.AbstractMappedPolicy;
import io.apiman.gateway.engine.policy.IDataPolicy;
import io.apiman.gateway.engine.policy.IPolicyChain;
import io.apiman.gateway.engine.policy.IPolicyContext;
import io.apiman.plugins.jsonp_policy.beans.JsonpConfigBean;
import io.apiman.plugins.jsonp_policy.http.HttpHeaders;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
/**
* Policy that turns an endpoint into a JSONP compatible endpoint. It removes the callback function param from the
* request and uses it to wrap the response. The content type is set to <code>application/javascript</code>.
*
* @see <a href="http://en.wikipedia.org/wiki/JSONP" target="_blank">JSONP - Wikipedia</a>
*
* @author Alexandre Kieling {@literal <alex.kieling@gmail.com>}
*/
public class JsonpPolicy extends AbstractMappedPolicy<JsonpConfigBean> implements IDataPolicy {
private static final String OPEN_PARENTHESES = "("; //$NON-NLS-1$
private static final String CLOSE_PARENTHESES = ")"; //$NON-NLS-1$
static final String CALLBACK_FUNCTION_NAME = "callbackFunctionName"; //$NON-NLS-1$
private static final String APPLICATION_JAVASCRIPT = "application/javascript"; //$NON-NLS-1$
@Override
protected Class<JsonpConfigBean> getConfigurationClass() {
return JsonpConfigBean.class;
}
@Override
protected void doApply(ApiRequest request, IPolicyContext context, JsonpConfigBean config,
IPolicyChain<ApiRequest> chain) {
String callbackParamName = config.getCallbackParamName();
String callbackFunctionName = request.getQueryParams().get(callbackParamName);
request.getQueryParams().remove(callbackParamName);
if (callbackFunctionName != null) {
context.setAttribute(CALLBACK_FUNCTION_NAME, callbackFunctionName);
}
super.doApply(request, context, config, chain);
}
/**
* @see io.apiman.gateway.engine.policy.IDataPolicy#getRequestDataHandler(io.apiman.gateway.engine.beans.ApiRequest, io.apiman.gateway.engine.policy.IPolicyContext, java.lang.Object)
*/
@Override
public IReadWriteStream<ApiRequest> getRequestDataHandler(ApiRequest request,
IPolicyContext context, Object policyConfiguration) {
return null;
}
/**
* @see io.apiman.gateway.engine.policy.IDataPolicy#getResponseDataHandler(io.apiman.gateway.engine.beans.ApiResponse, io.apiman.gateway.engine.policy.IPolicyContext, java.lang.Object)
*/
@Override
public IReadWriteStream<ApiResponse> getResponseDataHandler(final ApiResponse response,
IPolicyContext context, Object policyConfiguration) {
final String callbackFunctionName = (String) context.getAttribute(CALLBACK_FUNCTION_NAME, null);
if (callbackFunctionName != null) {
HttpHeaders httpHeaders = new HttpHeaders(response.getHeaders());
final String encoding = httpHeaders.getCharsetFromContentType(StandardCharsets.UTF_8.name());
final int additionalContentLength = callbackFunctionName.length()
+ OPEN_PARENTHESES.length() + CLOSE_PARENTHESES.length();
// JSONP responses should have the Content-Type header set to "application/javascript"
httpHeaders.setContentType(APPLICATION_JAVASCRIPT);
// the Content-Length will need to be longer
httpHeaders.incrementContentLength(additionalContentLength);
final IBufferFactoryComponent bufferFactory = context.getComponent(IBufferFactoryComponent.class);
return new AbstractStream<ApiResponse>() {
private boolean firstChunk = true;
@Override
public ApiResponse getHead() {
return response;
}
@Override
protected void handleHead(ApiResponse head) {
}
@Override
public void write(IApimanBuffer chunk) {
if (firstChunk) {
IApimanBuffer buffer = bufferFactory.createBuffer(callbackFunctionName.length()
+ OPEN_PARENTHESES.length());
try {
buffer.append(callbackFunctionName, encoding);
buffer.append(OPEN_PARENTHESES, encoding);
} catch (UnsupportedEncodingException e) {
// TODO Review the exception handling. A better approach might be throwing an IOException.
throw new RuntimeException(e);
}
// Write callbackFunctionName(
super.write(buffer);
firstChunk = false;
}
super.write(chunk);
}
@Override
public void end() {
// Write close parenth ) on end if something has been written
if (!firstChunk) {
IApimanBuffer buffer = bufferFactory.createBuffer(CLOSE_PARENTHESES.length());
try {
buffer.append(CLOSE_PARENTHESES, encoding);
} catch (UnsupportedEncodingException e) {
// TODO Review the exception handling. A better approach might be throwing an IOException.
throw new RuntimeException(e);
}
super.write(buffer);
}
super.end();
}
};
}
return null;
}
}