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);