org.cellprofiler.subimager.ImageWriterHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.cellprofiler.subimager.ImageWriterHandler.java

Source

/**
 * CellProfiler is distributed under the GNU General Public License.
 * See the accompanying file README for details.
 *
 * Developed by the Broad Institute
 * Copyright 2003-2011
 * Website: http://www.cellprofiler.org
 */
package org.cellprofiler.subimager;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;

import loci.common.DataTools;
import loci.common.services.DependencyException;
import loci.common.services.ServiceException;
import loci.common.services.ServiceFactory;
import loci.formats.FormatException;
import loci.formats.ImageWriter;
import loci.formats.meta.IMetadata;
import loci.formats.services.OMEXMLService;

import ome.xml.model.enums.PixelType;

import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.FileUpload;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.MultipartStream.MalformedStreamException;
import org.apache.commons.fileupload.RequestContext;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.log4j.Logger;

import static org.cellprofiler.subimager.SubimagerUtils.reportError;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;

/**
 * @author Lee Kamentsky
 * 
 * The writer handler accepts a POST request with a multipart/form-data content type.
 * 
 * The following parts are required:
 * 
 * An image encoded to the standards described in {@link org.cellprofiler.subimager.NDImage}
 *     Content-Disposition: form-data; name="image"
 *     Content-Type: application/octet-stream
 *     
 * OME-XML (see http://git.openmicroscopy.org/src/develop/components/specification/Documentation/Generated/OME-2011-06/ome.html)
 *     Content-Disposition: form-data; name="omexml"
 *     Content-Type: text/xml
 *     
 * The URL-encoded URL of the target location for the file:
 *     Content-Disposition: form-data; name="url"
 *     
 * The following parts are optional:
 * 
 * The index number of the plane being saved:
 *     Content-Disposition: form-data; name="index"
 *
 * Compression:
 *     Content-Disposition: form-data; name="compression"
 *     
 *     Compression choices are somewhat ad-hoc with "uncompressed" being
 *     supported by all. Others are LZW, zlib and JPEG2000.
 *     
 * The image should be scaled to match the range of the data type chosen inside the OME-XML.
 * For instance, if saved in UINT8, the image should have values between 0 and 255.0.
 * Values below the range of the data type chosen are stored as the type minimum
 * and values above the range are stored as the type maximum.
 */
@SuppressWarnings("restriction")
public class ImageWriterHandler implements HttpHandler {
    static final private String MULTIPART_FORM_DATA = "multipart/form-data";
    static final private String BOUNDARY_EQUALS = "boundary=";
    static final private String NAME_IMAGE = "image";
    static final private String NAME_OMEXML = "omexml";
    static final private String NAME_URL = "url";
    static final private String NAME_INDEX = "index";
    static final private String NAME_COMPRESSION = "compression";
    static final private String DEFAULT_COMPRESSION = "default";
    static final private String TYPE_BINARY = "application/octet-stream";
    static final private List<PixelType> SUPPORTED_PIXEL_TYPES = Arrays
            .asList(new PixelType[] { PixelType.DOUBLE, PixelType.FLOAT, PixelType.INT8, PixelType.INT16,
                    PixelType.INT32, PixelType.UINT8, PixelType.UINT16, PixelType.UINT32 });
    static final private String[] PREFERRED_COMPRESSION = { "LZW", "JPEG2000", "zlib" };
    static final Logger logger = Logger.getLogger(ImageWriterHandler.class);

    /* (non-Javadoc)
     * @see com.sun.net.httpserver.HttpHandler#handle(com.sun.net.httpserver.HttpExchange)
     */
    public void handle(HttpExchange exchange) throws IOException {
        if (!exchange.getRequestMethod().equals("POST")) {
            reportError(exchange, HttpURLConnection.HTTP_BAD_METHOD,
                    "<html><body>writeimage only accepts HTTP post</body></html>");
            return;
        }
        final String contentType = exchange.getRequestHeaders().getFirst("Content-Type");
        if (contentType == null) {
            reportError(exchange, HttpURLConnection.HTTP_BAD_REQUEST,
                    "<html><body>No Content-type request header</body></html>");
            return;
        }
        if (!contentType.startsWith(MULTIPART_FORM_DATA)) {
            reportError(exchange, HttpURLConnection.HTTP_BAD_REQUEST,
                    "<html><body>Content type must be " + MULTIPART_FORM_DATA + ".</body></html>");
            return;
        }
        int idx = contentType.indexOf(BOUNDARY_EQUALS, MULTIPART_FORM_DATA.length());
        if (idx == -1) {
            reportError(exchange, HttpURLConnection.HTTP_BAD_REQUEST,
                    "<html><body>Did not find boundary identifier in multipart/form-data content type.</body></html>");
            return;
        }
        final String contentEncoding = exchange.getRequestHeaders().getFirst("Content-Encoding");
        String contentLengthString = exchange.getRequestHeaders().getFirst("Content-Length");
        if (contentLengthString == null) {
            reportError(exchange, HttpURLConnection.HTTP_BAD_REQUEST,
                    "<html><body>No Content-Length request header</body></html>");
            return;
        }
        try {
            Integer.parseInt(contentLengthString);
        } catch (NumberFormatException e) {
            reportError(exchange, HttpURLConnection.HTTP_BAD_REQUEST, String
                    .format("<html><body>Content length was not a number: %s</body></html>", contentLengthString));
            return;
        }
        final int contentLength = Integer.parseInt(contentLengthString);
        final InputStream requestBodyStream = exchange.getRequestBody();
        FileUpload upload = new FileUpload();
        FileItemIterator fileItemIterator;
        String omeXML = null;
        URI uri = null;
        int index = 0;
        NDImage ndimage = null;
        String compression = DEFAULT_COMPRESSION;
        try {
            fileItemIterator = upload.getItemIterator(new RequestContext() {

                public String getCharacterEncoding() {
                    return contentEncoding;
                }

                public String getContentType() {
                    return contentType;
                }

                public int getContentLength() {
                    return contentLength;
                }

                public InputStream getInputStream() throws IOException {
                    return requestBodyStream;
                }
            });
            while (fileItemIterator.hasNext()) {
                FileItemStream fis = fileItemIterator.next();
                String name = fis.getFieldName();

                if (name.equals(NAME_IMAGE)) {
                    String imageContentType = fis.getContentType();
                    if (imageContentType == null) {
                        reportError(exchange, HttpURLConnection.HTTP_UNSUPPORTED_TYPE,
                                "<html><body>Image form-data field must have a content type.</body></html>");
                        return;
                    }
                    try {
                        InputStream is = SubimagerUtils.getInputStream(exchange, fis);
                        if (is == null)
                            return;
                        ndimage = NDImage.decode(is);
                    } catch (MalformedStreamException e) {
                        reportError(exchange, HttpURLConnection.HTTP_BAD_REQUEST,
                                "<html><body>Failed to read body for part, " + name + ".</body></html>");
                    } catch (IOException e) {
                        reportError(exchange, HttpURLConnection.HTTP_INTERNAL_ERROR,
                                "<html><body>Failed to read multipart body data</body></html>");
                        return;
                    } catch (org.cellprofiler.subimager.NDImage.FormatException e) {
                        reportError(exchange, HttpURLConnection.HTTP_BAD_REQUEST,
                                "<html><body>Image data was not in correct format: " + e.getMessage()
                                        + "</body></html>");
                        return;
                    }
                } else {
                    String partDataString = SubimagerUtils.readFully(exchange, fis);
                    if (partDataString == null)
                        return;
                    if (name.equals(NAME_INDEX)) {
                        try {
                            index = Integer.parseInt(partDataString);
                        } catch (NumberFormatException e) {
                            reportError(exchange, HttpURLConnection.HTTP_BAD_REQUEST,
                                    "<html><body>Index form-data field must be a number: " + partDataString
                                            + ".</body></html>");
                            return;
                        }
                    } else if (name.equals(NAME_OMEXML)) {
                        omeXML = partDataString;
                    } else if (name.equals(NAME_URL)) {
                        try {
                            uri = new URI(partDataString);
                        } catch (URISyntaxException e) {
                            reportError(exchange, HttpURLConnection.HTTP_BAD_REQUEST,
                                    "<html><body>Improperly formatted URL: " + partDataString + ".</body></html>");
                            return;
                        }
                    } else if (name.equals(NAME_COMPRESSION)) {
                        compression = partDataString;
                    }
                }
            }
            if (ndimage == null) {
                reportError(exchange, HttpURLConnection.HTTP_BAD_REQUEST,
                        "<html><body>Request did not have an image part</body></html>");
                return;
            }
            if (uri == null) {
                reportError(exchange, HttpURLConnection.HTTP_BAD_REQUEST,
                        "<html><body>Request did not have a URL part</body></html>");
                return;
            }
            if (omeXML == null) {
                reportError(exchange, HttpURLConnection.HTTP_BAD_REQUEST,
                        "<html><body>Request did not have an omexml part</body></html>");
                return;
            }
            writeImage(exchange, ndimage, uri, omeXML, index, compression);
        } catch (FileUploadException e) {
            reportError(exchange, HttpURLConnection.HTTP_BAD_REQUEST, String
                    .format("<html><body>Parsing error in multipart message: %s</body></html>", e.getMessage()));
            return;
        }
    }

    /**
     * Write the image plane to the given uri
     * 
     * @param exchange the HttpExchange for the connection
     * @param ndimage the image to write
     * @param uri the file URI to write to (must be a file URI currently)
     * @param omeXML the OME-XML metadata for the plane
     * @param index the planar index of the plane being written to the file. Note that the indices must be written in order and that an index of 0 will truncate the file.
     * @param compression the compression method to be used
     * @throws IOException
     */
    private void writeImage(HttpExchange exchange, NDImage ndimage, URI uri, String omeXML, int index,
            String compression) throws IOException {
        if (!uri.getScheme().equals("file")) {
            reportError(exchange, HttpURLConnection.HTTP_NOT_IMPLEMENTED,
                    "<html><body>This server currently only supports the file: protocol, url=" + uri.toString()
                            + "</body></html>");
            return;
        }
        File outputFile = new File(uri);
        if ((index == 0) && outputFile.exists()) {
            outputFile.delete();
        }
        IMetadata metadata = null;
        try {
            ServiceFactory factory;
            factory = new ServiceFactory();
            OMEXMLService service = factory.getInstance(OMEXMLService.class);
            metadata = service.createOMEXMLMetadata(omeXML);
        } catch (DependencyException e) {
            reportError(exchange, HttpURLConnection.HTTP_INTERNAL_ERROR,
                    "<html><body>Configuration error: could not create OMEXML service - check for missing omexml libraries</body></html>");
            return;
        } catch (ServiceException e) {
            reportError(exchange, HttpURLConnection.HTTP_INTERNAL_ERROR,
                    "<html><body>Possible OME-XML parsing error: " + e.getMessage() + "</body></html>");
            return;
        }
        ImageWriter writer = new ImageWriter();
        writer.setMetadataRetrieve(metadata);
        try {
            writer.setId(outputFile.getAbsolutePath());
        } catch (IOException e) {
            reportError(exchange, HttpURLConnection.HTTP_INTERNAL_ERROR,
                    "<html><body>Failed to open output file " + outputFile.getAbsolutePath() + "</body></html>");
            return;
        } catch (FormatException e) {
            reportError(exchange, HttpURLConnection.HTTP_INTERNAL_ERROR,
                    "<html><body>Format exception when opening output file: " + e.getMessage() + "</body></html>");
            return;
        }
        PixelType pixelType = metadata.getPixelsType(0);
        if (SUPPORTED_PIXEL_TYPES.indexOf(pixelType) == -1) {
            reportError(exchange, HttpURLConnection.HTTP_BAD_REQUEST,
                    "<html><body>Unsupported pixel type: " + pixelType.getValue() + "</body></html>");
            return;
        }
        boolean toBigEndian = metadata.getPixelsBinDataBigEndian(0, 0);
        writer.setInterleaved(true);
        List<String> compressionTypes = Arrays.asList(writer.getCompressionTypes());
        try {
            if (compression == DEFAULT_COMPRESSION) {
                for (String possibleCompression : PREFERRED_COMPRESSION) {
                    if (compressionTypes.indexOf(possibleCompression) != -1) {
                        //writer.setCompression(possibleCompression);
                        break;
                    }
                }
            } else {
                if (compressionTypes.indexOf(compression) == -1) {
                    reportError(exchange, HttpURLConnection.HTTP_BAD_REQUEST,
                            "<html><body>Unsupported compression type: " + compression + "</body></html>");
                    return;
                }
                writer.setCompression(compression);
            }
        } catch (FormatException e) {
            reportError(exchange, HttpURLConnection.HTTP_INTERNAL_ERROR,
                    "<html><body>Error when setting compression type: " + e.getMessage() + "</body></html>");
            return;
        }
        byte[] buffer = convertImage(ndimage, pixelType, toBigEndian);
        try {
            writer.saveBytes(index, buffer);
            writer.close();
        } catch (IOException e) {
            reportError(exchange, HttpURLConnection.HTTP_INTERNAL_ERROR,
                    "<html><body>An I/O error prevented the server from writing the image: " + e.getMessage()
                            + "</body></html>");
            return;
        } catch (FormatException e) {
            reportError(exchange, HttpURLConnection.HTTP_INTERNAL_ERROR,
                    "<html><body>The imaging library failed to write the image because of a format error: "
                            + e.getMessage() + "</body></html>");
            return;
        }
        String html = String.format("<html><body>%s successfully written</body></html>",
                StringEscapeUtils.escapeHtml(outputFile.getAbsolutePath()));
        reportError(exchange, HttpURLConnection.HTTP_OK, html);
    }

    private byte[] convertImage(NDImage ndimage, PixelType pixelType, boolean toBigEndian) {
        double[] inputDouble = ndimage.getBuffer();
        switch (pixelType) {
        case INT8:
            return convertToIntegerType(inputDouble, Byte.MIN_VALUE, Byte.MAX_VALUE, Byte.SIZE / 8, toBigEndian);
        case UINT8:
            return convertToIntegerType(inputDouble, 0, (1L << Byte.SIZE) - 1, Byte.SIZE / 8, toBigEndian);
        case INT16:
            return convertToIntegerType(inputDouble, Short.MIN_VALUE, Short.MAX_VALUE, Short.SIZE / 8, toBigEndian);
        case UINT16:
            return convertToIntegerType(inputDouble, 0, (1L << Short.SIZE) - 1, Short.SIZE / 8, toBigEndian);
        case INT32:
            return convertToIntegerType(inputDouble, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.SIZE / 8,
                    toBigEndian);
        case UINT32:
            return convertToIntegerType(inputDouble, 0, (1L << Integer.SIZE) - 1, Integer.SIZE / 8, toBigEndian);
        case FLOAT: {
            int bpp = Float.SIZE / 8;
            byte[] buffer = new byte[inputDouble.length * bpp];
            for (int i = 0; i < inputDouble.length; i++) {
                DataTools.unpackBytes(Float.floatToIntBits((float) inputDouble[i]), buffer, i * bpp, bpp,
                        !toBigEndian);
            }
            return buffer;
        }
        case DOUBLE: {
            int bpp = Double.SIZE / 8;
            byte[] buffer = new byte[inputDouble.length * bpp];
            for (int i = 0; i < inputDouble.length; i++) {
                DataTools.unpackBytes(Double.doubleToLongBits(inputDouble[i]), buffer, i * bpp, bpp, !toBigEndian);
            }
            return buffer;
        }
        default:
            throw new UnsupportedOperationException("The pixel type, " + pixelType.getValue()
                    + ", should have been explicitly handled by the caller and an error should have been reported to the web client.");
        }
    }

    private byte[] convertToIntegerType(double[] buffer, long min, long max, int bpp, boolean toBigEndian) {
        byte[] outputData = new byte[buffer.length * bpp];
        for (int i = 0; i < buffer.length; i++) {
            long value = (buffer[i] < min) ? min : (buffer[i] > max) ? max : (long) buffer[i];
            DataTools.unpackBytes(value, outputData, i * bpp, bpp, !toBigEndian);
        }
        return outputData;
    }

}