package edu.lmu.cs.headmaster.client.web;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.apache.wicket.IRequestTarget;
import org.apache.wicket.PageParameters;
import org.apache.wicket.RequestCycle;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.model.PropertyModel;
import org.apache.wicket.protocol.http.WebResponse;
import edu.lmu.cs.headmaster.client.web.util.ServiceRelayUtils;
import edu.lmu.cs.headmaster.client.web.util.ServiceRelayUtils.RequestMethod;
/**
* ServiceRelayPage passes client web app service calls to the true web service.
* This allows Headmaster to run its web service and web client on different
* hosts, without violating the same origin policy.
*/
public class ServiceRelayPage extends ClientPage {
public ServiceRelayPage(final PageParameters pageParameters) {
super(pageParameters);
serviceTail = pageParameters.getString("s");
getRequestCycle().setRequestTarget(new IRequestTarget() {
@Override
public void detach(RequestCycle requestCycle) {
// Nothing to do here.
}
@Override
public void respond(RequestCycle requestCycle) {
// Put together the URI.
String serviceUri = getServiceRoot() + getServiceTail();
// Supply credentials. As a Wicket page derivative we use the
// property model approach.
String username = new PropertyModel<String>(ServiceRelayPage.this,
"session.currentUsername").getObject();
String password = new PropertyModel<String>(ServiceRelayPage.this,
"session.currentPassword").getObject();
// Relay any additional parameters.
List<NameValuePair> requestParameters = new ArrayList<NameValuePair>();
for (String parameterName: pageParameters.keySet()) {
if (!"s".equals(parameterName)) {
requestParameters.add(new BasicNameValuePair(parameterName,
pageParameters.getString(parameterName)));
}
}
// Build the request based on the method.
HttpUriRequest request = createRelayRequest(serviceUri, requestParameters,
getWebRequestCycle().getWebRequest().getHttpServletRequest(), username);
getLogger().info("Using service URI: [" + request.getURI() + "]");
// Issue the request.
try {
requestCycle.getResponse().write(new ByteArrayInputStream(
ServiceRelayUtils.sendServiceLayerRequest(request, username, password,
new ServiceResponseHandler(getWebRequestCycle().getWebResponse()
)
)));
} catch(ClientProtocolException cpexc) {
getLogger().error(cpexc.getMessage(), cpexc);
getSession().error(cpexc.getMessage());
throw new RestartResponseException(ServiceErrorPage.class);
} catch(IOException ioexc) {
getLogger().error(ioexc.getMessage(), ioexc);
error(ioexc.getMessage());
}
}
});
}
/**
* Convenience method for getting the service tail: drop a leading slash
* if necessary (since the service root will have the trailing slash).
*/
private String getServiceTail() {
// Simple encoding: ditch spaces.
return (serviceTail.startsWith("/") ?
serviceTail.substring(1) : serviceTail).replaceAll(" ", "%20");
}
/**
* Factory method for http requests.
*/
@SuppressWarnings("unchecked")
private HttpUriRequest createRelayRequest(String serviceUri,
List<NameValuePair> requestParameters, HttpServletRequest originalRequest,
String username) {
// Build the initial request.
RequestMethod requestMethod = RequestMethod.valueOf(originalRequest.getMethod());
HttpUriRequest result = ServiceRelayUtils.createServiceLayerRequest(serviceUri,
requestParameters, requestMethod, username);
// For PUT or POST, copy the entity (this consumes the original one). We
// trust ServiceRelayUtils.createServiceLayerRequest to have created an
// appropriate request based on the given request method.
if (requestMethod == RequestMethod.POST || requestMethod == RequestMethod.PUT) {
result = copyEntity((HttpEntityEnclosingRequestBase)result, originalRequest);
};
// Relay original request headers except for Content-Length.
Enumeration<String> headerNames = (Enumeration<String>)originalRequest.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
if (!"Content-Length".equalsIgnoreCase(headerName)) {
Enumeration<String> headerValues = originalRequest.getHeaders(headerName);
while (headerValues.hasMoreElements()) {
String headerValue = headerValues.nextElement();
result.setHeader(headerName, headerValue);
getLogger().debug("Setting header " + headerName + "=" + headerValue);
}
}
}
return result;
}
/**
* Helper method for copying the original request's body/entity into the
* given request. The original request's entity is read entirely into a byte
* array so we are not dependent on the life cycle of its input stream.
*/
private HttpEntityEnclosingRequestBase copyEntity(HttpEntityEnclosingRequestBase request,
HttpServletRequest originalRequest) {
try {
request.setEntity(new ByteArrayEntity(EntityUtils.toByteArray(
new InputStreamEntity(originalRequest.getInputStream(),
originalRequest.getContentLength()))));
} catch(IOException ioexc) {
// Shouldn't happen, but we log anyway.
getLogger().error("Could not copy entity", ioexc);
}
return request;
}
/**
* Helper class that passes on certain service response headers to the relayed response.
*/
private class ServiceResponseHandler implements ResponseHandler<byte[]> {
private WebResponse webResponse;
public ServiceResponseHandler(WebResponse webResponse) {
this.webResponse = webResponse;
}
public byte[] handleResponse(final HttpResponse response)
throws HttpResponseException, IOException {
// Relay most response headers. The omitted ones either have been known to break Ajax
// calls for some reason, or are already emitted by Wicket automatically.
relayHeadersExcept(response, "server", "transfer-encoding");
// Relay the response status.
webResponse.getHttpServletResponse().setStatus(
response.getStatusLine().getStatusCode()
);
// Write out the entity.
HttpEntity httpEntity = response.getEntity();
return (httpEntity != null) ? EntityUtils.toByteArray(httpEntity) : new byte[0];
}
private void relayHeadersExcept(final HttpResponse response, String... headerNames) {
for (Header header: response.getAllHeaders()) {
boolean includeHeader = true;
for (String headerToExclude: headerNames) {
if (headerToExclude.equalsIgnoreCase(header.getName())) {
includeHeader = false;
break;
}
}
if (includeHeader) {
if ("content-length".equalsIgnoreCase(header.getName())) {
webResponse.setContentLength(Long.parseLong(header.getValue()));
} else {
webResponse.setHeader(header.getName(), header.getValue());
}
}
}
}
}
private String serviceTail;
}