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}