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}