package xdi2.transport.impl.http;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import xdi2.core.Graph;
import xdi2.core.impl.memory.MemoryGraphFactory;
import xdi2.core.io.MimeType;
import xdi2.core.io.XDIReader;
import xdi2.core.io.XDIReaderRegistry;
import xdi2.core.io.XDIWriter;
import xdi2.core.io.XDIWriterRegistry;
import xdi2.core.syntax.XDIAddress;
import xdi2.core.syntax.XDIArc;
import xdi2.messaging.Message;
import xdi2.messaging.MessageEnvelope;
import xdi2.messaging.constants.XDIMessagingConstants;
import xdi2.messaging.http.AcceptHeader;
import xdi2.messaging.response.TransportMessagingResponse;
import xdi2.messaging.container.MessagingContainer;
import xdi2.transport.exceptions.Xdi2TransportException;
import xdi2.transport.impl.uri.UriTransport;
import xdi2.transport.registry.impl.uri.UriMessagingContainerMount;
import xdi2.transport.registry.impl.uri.UriMessagingContainerRegistry;
public class HttpTransport extends UriTransport<HttpTransportRequest, HttpTransportResponse> {
private static final Logger log = LoggerFactory.getLogger(HttpTransport.class);
private static final Map<String, String> DEFAULT_HEADERS;
private static final Map<String, String> DEFAULT_HEADERS_GET;
private static final Map<String, String> DEFAULT_HEADERS_POST;
private static final Map<String, String> DEFAULT_HEADERS_PUT;
private static final Map<String, String> DEFAULT_HEADERS_DELETE;
private static final Map<String, String> DEFAULT_HEADERS_OPTIONS;
static {
DEFAULT_HEADERS = new HashMap<String, String> ();
DEFAULT_HEADERS.put("Access-Control-Allow-Origin", "*");
DEFAULT_HEADERS.put("Access-Control-Allow-Credentials", "true");
DEFAULT_HEADERS.put("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Cache-Control, Expires, X-Cache, X-HTTP-Method-Override, Accept");
DEFAULT_HEADERS.put("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS");
DEFAULT_HEADERS_GET = new HashMap<String, String> ();
DEFAULT_HEADERS_POST = new HashMap<String, String> ();
DEFAULT_HEADERS_PUT = new HashMap<String, String> ();
DEFAULT_HEADERS_DELETE = new HashMap<String, String> ();
DEFAULT_HEADERS_OPTIONS = new HashMap<String, String> ();
DEFAULT_HEADERS_OPTIONS.put("Allow", "GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS");
}
private UriMessagingContainerRegistry uriMessagingContainerRegistry;
private Map<String, String> headers;
private Map<String, String> headersGet;
private Map<String, String> headersPost;
private Map<String, String> headersPut;
private Map<String, String> headersDelete;
private Map<String, String> headersOptions;
public HttpTransport(UriMessagingContainerRegistry uriMessagingContainerRegistry) {
this.uriMessagingContainerRegistry = uriMessagingContainerRegistry;
this.headers = DEFAULT_HEADERS;
this.headersGet = DEFAULT_HEADERS_GET;
this.headersPost = DEFAULT_HEADERS_POST;
this.headersPut = DEFAULT_HEADERS_PUT;
this.headersDelete = DEFAULT_HEADERS_DELETE;
this.headersOptions = DEFAULT_HEADERS_OPTIONS;
}
public HttpTransport() {
this(null);
}
@Override
public void init() throws Exception {
super.init();
}
@Override
public void shutdown() throws Exception {
super.shutdown();
}
@Override
public void execute(HttpTransportRequest request, HttpTransportResponse response) throws IOException {
if (log.isInfoEnabled()) log.info("Incoming " + request.getMethod() + " request to " + request.getRequestPath() + ". Content-Type: " + request.getContentType());
try {
UriMessagingContainerMount uriMessagingContainerMount = this.getUriMessagingContainerRegistry().lookup(request.getRequestPath());
if (HttpTransportRequest.METHOD_GET.equals(request.getMethod())) this.processGetRequest(request, response, uriMessagingContainerMount);
else if (HttpTransportRequest.METHOD_POST.equals(request.getMethod())) this.processPostRequest(request, response, uriMessagingContainerMount);
else if (HttpTransportRequest.METHOD_PUT.equals(request.getMethod())) this.processPutRequest(request, response, uriMessagingContainerMount);
else if (HttpTransportRequest.METHOD_DELETE.equals(request.getMethod())) this.processDeleteRequest(request, response, uriMessagingContainerMount);
else if (HttpTransportRequest.METHOD_OPTIONS.equals(request.getMethod())) this.processOptionsRequest(request, response);
else throw new Xdi2TransportException("Invalid HTTP method: " + request.getMethod());
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
sendErrorInternalServer(request, response, ex);
return;
}
if (log.isDebugEnabled()) log.debug("Successfully processed " + request.getMethod() + " request.");
}
protected void processGetRequest(HttpTransportRequest request, HttpTransportResponse response, UriMessagingContainerMount uriMessagingContainerMount) throws Xdi2TransportException, IOException {
final MessagingContainer messagingContainer = uriMessagingContainerMount == null ? null : uriMessagingContainerMount.getMessagingContainer();
MessageEnvelope messageEnvelope;
TransportMessagingResponse messagingResponse;
// execute interceptors
boolean result = InterceptorExecutor.executeHttpTransportInterceptorsGet(this.getInterceptors(), this, request, response, uriMessagingContainerMount);
if (result) {
if (log.isDebugEnabled()) log.debug("Skipping request according to HTTP transport interceptor (GET).");
return;
}
// no messaging container?
if (messagingContainer == null) {
sendErrorNotFound(request, response);
return;
}
// construct message envelope from url
try {
messageEnvelope = readFromUrl(uriMessagingContainerMount, request, response, XDIMessagingConstants.XDI_ADD_GET);
if (messageEnvelope == null) throw new Xdi2TransportException("No messaging request.");
} catch (IOException ex) {
throw new Xdi2TransportException("Invalid message envelope: " + ex.getMessage(), ex);
}
// execute the messaging request against our messaging container, save messaging response
messagingResponse = this.execute(messageEnvelope, messagingContainer, request, response);
if (messagingResponse == null || messagingResponse.getGraph() == null) throw new Xdi2TransportException("No messaging response.");
// done
this.sendOk(request, response, messagingResponse);
}
protected void processPostRequest(HttpTransportRequest request, HttpTransportResponse response, UriMessagingContainerMount uriMessagingContainerMount) throws Xdi2TransportException, IOException {
final MessagingContainer messagingContainer = uriMessagingContainerMount == null ? null : uriMessagingContainerMount.getMessagingContainer();
MessageEnvelope messageEnvelope;
TransportMessagingResponse messagingResponse;
// execute interceptors
boolean result = InterceptorExecutor.executeHttpTransportInterceptorsPost(this.getInterceptors(), this, request, response, uriMessagingContainerMount);
if (result) {
if (log.isDebugEnabled()) log.debug("Skipping request according to HTTP transport interceptor (POST).");
return;
}
// no messaging container?
if (messagingContainer == null) {
sendErrorNotFound(request, response);
return;
}
// construct messaging request from body
try {
messageEnvelope = readFromBody(request, response);
if (messageEnvelope == null) throw new Xdi2TransportException("No messaging request.");
} catch (IOException ex) {
throw new Xdi2TransportException("Invalid message envelope: " + ex.getMessage(), ex);
}
// execute the messaging request against our messaging container, save messaging response
messagingResponse = this.execute(messageEnvelope, messagingContainer, request, response);
if (messagingResponse == null || messagingResponse.getGraph() == null) throw new Xdi2TransportException("No messaging response.");
// done
this.sendOk(request, response, messagingResponse);
}
protected void processPutRequest(HttpTransportRequest request, HttpTransportResponse response, UriMessagingContainerMount uriMessagingContainerMount) throws Xdi2TransportException, IOException {
final MessagingContainer messagingContainer = uriMessagingContainerMount == null ? null : uriMessagingContainerMount.getMessagingContainer();
MessageEnvelope messageEnvelope;
TransportMessagingResponse messagingResponse;
// execute interceptors
boolean result = InterceptorExecutor.executeHttpTransportInterceptorsPut(this.getInterceptors(), this, request, response, uriMessagingContainerMount);
if (result) {
if (log.isDebugEnabled()) log.debug("Skipping request according to HTTP transport interceptor (PUT).");
return;
}
// no messaging container?
if (messagingContainer == null) {
sendErrorNotFound(request, response);
return;
}
// construct message envelope from url
try {
messageEnvelope = readFromUrl(uriMessagingContainerMount, request, response, XDIMessagingConstants.XDI_ADD_SET);
if (messageEnvelope == null) throw new Xdi2TransportException("No messaging request.");
} catch (IOException ex) {
throw new Xdi2TransportException("Invalid message envelope: " + ex.getMessage(), ex);
}
// execute the messaging request against our messaging container, save messaging response
messagingResponse = this.execute(messageEnvelope, messagingContainer, request, response);
if (messagingResponse == null || messagingResponse.getGraph() == null) throw new Xdi2TransportException("No messaging response.");
// done
this.sendOk(request, response, messagingResponse);
}
protected void processDeleteRequest(HttpTransportRequest request, HttpTransportResponse response, UriMessagingContainerMount uriMessagingContainerMount) throws Xdi2TransportException, IOException {
final MessagingContainer messagingContainer = uriMessagingContainerMount == null ? null : uriMessagingContainerMount.getMessagingContainer();
MessageEnvelope messageEnvelope;
TransportMessagingResponse messagingResponse;
// execute interceptors
boolean result = InterceptorExecutor.executeHttpTransportInterceptorsDelete(this.getInterceptors(), this, request, response, uriMessagingContainerMount);
if (result) {
if (log.isDebugEnabled()) log.debug("Skipping request according to HTTP transport interceptor (DELETE).");
return;
}
// no messaging container?
if (messagingContainer == null) {
sendErrorNotFound(request, response);
return;
}
// construct message envelope from url
try {
messageEnvelope = readFromUrl(uriMessagingContainerMount, request, response, XDIMessagingConstants.XDI_ADD_DEL);
if (messageEnvelope == null) throw new Xdi2TransportException("No messaging request.");
} catch (IOException ex) {
throw new Xdi2TransportException("Invalid message envelope: " + ex.getMessage(), ex);
}
// execute the messaging request against our messaging container, save messaging response
messagingResponse = this.execute(messageEnvelope, messagingContainer, request, response);
if (messagingResponse == null || messagingResponse.getGraph() == null) throw new Xdi2TransportException("No messaging response.");
// done
this.sendOk(request, response, messagingResponse);
}
protected void processOptionsRequest(HttpTransportRequest request, HttpTransportResponse response) throws Xdi2TransportException, IOException {
// send out response
this.sendOk(request, response, null);
response.setContentLength(0);
}
/*
* Helper methods
*/
private static MessageEnvelope readFromUrl(UriMessagingContainerMount messagingContainerMount, HttpTransportRequest request, HttpTransportResponse response, XDIAddress operationAddress) throws IOException {
if (messagingContainerMount == null) throw new NullPointerException();
// parse an XDI address from the request path
String addr = request.getRequestPath().substring(messagingContainerMount.getMessagingContainerPath().length());
while (addr.length() > 0 && addr.charAt(0) == '/') addr = addr.substring(1);
if (log.isDebugEnabled()) log.debug("XDI address: " + addr);
XDIAddress targetAddress;
if (addr.equals("")) {
targetAddress = null;
} else {
try {
targetAddress = XDIAddress.create(addr);
} catch (Exception ex) {
log.error("Cannot parse XDI address: " + ex.getMessage(), ex);
throw new IOException("Cannot parse XDI graph: " + ex.getMessage(), ex);
}
}
// convert address to a mini messaging envelope
if (log.isDebugEnabled()) log.debug("Requested XDI context node: " + targetAddress + ".");
MessageEnvelope messageEnvelope = MessageEnvelope.fromOperationXDIAddressAndTargetXDIAddress(XDIMessagingConstants.XDI_ADD_GET, targetAddress);
// set the TO peer root to the owner peer root of the messaging container
XDIArc ownerPeerRootXDIArc = messagingContainerMount.getMessagingContainer().getOwnerPeerRootXDIArc();
if (ownerPeerRootXDIArc != null) {
Message message = messageEnvelope.getMessages().next();
message.setToPeerRootXDIArc(ownerPeerRootXDIArc);
}
// done
return messageEnvelope;
}
private static MessageEnvelope readFromBody(HttpTransportRequest request, HttpTransportResponse response) throws IOException {
InputStream inputStream = request.getBodyInputStream();
// try to find an appropriate reader for the provided mime type
XDIReader xdiReader = null;
String contentType = request.getContentType();
MimeType recvMimeType = contentType != null ? new MimeType(contentType) : null;
xdiReader = recvMimeType != null ? XDIReaderRegistry.forMimeType(recvMimeType) : null;
if (xdiReader == null) xdiReader = XDIReaderRegistry.getDefault();
// read everything into an in-memory XDI graph (a message envelope)
if (log.isDebugEnabled()) log.debug("Reading message in " + recvMimeType + " with reader " + xdiReader.getClass().getSimpleName() + ".");
MessageEnvelope messageEnvelope;
try {
Graph graph = MemoryGraphFactory.getInstance().openGraph();
xdiReader.read(graph, inputStream);
messageEnvelope = MessageEnvelope.fromGraph(graph);
} catch (IOException ex) {
throw ex;
} catch (Exception ex) {
log.error("Cannot parse XDI graph: " + ex.getMessage(), ex);
throw new IOException("Cannot parse XDI graph: " + ex.getMessage(), ex);
} finally {
inputStream.close();
}
if (log.isDebugEnabled()) log.debug("Message envelope received (" + messageEnvelope.getMessageCount() + " messages). Executing...");
// done
return messageEnvelope;
}
private void sendOk(HttpTransportRequest request, HttpTransportResponse response, TransportMessagingResponse messagingResponse) throws IOException {
response.setStatus(HttpTransportResponse.SC_OK);
Map<String, String> headers = new HashMap<String, String> ();
headers.putAll(this.getHeaders());
if (HttpTransportRequest.METHOD_GET.equals(request.getMethod())) headers.putAll(this.getHeadersGet());
if (HttpTransportRequest.METHOD_POST.equals(request.getMethod())) headers.putAll(this.getHeadersPost());
if (HttpTransportRequest.METHOD_PUT.equals(request.getMethod())) headers.putAll(this.getHeadersPut());
if (HttpTransportRequest.METHOD_DELETE.equals(request.getMethod())) headers.putAll(this.getHeadersDelete());
if (HttpTransportRequest.METHOD_OPTIONS.equals(request.getMethod())) headers.putAll(this.getHeadersOptions());
for (Map.Entry<String, String> header : headers.entrySet()) {
response.setHeader(header.getKey(), header.getValue());
}
if (messagingResponse != null) {
// find a suitable writer based on accept headers
if (log.isDebugEnabled()) log.debug("Accept: " + request.getHeader("Accept"));
XDIWriter writer = null;
String acceptHeader = request.getHeader("Accept");
MimeType sendMimeType = acceptHeader != null ? AcceptHeader.parse(acceptHeader).bestMimeType(false, true) : null;
writer = sendMimeType != null ? XDIWriterRegistry.forMimeType(sendMimeType) : null;
if (writer == null) writer = XDIWriterRegistry.getDefault();
// send out the message result
if (log.isDebugEnabled()) log.debug("Sending result in " + sendMimeType + " with writer " + writer.getClass().getSimpleName() + ".");
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
writer.write(messagingResponse.getGraph(), buffer);
response.setContentType(writer.getMimeType().toString());
response.setContentLength(buffer.size());
response.writeBody(buffer.toByteArray(), true);
}
if (log.isDebugEnabled()) log.debug("Output complete.");
}
private static void sendErrorNotFound(HttpTransportRequest request, HttpTransportResponse response) throws IOException {
log.warn("Not found: " + request.getRequestPath() + ". Sending " + HttpTransportResponse.SC_NOT_FOUND + ".");
response.sendError(HttpTransportResponse.SC_NOT_FOUND, "Not found: " + request.getRequestPath());
}
private static void sendErrorInternalServer(HttpTransportRequest request, HttpTransportResponse response, Exception ex) throws IOException {
log.error("Internal server error: " + ex.getMessage() + ". Sending " + HttpTransportResponse.SC_NOT_FOUND + ".", ex);
response.sendError(HttpTransportResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error: " + ex.getMessage());
}
/*
* Getters and setters
*/
@Override
public UriMessagingContainerRegistry getUriMessagingContainerRegistry() {
return this.uriMessagingContainerRegistry;
}
public void setUriMessagingContainerRegistry(UriMessagingContainerRegistry uriMessagingContainerRegistry) {
this.uriMessagingContainerRegistry = uriMessagingContainerRegistry;
}
public Map<String, String> getHeaders() {
return this.headers;
}
public void setHeaders(Map<String, String> headers) {
this.headers = headers;
}
public Map<String, String> getHeadersGet() {
return this.headersGet;
}
public void setHeadersGet(Map<String, String> headersGet) {
this.headersGet = headersGet;
}
public Map<String, String> getHeadersPost() {
return this.headersPost;
}
public void setHeadersPost(Map<String, String> headersPost) {
this.headersPost = headersPost;
}
public Map<String, String> getHeadersPut() {
return this.headersPut;
}
public void setHeadersPut(Map<String, String> headersPut) {
this.headersPut = headersPut;
}
public Map<String, String> getHeadersDelete() {
return this.headersDelete;
}
public void setHeadersDelete(Map<String, String> headersDelete) {
this.headersDelete = headersDelete;
}
public Map<String, String> getHeadersOptions() {
return this.headersOptions;
}
public void setHeadersOptions(Map<String, String> headersOptions) {
this.headersOptions = headersOptions;
}
}