/*- ******************************************************************************* * Copyright (c) 2011, 2015 Diamond Light Source Ltd. * 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 * * Contributors: * Matthew Gerring - initial API and implementation and/or initial documentation *******************************************************************************/ package org.eclipse.dawnsci.remotedataset.server.slice; import java.awt.image.BufferedImage; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.imageio.ImageIO; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSessionBindingEvent; import javax.servlet.http.HttpSessionBindingListener; import org.eclipse.dawnsci.analysis.api.io.IDataHolder; import org.eclipse.dawnsci.plotting.api.histogram.HistogramBound; import org.eclipse.dawnsci.plotting.api.histogram.IImageService; import org.eclipse.dawnsci.plotting.api.histogram.ImageServiceBean; import org.eclipse.dawnsci.plotting.api.histogram.ImageServiceBean.HistoType; import org.eclipse.dawnsci.plotting.api.histogram.ImageServiceBean.ImageOrigin; import org.eclipse.dawnsci.remotedataset.Constants; import org.eclipse.dawnsci.remotedataset.Format; import org.eclipse.dawnsci.remotedataset.ServiceHolder; import org.eclipse.dawnsci.remotedataset.server.utils.DataServerUtils; import org.eclipse.january.dataset.IDataset; import org.eclipse.january.dataset.IDynamicDataset; import org.eclipse.january.dataset.ILazyDataset; import org.eclipse.january.dataset.Random; import org.eclipse.january.dataset.Slice; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.PaletteData; import org.eclipse.swt.graphics.RGB; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * There are one of these objects per session. * * So a user only blocks their own session if they * do an unfriendly slice. * * Parameters which may be set in the request: * Essential * ========= * path` - path to file or directory for loader factory to use. * * Optional * ======== * dataset - dataset name, by default the dataset at position 0 will be returned * * slice` - Provides the slice in the form of that required org.eclipse.dawnsci.analysis.api.dataset.Slice.convertFromString(...) * for example: [0,:1024,:1024]. If left unset and data not too large, will send while dataset, no slice. * * bin` - downsample As in Downsample.encode(...) / Downsample.decode(...) ; examples: 'MEAN:2x3', 'MAXIMUM:2x2' * by default no downsampling is done * * format` - One of Format.values(): * DATA - Serialized slice, binary (default) * JPG - JPG made using IImageService to make the image * PNG - PNG made using IImageService to make the image * MJPG:<dim> e.g. MJPG:0 to send the first dimension as slices in a series. NOTE slice mist be set in this case. * MDATA:<dim> e.g. MDATA:0 to send the first dimension as slices in a series as IDatasets. NOTE slice mist be set in this case. * * histo` - Encoding of histo to the rules of ImageServiceBean.encode(...) / ImageServiceBean.decode(...) * Example: "MEAN", "OUTLIER_VALUES:5-95" * Only used when an actual image is requested. * * sleep - Time to sleep between sending images, default 100ms. * * `URL encoded. * * * * Example in GET format (POST is also ok): * * http://localhost:8080/slice/?path=c%3A/Work/results/TomographyDataSet.hdf5&dataset=/entry/exchange/data&slice=[0,%3A1024,%3A1024] * * Or in a browser: * http://localhost:8080/slice/?path=c%3A/Work/results/TomographyDataSet.hdf5&dataset=/entry/exchange/data&slice=[0,%3A1024,%3A1024]&bin=MAXIMUM:2x2&format=JPG * http://localhost:8080/slice/?path=c%3A/Work/results/TomographyDataSet.hdf5&dataset=/entry/exchange/data&slice=[0,%3A1024,%3A1024]&bin=MAXIMUM:2x2&format=MJPG:0 * * @author Matthew Gerring * */ class SliceRequest implements HttpSessionBindingListener { private ReentrantLock lock; private String sessionId; private static Logger logger = LoggerFactory.getLogger(SliceRequest.class); // Actually the SliceRequest SliceRequest(String sessionId) { this.lock = new ReentrantLock(); this.sessionId = sessionId; } public void slice(HttpServletRequest request, HttpServletResponse response) throws Exception { try { lock.lock(); // Blocks so that one thread at a time does the slice for a given session. doSlice(request, response); } finally { lock.unlock(); } } protected void doSlice(HttpServletRequest request, HttpServletResponse response) throws Exception { final String path = decode(request.getParameter("path")); final String dataset = decode(request.getParameter("dataset")); ILazyDataset lz = getLazyDataset(path, dataset); lz.clearMetadata(null); final String slice = decode(request.getParameter("slice")); final Slice[] slices = slice!=null ? Slice.convertFromString(slice) : null; Format format = Format.getFormat(decode(request.getParameter("format"))); String bin = decode(request.getParameter("bin")); // We set the meta data as header an switch(format) { case DATA: sendObject(getData(lz, slices, bin), response); break; case JPG: case PNG: sendImage(getData(lz, slices, bin), request, response, format); break; case MJPG: // In the case of MJPG, we loop over doSlice(...) case MDATA: // In the case of MDATA, we loop over doSlice(...) sendImages(lz, slices, request, response, format); break; default: throw new Exception("Cannot process format: "+format); } } private final static Pattern RANDOM = Pattern.compile("RANDOM\\:(\\d)+x(\\d)+"); private ILazyDataset getLazyDataset(String path, String dataset) throws Exception { Matcher m = RANDOM.matcher(path); if (m.matches()) { String[] ints = path.split("\\:")[1].split("x"); int[] shape = new int[ints.length]; for (int i = 0; i < ints.length; i++) shape[i] = Integer.parseInt(ints[i]); return Random.lazyRand("Test random data", shape); } final File file = new File(path); // Can we see the file using the local file system? if (!file.exists()) throw new IOException("Path '"+path+"' does not exist!"); final IDataHolder holder = DataServerUtils.getDataHolderWithLogging(path); final ILazyDataset lz = dataset!=null ? holder.getLazyDataset(dataset) : holder.getLazyDataset(0); // In order to pass RemoteDataset.testRemoteSlicingUsingSliceND() this is required. if (lz instanceof IDynamicDataset) { ((IDynamicDataset)lz).refreshShape(); } if (dataset!=null && lz==null) throw new Exception("Dataset '"+dataset+"' not found in data holder!"); return lz; } private IDataset getData(ILazyDataset lz, Slice[] slices, String bin) throws Exception { long startTime = System.currentTimeMillis(); IDataset data = slices!=null ? lz.getSlice(slices) : null; // We might load all the data if it is not too large if (data==null && lz.getRank()<3) data = lz.getSlice(); // Loads all data if (data==null) throw new Exception("Cannot get slice of data for '"+lz+"'"); long endTime = System.currentTimeMillis()-startTime; if (endTime > 100 && endTime < 500) { logger.info("Read of data slice {} from {} took {} ms", Slice.createString(slices), lz.getName(), endTime); } else if (endTime >= 500) { logger.warn("Read of data slice {} from {} took {} ms",Slice.createString(slices), lz.getName(), endTime); } // We downsample if there was one if (bin!=null) { data = data.squeeze(); data = ServiceHolder.getDownService().downsample(bin, data).get(0); } return data; } private void sendImages(ILazyDataset lz, Slice[] slices, HttpServletRequest request, HttpServletResponse response, Format format) throws Exception { response.setStatus(HttpServletResponse.SC_OK); String delemeter_str = getClass().getName()+Long.toHexString(System.currentTimeMillis()); response.setContentType(Constants.MCONTENT_TYPE+";boundary=" + delemeter_str); ImageServiceBean bean = createImageServiceBean(); // Override histo if they set it. String histo = decode(request.getParameter("histo")); if (histo!=null) bean.decode(histo); String bin = decode(request.getParameter("bin")); String sleepStr = decode(request.getParameter("sleep")); if (sleepStr == null|| "".equals(sleepStr)) sleepStr = "100"; // Traditional GDA sleep 100! int sleep = Integer.parseInt(sleepStr); OutputStream out = new BufferedOutputStream(response.getOutputStream(), 100000); byte[] mcontent_type = ("Content-Type: "+Constants.MCONTENT_TYPE+";boundary="+delemeter_str).getBytes("UTF-8"); byte[] delimiter = ("--"+delemeter_str).getBytes("UTF-8"); final String mimeType = format==Format.MJPG ? Constants.JPG_TYPE : Constants.OBJECT_TYPE; byte[] content_type = ("Content-Type: "+mimeType).getBytes("UTF-8"); try { if (isIE(request)) { out.write(mcontent_type); out.write(Constants.CRLF); out.write(Constants.CRLF); out.write(Constants.CRLF); } final int size = slices!=null ? lz.getShape()[format.getDimension()] : Integer.MAX_VALUE; final int from = slices!=null ? slices[format.getDimension()].getStart() : 0; // If no slice, stream forever. for (int i = from; i < size; i++) { if (slices!=null){ slices[format.getDimension()].setStart(i); slices[format.getDimension()].setStop(i+1); } IDataset data = getData(lz, slices, bin); if (data.getRank()!=2 && data.getRank()!=1) { throw new Exception("The data used to make an image must either be 1D or 2D!"); } final byte[] frame = getFrame(data, bean, format); byte[] content_length = ("Content-Length: " + frame.length).getBytes("UTF-8"); out.write(delimiter); out.write(Constants.CRLF); out.write(content_type); out.write(Constants.CRLF); out.write(content_length); out.write(Constants.CRLF); out.write(Constants.CRLF); out.write(frame); out.write(Constants.CRLF); out.write(Constants.CRLF); out.flush(); TimeUnit.MILLISECONDS.sleep(sleep); } } catch (Exception ne) { ne.printStackTrace(); throw ne; } finally { if (out!=null) out.close(); } } private boolean isIE(HttpServletRequest request) { String userAgent = request.getHeader("User-Agent"); return !userAgent.toLowerCase().contains("firefox") && !userAgent.toLowerCase().contains("chrome"); } private byte[] getFrame(IDataset data, ImageServiceBean bean, Format format) throws Exception { ByteArrayOutputStream stream = null; if (format == Format.MJPG) { bean.setImage(data); IImageService service = ServiceHolder.getImageService(); if (service == null) { throw new NullPointerException("Image service not set"); } final ImageData imdata = service.getImageData(bean); final BufferedImage image = service.getBufferedImage(imdata); stream = new ByteArrayOutputStream(); ImageIO.write(image, "jpg", stream); } else if (format == Format.MDATA) { stream = new ByteArrayOutputStream(); final ObjectOutputStream oout = new ObjectOutputStream(stream); try { oout.writeObject(data); } finally { oout.close(); } } return stream.toByteArray(); } private void sendImage(IDataset data, HttpServletRequest request, HttpServletResponse response, Format format) throws Exception { data.squeeze(); if (data.getRank()!=2 && data.getRank()!=1) { throw new Exception("The data used to make an image must either be 1D or 2D!"); } response.setContentType("image/jpeg"); response.setStatus(HttpServletResponse.SC_OK); ImageServiceBean bean = createImageServiceBean(); bean.setImage(data); // Override histo if they set it. String histo = decode(request.getParameter("histo")); if (histo!=null) bean.decode(histo); IImageService service = ServiceHolder.getImageService(); if (service == null) { throw new NullPointerException("Image service not set"); } final ImageData imdata = service.getImageData(bean); final BufferedImage image = service.getBufferedImage(imdata); ImageIO.write(image, format.getImageIOString(), response.getOutputStream()); } private void sendObject(IDataset data, HttpServletResponse response) throws Exception { response.setContentType("application/zip"); response.setStatus(HttpServletResponse.SC_OK); response.setHeader("elementClass", data.getElementClass().toString()); final ObjectOutputStream ostream = new ObjectOutputStream(response.getOutputStream()); try { // We remove the origin metadata because the reference // to the original dataset is not desirable. data.clearMetadata(null); ostream.writeObject(data); } catch (Exception ne) { logger.error("Error writing object to output stream",ne); throw ne; } finally { ostream.flush(); } } private ImageServiceBean createImageServiceBean() { ImageServiceBean imageServiceBean = new ImageServiceBean(); imageServiceBean.setPalette(makeGrayScalePalette()); imageServiceBean.setOrigin(ImageOrigin.TOP_LEFT); imageServiceBean.setMinimumCutBound(HistogramBound.DEFAULT_MINIMUM); imageServiceBean.setMaximumCutBound(HistogramBound.DEFAULT_MAXIMUM); imageServiceBean.setNanBound(HistogramBound.DEFAULT_NAN); imageServiceBean.setHistogramType(HistoType.OUTLIER_VALUES); imageServiceBean.setLo(5); imageServiceBean.setHi(95); return imageServiceBean; } /** * Make 256 level grayscale palette. */ public static PaletteData makeGrayScalePalette() { RGB grayscale[] = new RGB[256]; for (int i = 0; i < 256; i++) { grayscale[i] = new RGB(i, i, i); } return new PaletteData(grayscale); } private String decode(String value) throws UnsupportedEncodingException { if (value==null) return null; return URLDecoder.decode(value, "UTF-8"); } @Override public void valueBound(HttpSessionBindingEvent event) { // TODO Auto-generated method stub } @Override public void valueUnbound(HttpSessionBindingEvent event) { // TODO Auto-generated method stub } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((sessionId == null) ? 0 : sessionId.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; SliceRequest other = (SliceRequest) obj; if (sessionId == null) { if (other.sessionId != null) return false; } else if (!sessionId.equals(other.sessionId)) return false; return true; } public void start() { logger.info(">>>>>> Slice Request Started"); } public void stop() { logger.info(">>>>>> Slice Request Stopped"); } }