19 package org.sleuthkit.autopsy.keywordsearch;
 
   21 import java.util.ArrayList;
 
   22 import java.util.HashMap;
 
   23 import java.util.LinkedHashMap;
 
   24 import java.util.List;
 
   26 import java.util.TreeSet;
 
   27 import java.util.logging.Level;
 
   29 import org.openide.util.NbBundle;
 
   31 import org.apache.solr.client.solrj.SolrQuery;
 
   32 import org.apache.solr.client.solrj.SolrRequest.METHOD;
 
   33 import org.apache.solr.client.solrj.response.QueryResponse;
 
   34 import org.openide.util.NbBundle.Messages;
 
   44 class HighlightedText 
implements IndexedText, TextMarkupLookup {
 
   46     private static final Logger logger = Logger.getLogger(HighlightedText.class.getName());
 
   47     private static final String HIGHLIGHT_PRE = 
"<span style='background:yellow'>"; 
 
   48     private static final String HIGHLIGHT_POST = 
"</span>"; 
 
   49     private static final String ANCHOR_PREFIX = HighlightedText.class.getName() + 
"_";
 
   51     private long objectId;
 
   52     private String keywordHitQuery;
 
   53     private Server solrServer;
 
   54     private int numberPages;
 
   55     private int currentPage;
 
   56     private boolean isRegex = 
false;
 
   57     private boolean group = 
true;
 
   58     private boolean hasChunks = 
false;
 
   60     private LinkedHashMap<Integer, Integer> hitsPages;
 
   62     private HashMap<Integer, Integer> pagesToHits;
 
   63     private List<Integer> pages;
 
   64     private QueryResults hits = null; 
 
   65     private String originalQuery = null; 
 
   66     private boolean isPageInfoLoaded = 
false;
 
   67     private static final boolean DEBUG = (Version.getBuildType() == Version.Type.DEVELOPMENT);
 
   69     HighlightedText(
long objectId, String keywordHitQuery, 
boolean isRegex) {
 
   70         this.objectId = objectId;
 
   71         this.keywordHitQuery = keywordHitQuery;
 
   72         this.isRegex = isRegex;
 
   74         this.hitsPages = 
new LinkedHashMap<>();
 
   75         this.pages = 
new ArrayList<>();
 
   76         this.pagesToHits = 
new HashMap<>();
 
   78         this.solrServer = KeywordSearch.getServer();
 
   86     HighlightedText(
long objectId, String solrQuery, 
boolean isRegex, String originalQuery) {
 
   87         this(objectId, KeywordSearchUtil.quoteQuery(solrQuery), isRegex);
 
   88         this.originalQuery = originalQuery;
 
   91     HighlightedText(
long objectId, String solrQuery, 
boolean isRegex, QueryResults hits) {
 
   92         this(objectId, solrQuery, isRegex);
 
   96     HighlightedText(
long objectId, String solrQuery, 
boolean isRegex, 
boolean group, QueryResults hits) {
 
   97         this(objectId, solrQuery, isRegex, hits);
 
  105     @Messages({
"HighlightedText.query.exception.msg=Could not perform the query to get chunk info and get highlights:"})
 
  106     private void loadPageInfo() {
 
  107         if (isPageInfoLoaded) {
 
  111             this.numberPages = solrServer.queryNumFileChunks(this.objectId);
 
  112         } 
catch (KeywordSearchModuleException ex) {
 
  113             logger.log(Level.WARNING, 
"Could not get number pages for content: " + 
this.objectId); 
 
  115         } 
catch (NoOpenCoreException ex) {
 
  116             logger.log(Level.WARNING, 
"Could not get number pages for content: " + 
this.objectId); 
 
  120         if (this.numberPages == 0) {
 
  135                 String queryStr = KeywordSearchUtil.escapeLuceneQuery(this.keywordHitQuery);
 
  138                     queryStr = Server.Schema.CONTENT_WS + 
":" + 
"\"" + queryStr + 
"\"";
 
  141                 Keyword keywordQuery = 
new Keyword(queryStr, !isRegex);
 
  142                 List<Keyword> keywords = 
new ArrayList<>();
 
  143                 keywords.add(keywordQuery);
 
  144                 KeywordSearchQuery chunksQuery = 
new LuceneQuery(
new KeywordList(keywords), keywordQuery);
 
  146                 chunksQuery.addFilter(
new KeywordQueryFilter(FilterType.CHUNK, 
this.objectId));
 
  148                     hits = chunksQuery.performQuery();
 
  149                 } 
catch (KeywordSearchModuleException | NoOpenCoreException ex) {
 
  150                     logger.log(Level.SEVERE, 
"Could not perform the query to get chunk info and get highlights:" + keywordQuery.getSearchTerm(), ex); 
 
  151                     MessageNotifyUtil.Notify.error(Bundle.HighlightedText_query_exception_msg() + keywordQuery.getSearchTerm(), ex.getCause().getMessage());
 
  157             TreeSet<Integer> pagesSorted = 
new TreeSet<>();
 
  158             for (Keyword k : hits.getKeywords()) {
 
  159                 for (KeywordHit hit : hits.getResults(k)) {
 
  160                     int chunkID = hit.getChunkId();
 
  161                     if (chunkID != 0 && this.objectId == hit.getSolrObjectId()) {
 
  162                         pagesSorted.add(chunkID);
 
  168             if (pagesSorted.isEmpty()) {
 
  169                 this.currentPage = 0;
 
  171                 this.currentPage = pagesSorted.first();
 
  174             for (Integer page : pagesSorted) {
 
  175                 hitsPages.put(page, 0); 
 
  177                 pagesToHits.put(page, 0); 
 
  182             this.numberPages = 1;
 
  183             this.currentPage = 1;
 
  186             pagesToHits.put(1, 0);
 
  188         isPageInfoLoaded = 
true;
 
  192     private HighlightedText() {
 
  196         return this.objectId;
 
  200     public int getNumberPages() {
 
  201         return this.numberPages;
 
  207     public int getCurrentPage() {
 
  208         return this.currentPage;
 
  212     public boolean hasNextPage() {
 
  213         final int numPages = pages.size();
 
  214         int idx = pages.indexOf(this.currentPage);
 
  215         return idx < numPages - 1;
 
  220     public boolean hasPreviousPage() {
 
  221         int idx = pages.indexOf(this.currentPage);
 
  227     public int nextPage() {
 
  228         if (!hasNextPage()) {
 
  229             throw new IllegalStateException(
 
  230                     NbBundle.getMessage(
this.getClass(), 
"HighlightedMatchesSource.nextPage.exception.msg"));
 
  232         int idx = pages.indexOf(this.currentPage);
 
  233         currentPage = pages.get(idx + 1);
 
  238     public int previousPage() {
 
  239         if (!hasPreviousPage()) {
 
  240             throw new IllegalStateException(
 
  241                     NbBundle.getMessage(
this.getClass(), 
"HighlightedMatchesSource.previousPage.exception.msg"));
 
  243         int idx = pages.indexOf(this.currentPage);
 
  244         currentPage = pages.get(idx - 1);
 
  249     public boolean hasNextItem() {
 
  250         if (!this.pagesToHits.containsKey(currentPage)) {
 
  253         return this.pagesToHits.get(currentPage) < this.hitsPages.get(currentPage);
 
  257     public boolean hasPreviousItem() {
 
  258         if (!this.pagesToHits.containsKey(currentPage)) {
 
  261         return this.pagesToHits.get(currentPage) > 1;
 
  265     public int nextItem() {
 
  266         if (!hasNextItem()) {
 
  267             throw new IllegalStateException(
 
  268                     NbBundle.getMessage(
this.getClass(), 
"HighlightedMatchesSource.nextItem.exception.msg"));
 
  270         int cur = pagesToHits.get(currentPage) + 1;
 
  271         pagesToHits.put(currentPage, cur);
 
  276     public int previousItem() {
 
  277         if (!hasPreviousItem()) {
 
  278             throw new IllegalStateException(
 
  279                     NbBundle.getMessage(
this.getClass(), 
"HighlightedMatchesSource.previousItem.exception.msg"));
 
  281         int cur = pagesToHits.get(currentPage) - 1;
 
  282         pagesToHits.put(currentPage, cur);
 
  287     public int currentItem() {
 
  288         if (!this.pagesToHits.containsKey(currentPage)) {
 
  291         return pagesToHits.get(currentPage);
 
  295     public LinkedHashMap<Integer, Integer> getHitsPages() {
 
  296         return this.hitsPages;
 
  300     public String getText() {
 
  303         String highLightField = null;
 
  306             highLightField = LuceneQuery.HIGHLIGHT_FIELD_REGEX;
 
  308             highLightField = LuceneQuery.HIGHLIGHT_FIELD_LITERAL;
 
  311         SolrQuery q = 
new SolrQuery();
 
  312         q.setShowDebugInfo(DEBUG); 
 
  315         q.setQuery(keywordHitQuery);
 
  317         String contentIdStr = Long.toString(this.objectId);
 
  319             contentIdStr += 
"_" + Integer.toString(this.currentPage);
 
  322         final String filterQuery = Server.Schema.ID.toString() + 
":" + KeywordSearchUtil.escapeLuceneQuery(contentIdStr);
 
  323         q.addFilterQuery(filterQuery);
 
  324         q.addHighlightField(highLightField); 
 
  328         q.setHighlightFragsize(0); 
 
  331         q.setParam(
"hl.useFastVectorHighlighter", 
"on"); 
 
  332         q.setParam(
"hl.tag.pre", HIGHLIGHT_PRE); 
 
  333         q.setParam(
"hl.tag.post", HIGHLIGHT_POST); 
 
  334         q.setParam(
"hl.fragListBuilder", 
"single"); 
 
  337         q.setParam(
"hl.maxAnalyzedChars", Server.HL_ANALYZE_CHARS_UNLIMITED); 
 
  340             QueryResponse response = solrServer.query(q, METHOD.POST);
 
  341             Map<String, Map<String, List<String>>> responseHighlight = response.getHighlighting();
 
  343             Map<String, List<String>> responseHighlightID = responseHighlight.get(contentIdStr);
 
  344             if (responseHighlightID == null) {
 
  345                 return NbBundle.getMessage(this.getClass(), 
"HighlightedMatchesSource.getMarkup.noMatchMsg");
 
  347             List<String> contentHighlights = responseHighlightID.get(highLightField);
 
  348             if (contentHighlights == null) {
 
  349                 return NbBundle.getMessage(this.getClass(), 
"HighlightedMatchesSource.getMarkup.noMatchMsg");
 
  352                 String highlightedContent = contentHighlights.get(0).trim();
 
  353                 highlightedContent = insertAnchors(highlightedContent);
 
  355                 return "<html><pre>" + highlightedContent + 
"</pre></html>"; 
 
  357         } 
catch (Exception ex) {
 
  358             logger.log(Level.WARNING, 
"Error executing Solr highlighting query: " + keywordHitQuery, ex); 
 
  359             return NbBundle.getMessage(this.getClass(), 
"HighlightedMatchesSource.getMarkup.queryFailedMsg");
 
  364     public String toString() {
 
  365         return NbBundle.getMessage(this.getClass(), 
"HighlightedMatchesSource.toString");
 
  369     public boolean isSearchable() {
 
  374     public String getAnchorPrefix() {
 
  375         return ANCHOR_PREFIX;
 
  379     public int getNumberHits() {
 
  380         if (!this.hitsPages.containsKey(
this.currentPage)) {
 
  383         return this.hitsPages.get(this.currentPage);
 
  386     private String insertAnchors(String searchableContent) {
 
  387         int searchOffset = 0;
 
  390         StringBuilder buf = 
new StringBuilder(searchableContent);
 
  392         final String searchToken = HIGHLIGHT_PRE;
 
  393         final int indexSearchTokLen = searchToken.length();
 
  394         final String insertPre = 
"<a name='" + ANCHOR_PREFIX; 
 
  395         final String insertPost = 
"'></a>"; 
 
  397         while ((index = buf.indexOf(searchToken, searchOffset)) >= 0) {
 
  398             String insertString = insertPre + Integer.toString(count + 1) + insertPost;
 
  399             int insertStringLen = insertString.length();
 
  400             buf.insert(index, insertString);
 
  401             searchOffset = index + indexSearchTokLen + insertStringLen; 
 
  406         this.hitsPages.put(this.currentPage, count);
 
  407         if (this.currentItem() == 0 && this.hasNextItem()) {
 
  411         return buf.toString();
 
  414     private static TextMarkupLookup instance = null;
 
  418     public static synchronized TextMarkupLookup getDefault() {
 
  419         if (instance == null) {
 
  420             instance = 
new HighlightedText();
 
  427     public TextMarkupLookup createInstance(
long objectId, String keywordHitQuery, 
boolean isRegex, String originalQuery) {
 
  428         return new HighlightedText(objectId, keywordHitQuery, isRegex, originalQuery);