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.dicom; 020 021import static pt.ua.dicoogle.server.web.utils.Query.addExtraQueryParam; 022 023import java.io.UnsupportedEncodingException; 024import java.text.SimpleDateFormat; 025import java.util.ArrayList; 026import java.util.Calendar; 027import java.util.Collection; 028import java.util.Collections; 029import java.util.HashMap; 030import java.util.HashSet; 031import java.util.List; 032import java.util.Set; 033import java.util.concurrent.ExecutionException; 034import org.slf4j.LoggerFactory; 035 036import javax.servlet.ServletRequest; 037import javax.servlet.http.HttpServletRequest; 038import javax.servlet.http.HttpSession; 039 040import net.sf.json.JSONArray; 041import net.sf.json.JSONObject; 042import pt.ua.dicoogle.core.QueryExpressionBuilder; 043import pt.ua.dicoogle.plugins.PluginController; 044import pt.ua.dicoogle.sdk.utils.DictionaryAccess; 045import pt.ua.dicoogle.sdk.datastructs.SearchResult; 046import pt.ua.dicoogle.sdk.settings.Utils; 047import pt.ua.dicoogle.sdk.task.JointQueryTask; 048import pt.ua.dicoogle.sdk.task.Task; 049 050/** 051 * A simple class used by the Dicoogle Web interface that allow web users to 052 * have the same searching capabilities of desktop application users. 053 * 054 * The new version of these class generates the query tasks. These tasks take into 055 * account the multiple available providers. The search task is then placed in a SearchHolder 056 * especially created to the issuing user. This holder is accessible via /search/holders. 057 * 058 * @author Tiago Marques Godinho 059 * @author António Novo <antonio.novo@ua.pt> 060 */ 061public class Search { 062 private List<String> selectedProviders; 063 064 /** 065 * Is this an advanced search? 066 */ 067 private boolean isAdvanced; 068 069 /** 070 * Simple query string. 071 */ 072 private String simpleQuery; 073 /** 074 * If the simple query string is keyword based. 075 */ 076 private boolean keyworded; 077 078 /** 079 * How long did the search procedure take to complete, in milliseconds. 080 */ 081 private long timeTaken; 082 083 /** 084 * Advanced search params. 085 */ 086 private String patientName; 087 private String patientID; 088 private String patientGender; 089 private String institutionName; 090 private String physician; 091 private String operatorName; 092 private String studyDateFormat; 093 private String exactDate; 094 private boolean useStartDate; 095 private boolean useEndDate; 096 private String startDate; 097 private String endDate; 098 private boolean modCR; 099 private boolean modMG; 100 private boolean modPT; 101 private boolean modXA; 102 private boolean modES; 103 private boolean modCT; 104 private boolean modMR; 105 private boolean modRF; 106 private boolean modUS; 107 private boolean modDX; 108 private boolean modNM; 109 private boolean modSC; 110 private boolean modOT; 111 private boolean modOthers; 112 113 /** 114 * Final query string. 115 */ 116 private String finalQuery; 117 118 /** 119 * Lists of search results in various forms. 120 */ 121 private Collection<SearchResult> searchResults; 122 123 /** 124 * If the result list is supposed to have all the extra fields (servlet XML 125 * request) or just the minimum (webapp search). 126 */ 127 private boolean fullRequest; 128 129 private HttpSession httpSession; 130 131 private int searchID; 132 133 @SuppressWarnings("unchecked") 134 public Search(ServletRequest request) { 135 this.searchID = -1; 136 // reset all the internal properties 137 isAdvanced = false; 138 simpleQuery = null; 139 patientName = null; 140 patientID = null; 141 patientGender = null; 142 institutionName = null; 143 physician = null; 144 operatorName = null; 145 studyDateFormat = null; 146 exactDate = null; 147 useStartDate = false; 148 useEndDate = false; 149 startDate = null; 150 endDate = null; 151 modCR = false; 152 modMG = false; 153 modPT = false; 154 modXA = false; 155 modES = false; 156 modCT = false; 157 modMR = false; 158 modRF = false; 159 modUS = false; 160 modDX = false; 161 modNM = false; 162 modSC = false; 163 modOT = false; 164 modOthers = false; 165 finalQuery = null; 166 searchResults = null; 167 fullRequest = false; 168 169 // this ensures that any special characters won't be parsed incorrectly 170 try { 171 request.setCharacterEncoding("UTF-8"); 172 } catch (UnsupportedEncodingException ex) { 173 // do nothing 174 } 175 176 // get the query method (either default or advanced) 177 String method = request.getParameter("method"); 178 179 // see if we should parse the request as simple or advanced search 180 if ((method != null) && (!method.isEmpty())) { 181 if (method.equalsIgnoreCase("Advanced")) // advanced search 182 { 183 isAdvanced = true; 184 } 185 } 186 187 // and now mount the proper query 188 if (isAdvanced) { 189 mountAdvancedSearch(request); 190 } else { 191 mountSimpleSearch(request); 192 } 193 194 String qp = request.getParameter("queryProviders"); 195 196 System.out.println("QueryProviders: " + qp); 197 JSONArray arr = JSONArray.fromObject(qp); 198 199 System.out.println("QueryProviders: " + qp); 200 if (qp != null) { 201 this.selectedProviders = new ArrayList<>(); 202 this.selectedProviders.addAll(JSONArray.toCollection(arr, 203 String.class)); 204 } else { 205 this.selectedProviders = PluginController.getInstance() 206 .getQueryProvidersName(true); 207 } 208 this.httpSession = ((HttpServletRequest) request).getSession(false); 209 } 210 211 private void mountSimpleSearch(ServletRequest request) { 212 // get the only param of this simple query 213 simpleQuery = request.getParameter("query"); 214 keyworded = Utils.parseCheckBoxValue(request.getParameter("keywords")); 215 216 // parse the user query into an expression 217 if ((simpleQuery != null) && (!simpleQuery.trim().isEmpty())) // if the 218 // query 219 // is 220 // not 221 // empty... 222 { 223 // ... build a query expression based on it and get its resulting 224 // query string 225 if (!isKeyworded()) { 226 // write the QueryString respecting BNF grammer defined 227 // regarding Lucene documentation 2.4.X branch 228 QueryExpressionBuilder exp = new QueryExpressionBuilder( 229 simpleQuery); 230 finalQuery = exp.getQueryString(); 231 } else { 232 finalQuery = simpleQuery; 233 } 234 } else { 235 finalQuery = "*:*"; // default query string 236 } 237 } 238 239 private void mountAdvancedSearch(ServletRequest request) { 240 isAdvanced = true; 241 242 // get all the params defined on this advanced query 243 patientName = request.getParameter("patientName"); 244 patientID = request.getParameter("patientID"); 245 patientGender = request.getParameter("patientGender"); 246 institutionName = request.getParameter("institutionName"); 247 physician = request.getParameter("physician"); 248 operatorName = request.getParameter("operatorName"); 249 studyDateFormat = request.getParameter("studyDate"); 250 exactDate = request.getParameter("exactDate"); 251 useStartDate = Utils.parseCheckBoxValue(request 252 .getParameter("fromDate")); 253 useEndDate = Utils.parseCheckBoxValue(request.getParameter("toDate")); 254 startDate = request.getParameter("startDate"); 255 endDate = request.getParameter("endDate"); 256 modCR = Utils.parseCheckBoxValue(request.getParameter("modCR")); 257 modMG = Utils.parseCheckBoxValue(request.getParameter("modMG")); 258 modPT = Utils.parseCheckBoxValue(request.getParameter("modPT")); 259 modXA = Utils.parseCheckBoxValue(request.getParameter("modXA")); 260 modES = Utils.parseCheckBoxValue(request.getParameter("modES")); 261 modCT = Utils.parseCheckBoxValue(request.getParameter("modCT")); 262 modMR = Utils.parseCheckBoxValue(request.getParameter("modMR")); 263 modRF = Utils.parseCheckBoxValue(request.getParameter("modRF")); 264 modUS = Utils.parseCheckBoxValue(request.getParameter("modUS")); 265 modDX = Utils.parseCheckBoxValue(request.getParameter("modDX")); 266 modNM = Utils.parseCheckBoxValue(request.getParameter("modNM")); 267 modSC = Utils.parseCheckBoxValue(request.getParameter("modSC")); 268 modOT = Utils.parseCheckBoxValue(request.getParameter("modOT")); 269 modOthers = Utils.parseCheckBoxValue(request.getParameter("modOthers")); 270 271 // and form the query string 272 finalQuery = getAdvancedQuery(); 273 } 274 275 /** 276 * Returns a String with a valid query, that can be combined with others if 277 * needed, for searching for all modalities currently defined on the web 278 * interface (supported directly, minus the Others). 279 * 280 * @return a String with a valid query, that can be combined with others if 281 * needed, for searching for all modalities currently defined on the 282 * web interface (supported directly, minus the Others). 283 */ 284 private List<String> getAllDefinedModalitiesQuery() { 285 List<String> ret = new ArrayList<>(13); 286 287 ret.add("CR"); 288 ret.add("CT"); 289 ret.add("DX"); 290 ret.add("ES"); 291 ret.add("MG"); 292 ret.add("MR"); 293 ret.add("NM"); 294 ret.add("OT"); 295 ret.add("PT"); 296 ret.add("RF"); 297 ret.add("SC"); 298 ret.add("US"); 299 ret.add("XA"); 300 301 return ret; 302 } 303 304 /** 305 * Based on the set of optional advanced search params, returns a advanced 306 * query string that reflects those params. 307 * 308 * @return an advanced query string for the params that match their values. 309 */ 310 public String getAdvancedQuery() { 311 String result = ""; 312 313 // patient name 314 if ((patientName != null) && (!patientName.isEmpty())) { 315 result = addExtraQueryParam(result, "PatientName:(" + patientName 316 + ")"); 317 } 318 319 // patient id 320 if ((patientID != null) && (!patientID.isEmpty())) { 321 result = addExtraQueryParam(result, "PatientID:(" + patientID + ")"); 322 } 323 324 // patient gender 325 if ((patientGender != null) && (!patientGender.isEmpty()) 326 && (!isPatientGenderAll())) { 327 if (isPatientGenderMale()) { 328 result = addExtraQueryParam(result, "PatientSex:M"); 329 } else { 330 if (isPatientGenderFemale()) { 331 result = addExtraQueryParam(result, "PatientSex:F"); 332 } 333 } 334 } 335 336 // institution name 337 if ((institutionName != null) && (!institutionName.isEmpty())) { 338 result = addExtraQueryParam(result, "InstitutionName:(" 339 + institutionName + ")"); 340 } 341 342 // physician 343 if ((physician != null) && (!physician.isEmpty())) { 344 result = addExtraQueryParam(result, "(PerformingPhysicianName:(" 345 + physician + ") OR ReferringPhysicianName:(" + physician 346 + "))"); 347 } 348 349 // operator name 350 if ((operatorName != null) && (!operatorName.isEmpty())) { 351 result = addExtraQueryParam(result, "OperatorName:(" + operatorName 352 + ")"); 353 } 354 355 // study date 356 if ((studyDateFormat != null) && (!studyDateFormat.isEmpty())) { 357 if (studyDateFormat.equalsIgnoreCase("Exact")) { 358 if ((exactDate != null) && (!exactDate.isEmpty())) 359 result = addExtraQueryParam(result, "StudyDate:(" 360 + exactDate + ")"); 361 } else { 362 if (studyDateFormat.equalsIgnoreCase("Range")) { 363 String date = "StudyDate:["; 364 365 if (useStartDate && (startDate != null) 366 && (!startDate.isEmpty())) { 367 date += startDate; 368 } else { 369 date += "0000101"; 370 } 371 date += " TO "; 372 if (useEndDate && (endDate != null) && (!endDate.isEmpty())) { 373 date += endDate; 374 } else { 375 Calendar cal = Calendar.getInstance(); 376 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); 377 378 date += sdf.format(cal.getTime()); 379 } 380 381 date += "]"; 382 383 result = addExtraQueryParam(result, date); 384 } 385 } 386 } 387 388 // modalities 389 { 390 String modalities = ""; 391 Set<String> mods = new HashSet<String>(); 392 393 394 if (modCR) { 395 mods.add("CR"); 396 } 397 if (modCT) { 398 mods.add("CT"); 399 } 400 if (modDX) { 401 mods.add("DX"); 402 } 403 if (modES) { 404 mods.add("ES"); 405 } 406 if (modMG) { 407 mods.add("MG"); 408 } 409 if (modMR) { 410 mods.add("MR"); 411 } 412 if (modNM) { 413 mods.add("NM"); 414 } 415 if (modOT) { 416 mods.add("OT"); 417 } 418 if (modPT) { 419 mods.add("PT"); 420 } 421 if (modRF) { 422 mods.add("RF"); 423 } 424 if (modSC) { 425 mods.add("SC"); 426 } 427 if (modUS) { 428 mods.add("US"); 429 } 430 if (modXA) { 431 mods.add("XA"); 432 } 433 434 for(String s : mods){ 435 modalities+=s +" "; 436 } 437 438 if(modOthers){ 439 for(String s : getAllDefinedModalitiesQuery()){ 440 if(!mods.contains(s)) 441 modalities += "-"+s+" "; 442 } 443 } 444 445 // and add the modalities to the resulting query 446 if (!modalities.isEmpty()) { 447 result = addExtraQueryParam(result, "Modality:(" + modalities + ")"); 448 } 449 } 450 451 // if no "options" were modified then use the default query string 452 if (result.isEmpty()) { 453 result = "*:*"; 454 } 455 456 return result; 457 } 458 459 /** 460 * Returns if there is a query to be used (NOTE: an empty string is still a 461 * valid query!). 462 * 463 * @return if there is a query to be used. 464 */ 465 public boolean hasQuery() { 466 return ((!isAdvanced) && (simpleQuery != null)) || isAdvanced; 467 } 468 469 /** 470 * Returns the original query. 471 * 472 * @return the original query. 473 */ 474 public String getSimpleQuery() { 475 return simpleQuery; 476 } 477 478 /** 479 * Returns the final query. 480 * 481 * @return the final query. 482 */ 483 public String getFinalQuery() { 484 return finalQuery; 485 } 486 487 /** 488 * Returns a list with the search results for the query. 489 * 490 * @return a list with the search results for the query. 491 */ 492 @Deprecated 493 public Collection<SearchResult> getSearchResults() { 494 // if there is no valid query then abort 495 if (!hasQuery()) 496 return null; 497 498 // if the value is not cached then do the search and cache it 499 if (searchResults == null) { 500 // perform the search 501 long startime = System.nanoTime(); 502 searchResults = search(finalQuery, fullRequest); 503 long endTime = System.nanoTime(); 504 // update the time taken for the search to complete 505 timeTaken = (endTime - startime) / 1000000; 506 } 507 508 return searchResults; 509 } 510 511 /** 512 * Returns the Selected Query Providers in the JSON Format 513 * @return The selected Query Providers 514 */ 515 public String getQueryProvidersJSON() { 516 517 JSONArray arr = new JSONArray(); 518 519 for (String p : getQueryProviders()) { 520 JSONObject obj = new JSONObject(); 521 obj.put("name", p); 522 boolean sel = this.selectedProviders.contains(p); 523 obj.put("selected", sel); 524 arr.add(obj); 525 } 526 527 return arr.toString(); 528 } 529 530 /** 531 * Returns a List containing the Selected Query Providers 532 * @return The selected Query Providers 533 */ 534 public List<String> getQueryProviders() { 535 List<String> providers = PluginController.getInstance() 536 .getQueryProvidersName(true); 537 538 return providers; 539 } 540 541 /** 542 * @return 543 */ 544 public List<String> getSelectedProviders() { 545 return this.selectedProviders; 546 } 547 548 /** 549 * Places a new search in the SearchHolder 550 * @param query The query String 551 * @return The Given Query Identifier 552 */ 553 private int fireSearch(String query) { 554 if (selectedProviders.isEmpty()) 555 return -1; 556 557 HashMap<String, String> searchParam = new HashMap<>(); 558 559 searchParam.put("PatientName", "PatientName"); 560 searchParam.put("Modality", "Modality"); 561 searchParam.put("StudyDate", "StudyDate"); 562 searchParam.put("SOPInstanceUID", "SOPInstanceUID"); 563 searchParam.put("Thumbnail", "Thumbnail"); 564 searchParam.put("StudyDescription", "StudyDescription"); 565 searchParam.put("InstitutionName", "InstitutionName"); 566 searchParam.put("SeriesDescription", "SeriesDescription"); 567 searchParam.put("PatientID", "PatientID"); 568 searchParam.put("PatientSex", "PatientSex"); 569 570 if (httpSession == null) 571 return -1; 572 573 SearchHolder holder = (SearchHolder) httpSession 574 .getAttribute("dicoogle.web.queryHolder"); 575 if (holder == null) { 576 holder = new SearchHolder(); 577 httpSession.setAttribute("dicoogle.web.queryHolder", holder); 578 } 579 580 int id = holder.registerNewQuery(selectedProviders, query, searchParam); 581 582 return id; 583 } 584 585 /** 586 * Places the search in the Holder. If it has not been done before. 587 * @return 588 */ 589 public int placeSearchOrder() { 590 // if there is no valid query then abort 591 if (!hasQuery()) 592 return -1; 593 594 595 // performs the search, if it hasn't already 596 if(searchID == -1) 597 searchID = fireSearch(finalQuery); 598 599 return searchID; 600 } 601 602 /** 603 * Performs a simple search. 604 * 605 * @param query 606 * the user entered query. 607 * @return a list of SearchResult objects. 608 */ 609 @Deprecated 610 private Collection<SearchResult> search(String query, boolean fullRequest) { 611 if (selectedProviders.isEmpty()) 612 return Collections.emptyList(); 613 // get the search results for this query 614 // results = idx.searchSync(query, (fullRequest ? extraFieldsFull : 615 // extraFields)); 616 617 List<SearchResult> targetCollection = new ArrayList<SearchResult>(); 618 Iterable<SearchResult> itResults = null; 619 620 HashMap<String, String> searchParam = new HashMap<>(); 621 622 searchParam.put("PatientName", "PatientName"); 623 searchParam.put("Modality", "Modality"); 624 searchParam.put("StudyDate", "StudyDate"); 625 searchParam.put("SOPInstanceUID", "SOPInstanceUID"); 626 searchParam.put("Thumbnail", "Thumbnail"); 627 searchParam.put("StudyDescription", "StudyDescription"); 628 searchParam.put("InstitutionName", "InstitutionName"); 629 searchParam.put("SeriesDescription", "SeriesDescription"); 630 searchParam.put("PatientID", "PatientID"); 631 searchParam.put("PatientSex", "PatientSex"); 632 try { 633 // for(String provider : selectedProviders){ 634 // System.out.println("Searching in Provider: "+provider); 635 JointQueryTask t = new JointQueryTask() { 636 637 @Override 638 public void onReceive(Task<Iterable<SearchResult>> e) { 639 // TODO Auto-generated method stub 640 641 } 642 643 @Override 644 public void onCompletion() { 645 // TODO Auto-generated method stub 646 647 } 648 }; 649 itResults = PluginController.getInstance() 650 .query(t, selectedProviders, query, searchParam).get(); 651 652 // } 653 654 } catch (InterruptedException | ExecutionException ex) { 655 LoggerFactory.getLogger(Search.class).error(ex.getMessage(), ex); 656 } 657 658 if (itResults == null) 659 return Collections.emptyList(); 660 661 for (SearchResult s : itResults) { 662 targetCollection.add(s); 663 } 664 665 // and return them 666 return targetCollection; 667 } 668 669 /** 670 * @return the isAdvanced 671 */ 672 public boolean isAdvancedQuery() { 673 return isAdvanced; 674 } 675 676 /** 677 * @return the patientName 678 */ 679 public String getPatientName() { 680 return patientName; 681 } 682 683 /** 684 * @return the patientID 685 */ 686 public String getPatientID() { 687 return patientID; 688 } 689 690 /** 691 * @return the patientGender 692 */ 693 public String getPatientGender() { 694 return patientGender; 695 } 696 697 public boolean isPatientGenderAll() { 698 return (patientGender == null) 699 || patientGender.isEmpty() 700 || patientGender.equalsIgnoreCase("All") 701 || ((!patientGender.equalsIgnoreCase("Male")) && (!patientGender 702 .equalsIgnoreCase("Female"))); 703 } 704 705 public boolean isPatientGenderMale() { 706 return (patientGender != null) 707 && patientGender.equalsIgnoreCase("Male"); 708 } 709 710 public boolean isPatientGenderFemale() { 711 return (patientGender != null) 712 && patientGender.equalsIgnoreCase("Female"); 713 } 714 715 /** 716 * @return the institutionName 717 */ 718 public String getInstitutionName() { 719 return institutionName; 720 } 721 722 /** 723 * @return the physician 724 */ 725 public String getPhysician() { 726 return physician; 727 } 728 729 /** 730 * @return the operatorName 731 */ 732 public String getOperatorName() { 733 return operatorName; 734 } 735 736 /** 737 * @return the studyDateFormat 738 */ 739 public String getStudyDateFormat() { 740 return studyDateFormat; 741 } 742 743 public boolean isExactDate() { 744 return (studyDateFormat != null) && (!studyDateFormat.isEmpty()) 745 && studyDateFormat.equalsIgnoreCase("Exact"); 746 } 747 748 public boolean isRangedDate() { 749 return (studyDateFormat != null) && (!studyDateFormat.isEmpty()) 750 && studyDateFormat.equalsIgnoreCase("Range"); 751 } 752 753 /** 754 * @return the exactDate 755 */ 756 public String getExactDate() { 757 return exactDate; 758 } 759 760 /** 761 * @return the useStartDate 762 */ 763 public boolean isUseStartDate() { 764 return useStartDate; 765 } 766 767 /** 768 * @return the useEndDate 769 */ 770 public boolean isUseEndDate() { 771 return useEndDate; 772 } 773 774 /** 775 * @return the startDate 776 */ 777 public String getStartDate() { 778 return startDate; 779 } 780 781 /** 782 * @return the endDate 783 */ 784 public String getEndDate() { 785 return endDate; 786 } 787 788 /** 789 * @return the modCR 790 */ 791 public boolean isModCR() { 792 return modCR; 793 } 794 795 /** 796 * @return the modMG 797 */ 798 public boolean isModMG() { 799 return modMG; 800 } 801 802 /** 803 * @return the modPT 804 */ 805 public boolean isModPT() { 806 return modPT; 807 } 808 809 /** 810 * @return the modXA 811 */ 812 public boolean isModXA() { 813 return modXA; 814 } 815 816 /** 817 * @return the modES 818 */ 819 public boolean isModES() { 820 return modES; 821 } 822 823 /** 824 * @return the modCT 825 */ 826 public boolean isModCT() { 827 return modCT; 828 } 829 830 /** 831 * @return the modMR 832 */ 833 public boolean isModMR() { 834 return modMR; 835 } 836 837 /** 838 * @return the modRF 839 */ 840 public boolean isModRF() { 841 return modRF; 842 } 843 844 /** 845 * @return the modUS 846 */ 847 public boolean isModUS() { 848 return modUS; 849 } 850 851 /** 852 * @return the modDX 853 */ 854 public boolean isModDX() { 855 return modDX; 856 } 857 858 /** 859 * @return the modNM 860 */ 861 public boolean isModNM() { 862 return modNM; 863 } 864 865 /** 866 * @return the modSC 867 */ 868 public boolean isModSC() { 869 return modSC; 870 } 871 872 /** 873 * @return the modOT 874 */ 875 public boolean isModOT() { 876 return modOT; 877 } 878 879 /** 880 * @return the modOthers 881 */ 882 public boolean isModOthers() { 883 return modOthers; 884 } 885 886 /** 887 * @return the keyworded 888 */ 889 public boolean isKeyworded() { 890 return keyworded; 891 } 892 893 /** 894 * @return the time taken for the search to complete in milliseconds. 895 */ 896 public long getTimeTaken() { 897 return timeTaken; 898 } 899 900 public static String getAllTags(){ 901 List<String> tags = new ArrayList<String>( DictionaryAccess.getInstance().getTagList().keySet() ); 902 Collections.sort(tags); 903 JSONArray arr = JSONArray.fromObject(tags); 904 return arr.toString(); 905 } 906}