001/**
002 * Copyright (C) 2014  Universidade de Aveiro, DETI/IEETA, Bioinformatics Group - http://bioinformatics.ua.pt/
003 *
004 * This file is part of Dicoogle/dicoogle.
005 *
006 * Dicoogle/dicoogle is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * Dicoogle/dicoogle is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with Dicoogle.  If not, see <http://www.gnu.org/licenses/>.
018 */
019package pt.ua.dicoogle.server.web.servlets;
020
021import java.io.ByteArrayOutputStream;
022import java.io.PrintWriter;
023
024import javax.servlet.ServletException;
025import javax.servlet.http.HttpServlet;
026import javax.servlet.http.HttpServletRequest;
027import javax.servlet.http.HttpServletResponse;
028
029import java.io.IOException;
030import java.io.InputStream;
031import java.net.URI;
032import java.net.URISyntaxException;
033import java.util.Arrays;
034import java.util.Iterator;
035import java.util.List;
036import java.util.concurrent.ExecutionException;
037
038import javax.servlet.ServletOutputStream;
039
040import net.sf.json.JSONObject;
041import org.apache.commons.io.IOUtils;
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044import pt.ua.dicoogle.core.ServerSettings;
045import pt.ua.dicoogle.plugins.PluginController;
046import pt.ua.dicoogle.sdk.StorageInputStream;
047import pt.ua.dicoogle.sdk.StorageInterface;
048import pt.ua.dicoogle.sdk.datastructs.SearchResult;
049import pt.ua.dicoogle.sdk.task.JointQueryTask;
050import pt.ua.dicoogle.sdk.task.Task;
051import pt.ua.dicoogle.server.web.dicom.Convert2PNG;
052import pt.ua.dicoogle.server.web.dicom.Information;
053import pt.ua.dicoogle.server.web.utils.LocalImageCache;
054
055/**
056 * Handles the requests for DICOM frames, returning them as PNG images.
057 * Also maintains a cache of the images already served to speed-up the next requests (minimizing server load by doing way less conversions).
058 *
059 * @author Antonio
060 * @author Eduardo Pinho <eduardopinho@ua.pt>
061 */
062public class ImageServlet extends HttpServlet
063{
064    private static final Logger logger = LoggerFactory.getLogger(ImageServlet.class);
065    private static final long serialVersionUID = 1L;
066
067    public static final int BUFFER_SIZE = 1500; // byte size for read-write ring bufer, optimized for regular TCP connection windows
068
069    private final LocalImageCache cache;
070        
071    /**
072     * Creates an image servlet.
073     *
074     * @param cache the local image caching system, can be null and if so no caching mechanism will be used.
075     */
076    public ImageServlet(LocalImageCache cache) {
077        this.cache = cache;
078    }
079    
080        @Override
081        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
082        {
083                String sopInstanceUID = request.getParameter("SOPInstanceUID");
084                String uri = request.getParameter("uri");
085        boolean thumbnail = Boolean.valueOf(request.getParameter("thumbnail"));
086        
087                if (sopInstanceUID == null) {
088            if (uri == null) {
089                response.sendError(400, "URI or SOP Instance UID not provided");
090                return;
091            }
092        } else if (sopInstanceUID.trim().isEmpty()) {
093                        response.sendError(400, "Invalid SOP Instance UID!");
094                        return;
095                }
096                String[] providerArray = request.getParameterValues("provider");
097        List<String> providers = providerArray == null ? null : Arrays.asList(providerArray);
098                String sFrame = request.getParameter("frame");
099        int frame;
100                if (sFrame == null) {
101            frame = 0;
102        } else {
103            frame = Integer.parseInt(sFrame);
104        }
105        
106        StorageInputStream imgFile;
107        if (sopInstanceUID != null) {
108            // get the image file for that SOP Instance UID
109            imgFile = getFileFromSOPInstanceUID(sopInstanceUID, providers);
110            // if no .dcm file was found tell the client
111            if (imgFile == null) {
112                response.sendError(404, "No image file for supplied SOP Instance UID!");
113                return;
114            }
115        } else {
116            try {
117                // get the image file by the URI
118                URI imgUri = new URI(uri);
119                StorageInterface storageInt = PluginController.getInstance().getStorageForSchema(imgUri);
120                Iterator<StorageInputStream> storages = storageInt.at(imgUri).iterator();
121                // take the first valid storage
122                if (!storages.hasNext()) {
123                    response.sendError(404, "No image file for supplied URI!");
124                    return;
125                }
126                imgFile = storages.next();
127                 
128            } catch (URISyntaxException ex) {
129                response.sendError(400, "Bad URI syntax");
130                return;
131            }
132        }
133
134                // if there is a cache available then use it
135                if (cache != null && cache.isRunning()) {
136
137            try {
138                InputStream istream = cache.get(imgFile.getURI(), frame, thumbnail);
139                response.setContentType("image/png");
140                try(ServletOutputStream out = response.getOutputStream()) {
141                    IOUtils.copy(istream, out);
142                }
143            } catch (IOException ex) {
144                logger.warn("Could not convert the image", ex);
145                response.sendError(500);
146            } catch (RuntimeException ex) {
147                logger.error("Unexpected exception", ex);
148                response.sendError(500);
149            }
150            
151                } else {
152            // if the cache is invalid or not running convert the image and return it "on-the-fly"
153            try {
154                ByteArrayOutputStream pngStream = getPNGStream(imgFile, frame, thumbnail);
155                response.setContentType("image/png"); // set the appropriate type for the PNG image
156                response.setContentLength(pngStream.size()); // set the image size
157                try (ServletOutputStream out = response.getOutputStream()) {
158                    pngStream.writeTo(out);
159                    pngStream.flush();
160                }
161            } catch (IOException ex) {
162                logger.warn("Could not convert the image", ex);
163                response.sendError(500, "Could not convert the image");
164            }
165                }
166    }
167    
168    private ByteArrayOutputStream getPNGStream(StorageInputStream imgFile, int frame, boolean thumbnail) throws IOException {
169        ByteArrayOutputStream pngStream;
170        if (thumbnail) {
171            int thumbSize;
172            try {
173                // retrieve thumbnail dimension settings
174                thumbSize = Integer.parseInt(ServerSettings.getInstance().getThumbnailsMatrix());
175            } catch (NumberFormatException ex) {
176                logger.warn("Failed to parse ThumbnailMatrix, using default thumbnail size");
177                thumbSize = 64;
178            }
179            pngStream = Convert2PNG.DICOM2ScaledPNGStream(imgFile, frame, thumbSize, thumbSize);
180        } else {
181            pngStream = Convert2PNG.DICOM2PNGStream(imgFile, frame);
182        }
183        return pngStream;
184    }
185
186        @Override
187        protected void doPost(HttpServletRequest req, HttpServletResponse resp)
188                        throws ServletException, IOException {
189        // TODO this service does not make sense as a POST.
190        // either remove or relocate to another resource
191        
192                String sop = req.getParameter("SOPInstanceUID");
193                if(sop == null){
194                        resp.sendError(500, "No SOPInstanceUID in Request");
195                        return;
196                }
197
198                float frameRate = Information.getFrameRateFromImage(sop);
199                if(frameRate == -1){
200                        resp.sendError(500, "Cannot Locate the image File.");
201                        return;
202                }
203                
204                int nFrames = Information.getNumberOfFramesInFile(sop);
205                
206                JSONObject r = new JSONObject();
207                r.put("SOPInstanceUID", sop);
208                r.put("NumberOfFrames", nFrames);
209                r.put("FrameRate", frameRate);
210                
211                resp.setContentType("application/json");
212                
213                PrintWriter wr = resp.getWriter();
214                wr.print(r.toString()); 
215        }
216        
217    private static StorageInputStream getFileFromSOPInstanceUID(String sopInstanceUID, List<String> providers) throws IOException {
218        // TODO use only DIM sources?
219        JointQueryTask qt = new JointQueryTask() {
220            @Override
221            public void onCompletion() {
222            }
223            @Override
224            public void onReceive(Task<Iterable<SearchResult>> e) {
225            }
226        };
227        try {
228            if (providers == null) {
229                providers = PluginController.getInstance().getQueryProvidersName(true);
230            }
231            Iterator<SearchResult> it = PluginController.getInstance()
232                    .query(qt, providers, "SOPInstanceUID:" + sopInstanceUID).get().iterator();
233            if (!it.hasNext()) {
234                throw new IOException("No such image of SOPInstanceUID " + sopInstanceUID);
235            }
236            SearchResult res = it.next();
237            StorageInterface storage = PluginController.getInstance().getStorageForSchema(res.getURI());
238            if (storage == null) {
239                throw new IOException("Unsupported file scheme");
240            }
241            Iterator<StorageInputStream> store = storage.at(res.getURI()).iterator();
242            if (!store.hasNext()) {
243                throw new IOException("No storage item found");
244            }
245            return store.next();
246        } catch (InterruptedException | ExecutionException ex) {
247            throw new IOException(ex);
248        }
249        
250    }
251        
252}