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.search; 020 021import javax.servlet.ServletException; 022import javax.servlet.http.HttpServlet; 023import javax.servlet.http.HttpServletRequest; 024import javax.servlet.http.HttpServletResponse; 025 026import java.io.IOException; 027import java.util.*; 028 029import java.util.Map.Entry; 030import java.util.concurrent.ExecutionException; 031 032import net.sf.json.JSONArray; 033import org.apache.commons.collections.ArrayStack; 034import org.json.JSONException; 035import org.json.JSONWriter; 036import org.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038 039import net.sf.json.JSONObject; 040 041import org.apache.commons.lang3.StringUtils; 042 043import pt.ua.dicoogle.core.QueryExpressionBuilder; 044import pt.ua.dicoogle.core.dim.DIMGeneric; 045import pt.ua.dicoogle.plugins.PluginController; 046import pt.ua.dicoogle.sdk.datastructs.SearchResult; 047import pt.ua.dicoogle.sdk.task.JointQueryTask; 048import pt.ua.dicoogle.sdk.task.Task; 049 050/** 051 * Search the DICOM metadata, perform queries on images. Returns the data in JSON. 052 * 053 * @author Frederico Silva <fredericosilva@ua.pt> 054 * @author Eduardo Pinho <eduardopinho@ua.pt> 055 */ 056public class SearchServlet extends HttpServlet { 057 private static final Logger logger = LoggerFactory.getLogger(SearchServlet.class); 058 059 private static final long serialVersionUID = 1L; 060 061 private final Collection<String> DEFAULT_FIELDS = Arrays.asList( 062 "SOPInstanceUID", "StudyInstanceUID", "SeriesInstanceUID", "PatientID", 063 "PatientName", "PatientSex", "Modality", "StudyDate", 064 "StudyID", "StudyDescription", "SeriesNumber", "SeriesDescription", 065 "InstitutionName", "uri"); 066 067 public enum SearchType { 068 ALL, PATIENT; 069 } 070 private final SearchType searchType; 071 public SearchServlet(){ 072 searchType = SearchType.ALL; 073 } 074 public SearchServlet(SearchType stype){ 075 if(stype == null) 076 searchType = SearchType.ALL; 077 else 078 searchType = stype; 079 } 080 081 @Override 082 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 083 /* 084 Example: http://localhost:8080/search?query=wrix&keyword=false&provider=lucene&psize=10&offset=10 085 */ 086 response.setContentType("application/json"); 087 088 String query = request.getParameter("query"); 089 String providers[] = request.getParameterValues("provider"); 090 boolean keyword = Boolean.parseBoolean(request.getParameter("keyword")); 091 String[] fields = request.getParameterValues("field"); 092 093 final int psize; 094 final int offset; 095 final int depth; 096 try { 097 psize = getReqParameter(request, "psize", Integer.MAX_VALUE); 098 if (psize < 0) throw new NumberFormatException(); 099 } catch (NumberFormatException e) { 100 sendError(response, 400, "Invalid parameter psize: must be a non-negative integer"); 101 return; 102 } 103 try { 104 offset = getReqParameter(request, "offset", 0); 105 if (offset < 0) throw new NumberFormatException(); 106 } catch (NumberFormatException e) { 107 sendError(response, 400, "Invalid parameter offset: must be a non-negative integer"); 108 return; 109 } 110 String paramDepth = request.getParameter("depth"); 111 if (paramDepth != null) { 112 if (this.searchType != SearchType.PATIENT) { 113 sendError(response, 400, "Parameter depth is only applicable to /searchDIM endpoint"); 114 return; 115 } 116 switch (paramDepth.toLowerCase()) { 117 case "none": depth = 0; break; 118 case "patient": depth = 1; break; 119 case "study": depth = 2; break; 120 case "series": depth = 3; break; 121 case "image": depth = 4; break; 122 default: 123 sendError(response, 400, "Invalid parameter depth: must be a valid level: " 124 + "'none', 'patient', 'study', 'series' or 'image'"); 125 return; 126 } 127 } else { 128 depth = 4; 129 } 130 131 // retrieve desired fields 132 Set<String> actualFields; 133 if (fields == null || fields.length == 0) { 134 actualFields = null; 135 } else { 136 actualFields = new HashSet<>(Arrays.asList(fields)); 137 } 138 139 if (StringUtils.isEmpty(query)) { 140 sendError(response, 400, "No query supplied!"); 141 return; 142 } 143 144 if (!keyword) { 145 QueryExpressionBuilder q = new QueryExpressionBuilder(query); 146 query = q.getQueryString(); 147 } 148 149 List<String> providerList; 150 boolean queryAllProviders = false; 151 if (providers == null || providers.length == 0) { 152 queryAllProviders = true; 153 } else { 154 providerList = Arrays.asList(providers); 155 if (providerList.isEmpty()) { 156 queryAllProviders = true; 157 } 158 } 159 160 List<String> knownProviders = null; 161 if (!queryAllProviders) 162 { 163 knownProviders = new ArrayList<>(); 164 List<String> activeProviders = PluginController.getInstance().getQueryProvidersName(true); 165 for (String p : providers) 166 { 167 if (activeProviders.contains(p)) 168 { 169 knownProviders.add(p); 170 } 171 else 172 { 173 response.setStatus(400); 174 JSONObject obj = new JSONObject(); 175 obj.put("error", p.toString() +" is not a valid query provider"); 176 response.getWriter().append(obj.toString()); 177 return; 178 } 179 } 180 } 181 182 HashMap<String, String> extraFields = new HashMap<>(); 183 if (actualFields == null) { 184 185 //attaches the required extrafields 186 for (String field : DEFAULT_FIELDS) { 187 extraFields.put(field, field); 188 } 189 } else { 190 for (String f : actualFields) { 191 extraFields.put(f, f); 192 } 193 } 194 195 JointQueryTask queryTaskHolder = new JointQueryTask() { 196 197 @Override 198 public void onCompletion() { 199 } 200 201 @Override 202 public void onReceive(Task<Iterable<SearchResult>> e) { 203 } 204 }; 205 206 try { 207 long elapsedTime = System.currentTimeMillis(); 208 Iterable<SearchResult> results; 209 if (queryAllProviders) { 210 results = PluginController.getInstance().queryAll(queryTaskHolder, query, extraFields).get(); 211 } 212 213 else { 214 results = PluginController.getInstance().query(queryTaskHolder, knownProviders, query, extraFields).get(); 215 } 216 217 if (this.searchType == SearchType.PATIENT) { 218 try { 219 DIMGeneric dimModel = new DIMGeneric(results, depth); 220 elapsedTime = System.currentTimeMillis() - elapsedTime; 221 dimModel.writeJSON(response.getWriter(), elapsedTime, depth, offset, psize); 222 } catch (Exception e) { 223 logger.warn("Failed to get DIM", e); 224 } 225 } else { 226 elapsedTime = System.currentTimeMillis() - elapsedTime; 227 this.writeResponse(response, results, elapsedTime, offset, psize); 228 } 229 230 } catch (InterruptedException | ExecutionException | RuntimeException ex) { 231 logger.error("Failed to retrieve results", ex); 232 sendError(response, 500, "Could not generate results"); 233 return; 234 } catch (JSONException e) { 235 logger.error("Failed to serialize results", e); 236 return; 237 } 238 } 239 240 private void writeResponse(HttpServletResponse resp, Iterable<SearchResult> results, long elapsedTime, int offset, int psize) throws IOException, JSONException { 241 JSONWriter writer = new JSONWriter(resp.getWriter()); 242 writer.object(); //begin output 243 // results 244 writer.key("results").array(); // begin results 245 int count = 0; 246 for (Iterator<SearchResult> it = results.iterator(); it.hasNext(); ++count) { 247 SearchResult res = it.next(); 248 if (count < offset || count >= offset + psize) continue; 249 writer.object() // begin result 250 .key("uri").value(res.getURI().toString()) 251 .key("fields").object(); 252 for (Map.Entry<String, Object> e : res.getExtraData().entrySet()) { 253 writer.key(e.getKey()).value(String.valueOf(e.getValue()).trim()); 254 } 255 writer.endObject().endObject(); // end result 256 } 257 // other fields 258 writer.endArray() // end results 259 .key("elapsedTime").value(elapsedTime) 260 .key("numResults").value(count); 261 writer.endObject(); // end output 262 } 263 264 private int getReqParameter(HttpServletRequest req, String name, int defaultValue) throws NumberFormatException { 265 String param = req.getParameter(name); 266 int val = defaultValue; 267 if (param != null) { 268 val = Integer.parseInt(param); 269 } 270 return val; 271 } 272 273 private static void sendError(HttpServletResponse resp, int code, String message) throws IOException { 274 resp.setStatus(code); 275 JSONObject obj = new JSONObject(); 276 obj.put("results", new JSONArray()); 277 obj.put("error", message); 278 resp.getWriter().append(obj.toString()); 279 } 280}