/*******************************************************************************
* Copyright (c) 2009 MATERNA Information & Communications. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html. For further
* project-related information visit http://www.ws4d.org. The most recent
* version of the JMEDS framework can be obtained from
* http://sourceforge.net/projects/ws4d-javame.
******************************************************************************/
package org.ws4d.java.communication.protocol.mime;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.ws4d.java.DPWSFramework;
import org.ws4d.java.communication.ContextID;
import org.ws4d.java.communication.ProtocolData;
import org.ws4d.java.communication.monitor.MonitoringContext;
import org.ws4d.java.communication.protocol.http.HTTPRequest;
import org.ws4d.java.communication.protocol.http.HTTPResponse;
import org.ws4d.java.communication.protocol.http.HTTPResponseHandler;
import org.ws4d.java.communication.protocol.http.HTTPResponseUtil;
import org.ws4d.java.communication.protocol.http.header.HTTPRequestHeader;
import org.ws4d.java.communication.protocol.http.header.HTTPResponseHeader;
import org.ws4d.java.communication.protocol.http.server.HTTPRequestHandler;
import org.ws4d.java.constants.HTTPConstants;
import org.ws4d.java.constants.MIMEConstants;
import org.ws4d.java.structures.HashMap;
import org.ws4d.java.structures.Queue;
import org.ws4d.java.types.InternetMediaType;
import org.ws4d.java.types.URI;
import org.ws4d.java.util.IDGenerator;
import org.ws4d.java.util.Log;
import org.ws4d.java.util.MIMEUtil;
import org.ws4d.java.util.Sync;
/**
* Default implementation of a HTTP handler which handles HTTP bodies containing
* MIME content.
* <p>
* This implementation allows the registration of MIME part handlers (
* {@link MIMEHandler} ) which handle the incoming content within the MIME part.
* </p>
* <p>
* The registration of such a MIME part handler can be done for one MIME part, a
* sequence of MIME parts or all MIME parts with, or without a content type.
* </p>
* <h3>Registration</h3>
* <p>
* <ul>
* <li>All MIME parts.
* <ul>
* <li>{@link #register(MIMEHandler)}</li>
* <li>Allows the registration of a MIME part handler for every incoming MIME
* part.</li>
* </ul>
* </li>
* <li>Single MIME part.
* <ul>
* <li>{@link #register(int, MIMEHandler)}</li>
* <li>Allows the registration of a MIME part handler for an explicit incoming
* MIME part.</li>
* </ul>
* </li>
* <li>All MIME parts with content type.
* <ul>
* <li>{@link #register(InternetMediaType, MIMEHandler)}</li>
* <li>Allows the registration of a MIME part handler for every incoming MIME
* part with a given content type.</li>
* </ul>
* </li>
* <li>Sequence of MIME parts.
* <ul>
* <li>{@link #register(int, int, MIMEHandler)}</li>
* <li>Allows the registration of a MIME part handler for a sequence of incoming
* MIME parts with given content type.</li>
* </ul>
* </li>
* <li>Single MIME part with content type.
* <ul>
* <li>{@link #register(InternetMediaType, int, MIMEHandler)}</li>
* <li>Allows the registration of a MIME part handler for an explicit incoming
* MIME part with given content type.</li>
* </ul>
* </li>
* <li>Sequence of MIME parts with content type.
* <ul>
* <li>{@link #register(InternetMediaType, int, int, MIMEHandler)}</li>
* <li>Allows the registration of a MIME part handler for a sequence of incoming
* MIME parts with given content type.</li>
* </ul>
* </li>
* </ul>
* <h3>Handling MIME parts</h3>
* <p>
* If a MIME part handler matches the part (and the content type if given by
* registration) a new thread ( {@link StreamConsumerThread} ) will be started
* and the thread will handle the MIME exchange.<br />
* The thread in which the {@link DefaultMIMEHandler} acts will wait until the
* {@link StreamConsumerThread} has read the whole MIME part or ends correctly.<br />
* If the {@link StreamConsumerThread} ends without reading the complete MIME
* part, the omitted bytes will be consumed to wake up the
* {@link DefaultMIMEHandler}.
* </p>
* <p>
* If the {@link DefaultMIMEHandler} is woken up, it will check for another MIME
* part. If another MIME part exists, the handling procedure will start again.<br />
* If no other MIME part exists, the HTTP response is generated by the
* {@link DefaultMIMEHandler}.
* </p>
* <h3>Generating HTTP response</h3>
* <p>
* Every {@link MIMEHandler} which handles a MIME part can add a
* {@link MIMEEntityOutput} into the MIME exchange for the response. If all MIME
* parts have been read, the generating process will begin.<br />
* To generate the HTTP response the {@link DefaultMIMEHandler} looks at the
* queued MIME entities.
* <ul>
* <li>No MIME entities
* <ul>
* <li>A HTTP 204 (No Content) response will be sent.</li>
* </ul>
* </li>
* <li>One MIME entity
* <ul>
* <li>A HTTP 200 (OK) response will be sent.</li>
* </ul>
* </li>
* <li>More MIME entities
* <ul>
* <li>A HTTP 200 (OK) response with MIME content will be sent.</li>
* </ul>
* </li>
* </ul>
* </p>
*/
public class DefaultMIMEHandler implements HTTPRequestHandler, HTTPResponseHandler {
public static String createMimeBoundary() {
return MIMEConstants.BOUNDARY_PREFIX + IDGenerator.getUUID();
}
private InternetMediaType mimeType = null;
private volatile long mimeMessageNumber = 0;
/**
* Creates a default MIME handler.
*/
public DefaultMIMEHandler() {
}
/**
* This table contains path and handler.
*/
private HashMap handlers = new HashMap();
private InternetMediaType getMimeType() {
if (mimeType == null) {
mimeType = InternetMediaType.cloneAndSetParameter(InternetMediaType.getMultipartRelated(), MIMEConstants.PARAMETER_BOUNDARY, createMimeBoundary());
}
return mimeType;
}
/**
* Allows the registration of a MIME part handler without content type for
* every MIME part.
*
* @param handler the handler which handles the request.
*/
public void register(MIMEHandler handler) {
register(null, -1, handler);
}
/**
* Allows the registration of a MIME part handler with content type for
* every MIME part.
*
* @param handler the handler which handles the request.
*/
public void register(InternetMediaType type, MIMEHandler handler) {
register(type, -1, handler);
}
/**
* Allows the registration of a MIME part handler without content type from
* the first MIME part until the given part.
*
* @param handler the handler which handles the request.
*/
public void register(int partMax, MIMEHandler handler) {
register(null, partMax, handler);
}
/**
* Allows the registration of a MIME part handler without content type for
* the MIME parts from given start until the given end.
*
* @param handler the handler which handles the request.
*/
public void register(int partMin, int partMax, MIMEHandler handler) {
register(null, partMin, partMax, handler);
}
/**
* Allows the registration of a MIME part handler with content type for the
* MIME parts from a given start until a given end.
*
* @param handler the handler which handles the request.
*/
public void register(InternetMediaType type, int partMax, MIMEHandler handler) {
register(type, 1, partMax, handler);
}
/**
* Allows the registration of a MIME part handler with content type for the
* MIME parts from a given start until a given end.
*
* @param handler the handler which handles the request.
*/
public void register(InternetMediaType type, int partMin, int partMax, MIMEHandler handler) {
if (partMax != -1) {
for (int i = partMin; i <= partMax; i++) {
MappingEntry entry = new MappingEntry(type, i, i);
handlers.put(entry, handler);
}
} else {
MappingEntry entry = new MappingEntry(type, partMin, partMax);
handlers.put(entry, handler);
}
}
/**
* This class represents a MIME part with a given media type and part array.
*/
private class MappingEntry {
private InternetMediaType type = null;
private int partMin = 1;
private int partMax = -1;
MappingEntry(InternetMediaType type, int partMin, int partMax) {
this.type = type;
this.partMin = partMin;
this.partMax = partMax;
}
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getOuterType().hashCode();
result = prime * result + partMax;
result = prime * result + partMin;
result = prime * result + ((type == null) ? 0 : type.hashCode());
return result;
}
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
MappingEntry other = (MappingEntry) obj;
if (!getOuterType().equals(other.getOuterType())) return false;
if (partMax != other.partMax) return false;
if (partMin != other.partMin) return false;
if (type == null) {
if (other.type != null) return false;
} else if (!type.equals(other.type)) return false;
return true;
}
private DefaultMIMEHandler getOuterType() {
return DefaultMIMEHandler.this;
}
}
/*
* (non-Javadoc)
* @see
* org.ws4d.java.communication.protocol.http.server.HTTPRequestHandler#handle
* (org.ws4d.java.types.uri.URI,
* org.ws4d.java.communication.protocol.http.header.HTTPRequestHeader,
* java.io.InputStream)
*/
public HTTPResponse handle(URI request, HTTPRequestHeader header, InputStream body, ProtocolData protocolData, MonitoringContext context) throws IOException {
Queue responseContainer = new Queue();
String mediaType = header.getHeaderFieldValue(HTTPConstants.HTTP_HEADER_CONTENT_TYPE);
InternetMediaType type = new InternetMediaType(mediaType);
String boundary = type.getParameter(MIMEConstants.PARAMETER_BOUNDARY);
if (boundary == null) {
return new MIMEoverHTTPResponse(header, responseContainer);
}
ContextID mimeContext = new ContextID(protocolData.getInstanceId().longValue(), mimeMessageNumber++);
protocolData.setCurrentMIMEContext(mimeContext);
Sync incomingMIMEPartLock = new Sync();
Sync responseLock = new Sync();
MIMEReader reader = new MIMEReader(body, boundary.getBytes(), incomingMIMEPartLock);
final InternalHandler handler = new InternalHandler(reader, incomingMIMEPartLock, responseLock, responseContainer, protocolData, context);
DPWSFramework.getThreadPool().execute(handler);
synchronized (responseLock) {
while (!responseLock.isNotified()) {
try {
responseLock.wait();
IOException e = handler.getIOException();
if (e != null) {
throw e;
}
} catch (InterruptedException e) {
Log.printStackTrace(e);
}
}
}
/*
* Checks for queued responses. Gets the first one and checks for an
* special HTTP response.
*/
if (responseContainer.size() > 0) {
MIMEEntityOutput me = (MIMEEntityOutput) responseContainer.checkFirst();
HTTPResponse response = me.getHTTPResponse();
if (response != null) {
response.setMIMEReaderToWaitFor(reader);
return response;
}
}
return new MIMEoverHTTPResponse(header, responseContainer, reader);
}
/*
* (non-Javadoc)
* @see
* org.ws4d.java.communication.protocol.http.HTTPResponseHandler#handle(
* org.ws4d.java.communication.protocol.http.header.HTTPResponseHeader,
* java.io.InputStream,
* org.ws4d.java.communication.protocol.http.HTTPRequest,
* org.ws4d.java.communication.DPWSProtocolData,
* org.ws4d.java.io.monitor.MonitoringContext)
*/
public void handle(HTTPResponseHeader header, InputStream body, HTTPRequest request, ProtocolData protocolData, MonitoringContext context) throws IOException {
String mediaType = header.getHeaderFieldValue(HTTPConstants.HTTP_HEADER_CONTENT_TYPE);
InternetMediaType type = new InternetMediaType(mediaType);
String boundary = type.getParameter(MIMEConstants.PARAMETER_BOUNDARY);
if (boundary != null) {
ContextID mimeContext = new ContextID(protocolData.getInstanceId().longValue(), mimeMessageNumber++);
protocolData.setCurrentMIMEContext(mimeContext);
Sync streamLock = new Sync();
MIMEReader reader = new MIMEReader(body, boundary.getBytes(), streamLock);
handleInternal(reader, streamLock, null, null, protocolData, context);
}
}
/**
* Internal method for MIME part handling.
*
* @param reader the MIME reader.
* @param incomingMIMEPartLock the sync to use for signaling between
* threads.
* @param responseLock the sync to use for signaling between threads.
* @param responseContainer the queue for the responses.
* @param protocolData the protocol data.
* @param context the monitoring context.
* @throws IOException
*/
private void handleInternal(MIMEReader reader, Sync incomingMIMEPartLock, Sync responseLock, Queue responseContainer, ProtocolData protocolData, MonitoringContext context) throws IOException {
while (reader.nextPart()) {
MIMEHandler handler = getHandler(reader);
MIMEEntityInput input = new IncomingMIMEEntitiy(reader);
if (Log.isDebug()) {
MIMEBodyHeader header = input.getEntityHeader();
Log.debug("Reading incoming MIME with " + header);
}
StreamConsumerThread isc = new StreamConsumerThread(input, handler, responseLock, responseContainer, protocolData, context);
// only the first part may trigger the responseLock
if (reader.getPartNumber() == 1) {
responseLock = null;
}
/*
* Wait until the current part is fully read.
*/
incomingMIMEPartLock.reset();
synchronized (incomingMIMEPartLock) {
while (!incomingMIMEPartLock.isNotified()) {
try {
DPWSFramework.getThreadPool().execute(isc);
incomingMIMEPartLock.wait();
} catch (InterruptedException e) {
}
}
Exception e = incomingMIMEPartLock.getException();
if (e != null) {
if (e instanceof IOException) {
throw (IOException) e;
} else {
Log.error("A problem occured during MIME read. " + e.getMessage());
}
}
}
}
}
/**
* Returns a MIME part handler for the current part.
* <p>
* Tries to find the handler based on this conditions:<br />
* <ul>
* <li>Content-Type, exact part number</li>
* <li>Content-Type, part number and following parts</li>
* <li>No Content-Type, exact part number</li>
* <li>No Content-Type, part number and following parts</li>
* </ul>
* </p>
*
* @param reader the MIME reader.
* @return the handler.
*/
private MIMEHandler getHandler(MIMEReader reader) {
int part = reader.getPartNumber();
MIMEBodyHeader header = reader.getMIMEBodyHeader();
String mediaType = header.getHeaderFieldValue(MIMEConstants.MIME_HEADER_CONTENT_TYPE);
InternetMediaType type = null;
if (mediaType != null) {
type = new InternetMediaType(mediaType);
}
/*
* Tries to find handler for THIS part with THIS content-type.
*/
MappingEntry entry = new MappingEntry(type, part, part);
MIMEHandler handler = (MIMEHandler) handlers.get(entry);
if (handler != null) return handler;
/*
* Tries to find handler for THIS part with no content-type.
*/
entry = new MappingEntry(null, part, part);
handler = (MIMEHandler) handlers.get(entry);
if (handler != null) return handler;
/*
* Tries to find handler for THIS part with THIS content-type and every
* part afterwards.
*/
for (int i = part; i >= 1; i--) {
entry = new MappingEntry(type, i, -1);
handler = (MIMEHandler) handlers.get(entry);
if (handler != null) return handler;
}
/*
* Tries to find handler for THIS part with no content-type and every
* part afterwards.
*/
for (int i = part; i >= 1; i--) {
entry = new MappingEntry(null, i, -1);
handler = (MIMEHandler) handlers.get(entry);
if (handler != null) return handler;
}
/*
* No handler found ...
*/
return null;
}
/**
* Thread which allows the handling of incoming MIME parts. This thread is
* necessary to simultaneously allow MIME handling and the sending of a
* response.
*/
private class InternalHandler implements Runnable {
private MIMEReader reader;
private Sync incomingMIMEPartLock;
private Sync responseLock;
private Queue responses;
private ProtocolData protocolData;
private MonitoringContext context;
private IOException exception;
InternalHandler(MIMEReader reader, Sync incomingMIMEPartLock, Sync responseLock, Queue responses, ProtocolData protocolData, MonitoringContext context) {
this.reader = reader;
this.incomingMIMEPartLock = incomingMIMEPartLock;
this.responseLock = responseLock;
this.responses = responses;
this.protocolData = protocolData;
this.context = context;
}
public void run() {
try {
handleInternal(reader, incomingMIMEPartLock, responseLock, responses, protocolData, context);
} catch (IOException e) {
exception = e;
}
}
public IOException getIOException() {
return exception;
}
}
/**
* This thread allows a MIME part handler to read the bytes from the stream.
*/
private class StreamConsumerThread implements Runnable {
private MIMEHandler handler;
private MIMEEntityInput input;
private Queue responseContainer;
private final MonitoringContext context;
private final ProtocolData protocolData;
private Sync responseLock;
StreamConsumerThread(MIMEEntityInput input, MIMEHandler handler, Sync responseLock, Queue queue, ProtocolData protocolData, MonitoringContext context) {
this.handler = handler;
this.input = input;
this.responseContainer = queue;
this.protocolData = protocolData;
this.context = context;
this.responseLock = responseLock;
}
public void run() {
if (handler != null) {
try {
if (responseContainer == null) {
handler.handleResponse(input, protocolData, context);
} else {
handler.handleRequest(input, responseContainer, protocolData, context);
if (responseLock != null) {
synchronized (responseLock) {
responseLock.notifyNow();
}
}
}
} catch (IOException e) {
int n = 0;
try {
InputStream in = input.getBodyInputStream();
while (in.read() != -1) {
n++;
}
Log.warn("MIME part could not be handled by " + handler.getClass().getName() + ". " + n + " bytes omitted.");
} catch (IOException e1) {
Log.error("Could not consume omitted bytes from MIME part.");
}
}
} else {
int n = 0;
try {
InputStream in = input.getBodyInputStream();
while (in.read() != -1) {
n++;
}
Log.warn("No handler found for this MIME part. " + n + " bytes omitted.");
} catch (IOException e1) {
Log.error("Could not consume omitted bytes from MIME part.");
}
}
}
}
private class IncomingMIMEEntitiy implements MIMEEntityInput {
private MIMEReader reader = null;
IncomingMIMEEntitiy(MIMEReader reader) {
this.reader = reader;
}
public InputStream getBodyInputStream() {
return reader.getInputStream();
}
public MIMEBodyHeader getEntityHeader() {
return reader.getMIMEBodyHeader();
}
}
private class MIMEoverHTTPResponse extends HTTPResponse {
private HTTPResponseHeader responseHeader = null;
private Queue responses = null;
MIMEoverHTTPResponse(HTTPRequestHeader requestHeader, Queue responses, MIMEReader reader) {
this(requestHeader, responses);
setMIMEReaderToWaitFor(reader);
}
MIMEoverHTTPResponse(HTTPRequestHeader requestHeader, Queue responses) {
this.responses = responses;
if (responses.size() == 0) {
/*
* No response?! Okkkay!
*/
responseHeader = HTTPResponseUtil.getResponseHeader(202);
responseHeader.addHeaderFieldValue(HTTPConstants.HTTP_HEADER_CONTENT_LENGTH, "0");
responseHeader.addHeaderFieldValue("JMEDS-Debug", requestHeader.getRequest());
} else if (responses.size() == 1) {
/*
* One response? We MUST NOT send the response as MIME.
*/
MIMEEntityOutput response = (MIMEEntityOutput) responses.checkFirst();
responseHeader = HTTPResponseUtil.getResponseHeader(200);
MIMEBodyHeader mimeHeader = response.getEntityHeader();
String contentType = mimeHeader.getHeaderFieldValue(MIMEConstants.MIME_HEADER_CONTENT_TYPE);
responseHeader.addHeaderFieldValue(HTTPConstants.HTTP_HEADER_CONTENT_TRANSFER_ENCODING, HTTPConstants.HTTP_HEADERVALUE_TRANSFERCODING_CHUNKED);
responseHeader.addHeaderFieldValue(HTTPConstants.HTTP_HEADER_CONTENT_TYPE, contentType);
responseHeader.addHeaderFieldValue("JMEDS-Debug", requestHeader.getRequest());
} else {
/*
* Many responses? We MUST send the response as MIME.
*/
responseHeader = HTTPResponseUtil.getResponseHeader(200);
/*
* TODO: Search for content length and set correct length
*/
responseHeader.addHeaderFieldValue(HTTPConstants.HTTP_HEADER_CONTENT_TRANSFER_ENCODING, HTTPConstants.HTTP_HEADERVALUE_TRANSFERCODING_CHUNKED);
responseHeader.addHeaderFieldValue(HTTPConstants.HTTP_HEADER_CONTENT_TYPE, getMimeType().toString());
responseHeader.addHeaderFieldValue("JMEDS-Debug", requestHeader.getRequest());
}
}
/*
* (non-Javadoc)
* @see
* org.ws4d.java.communication.protocol.http.HTTPResponse#getResponseHeader
* ()
*/
public HTTPResponseHeader getResponseHeader() {
return responseHeader;
}
/*
* (non-Javadoc)
* @seeorg.ws4d.java.communication.protocol.http.HTTPResponse#
* serializeResponseBody(org.ws4d.java.types.uri.URI,
* org.ws4d.java.communication.protocol.http.header.HTTPRequestHeader,
* java.io.OutputStream, org.ws4d.java.communication.DPWSProtocolData)
*/
public void serializeResponseBody(URI request, HTTPRequestHeader header, OutputStream out, ProtocolData protocolData, MonitoringContext context) throws IOException {
if (responses.size() == 0) {
return;
} else if (responses.size() == 1) {
/*
* One response? We MUST NOT send the response as MIME.
*/
MIMEEntityOutput response = (MIMEEntityOutput) responses.get();
response.serialize(out);
out.flush();
} else {
/*
* Many responses? We MUST send the response as MIME.
*/
String b = getMimeType().getParameter(MIMEConstants.PARAMETER_BOUNDARY);
while (!responses.isEmpty()) {
MIMEEntityOutput response = (MIMEEntityOutput) responses.get();
MIMEBodyHeader mimeHeader = response.getEntityHeader();
MIMEUtil.writeBoundary(out, b.getBytes(), false, false);
mimeHeader.toStream(out);
response.serialize(out);
}
MIMEUtil.writeBoundary(out, b.getBytes(), false, true);
}
}
}
}