/******************************************************************************* * 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); } } } }