Autopsy  4.4
Graphical digital forensics platform for The Sleuth Kit and other tools.
HighlightedText.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2011-2017 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.keywordsearch;
20 
21 import com.google.common.collect.Iterators;
22 import com.google.common.collect.Range;
23 import com.google.common.collect.RangeSet;
24 import com.google.common.collect.TreeRangeSet;
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.HashMap;
28 import java.util.HashSet;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Set;
32 import java.util.TreeMap;
33 import java.util.logging.Level;
34 import java.util.stream.Collectors;
35 import javax.annotation.concurrent.GuardedBy;
36 import org.apache.commons.lang.StringEscapeUtils;
37 import org.apache.commons.lang.StringUtils;
38 import org.apache.commons.lang3.math.NumberUtils;
39 import org.apache.solr.client.solrj.SolrQuery;
40 import org.apache.solr.client.solrj.SolrRequest.METHOD;
41 import org.apache.solr.client.solrj.response.QueryResponse;
42 import org.apache.solr.common.SolrDocumentList;
43 import org.openide.util.NbBundle;
44 import org.openide.util.NbBundle.Messages;
48 import org.sleuthkit.datamodel.BlackboardArtifact;
49 import org.sleuthkit.datamodel.BlackboardAttribute;
50 import org.sleuthkit.datamodel.TskCoreException;
51 
56 class HighlightedText implements IndexedText {
57 
58  private static final Logger logger = Logger.getLogger(HighlightedText.class.getName());
59 
60  private static final boolean DEBUG = (Version.getBuildType() == Version.Type.DEVELOPMENT);
61 
62  private static final BlackboardAttribute.Type TSK_KEYWORD_SEARCH_TYPE = new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_SEARCH_TYPE);
63  private static final BlackboardAttribute.Type TSK_KEYWORD = new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD);
64  static private final BlackboardAttribute.Type TSK_ASSOCIATED_ARTIFACT = new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT);
65  static private final BlackboardAttribute.Type TSK_KEYWORD_REGEXP = new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_REGEXP);
66 
67  private static final String HIGHLIGHT_PRE = "<span style='background:yellow'>"; //NON-NLS
68  private static final String HIGHLIGHT_POST = "</span>"; //NON-NLS
69  private static final String ANCHOR_PREFIX = HighlightedText.class.getName() + "_"; //NON-NLS
70 
71  final private Server solrServer = KeywordSearch.getServer();
72 
73  private final long objectId;
74  /*
75  * The keywords to highlight
76  */
77  private final Set<String> keywords = new HashSet<>();
78 
79  private int numberPages;
80  private Integer currentPage = 0;
81 
82  @GuardedBy("this")
83  private boolean isPageInfoLoaded = false;
84 
85  /*
86  * map from page/chunk to number of hits. value is 0 if not yet known.
87  */
88  private final TreeMap<Integer, Integer> numberOfHitsPerPage = new TreeMap<>();
89  /*
90  * set of pages, used for iterating back and forth. Only stores pages with
91  * hits
92  */
93  private final Set<Integer> pages = numberOfHitsPerPage.keySet();
94  /*
95  * map from page/chunk number to current hit on that page.
96  */
97  private final HashMap<Integer, Integer> currentHitPerPage = new HashMap<>();
98 
99  private QueryResults hits = null; //original hits that may get passed in
100  private BlackboardArtifact artifact;
101  private KeywordSearch.QueryType qt;
102  private boolean isLiteral;
103 
115  HighlightedText(long objectId, QueryResults hits) {
116  this.objectId = objectId;
117  this.hits = hits;
118  }
119 
128  HighlightedText(BlackboardArtifact artifact) throws TskCoreException {
129  this.artifact = artifact;
130  BlackboardAttribute attribute = artifact.getAttribute(TSK_ASSOCIATED_ARTIFACT);
131  if (attribute != null) {
132  this.objectId = attribute.getValueLong();
133  } else {
134  this.objectId = artifact.getObjectID();
135  }
136 
137  }
138 
143  @Messages({"HighlightedText.query.exception.msg=Could not perform the query to get chunk info and get highlights:"})
144  synchronized private void loadPageInfo() throws TskCoreException, KeywordSearchModuleException, NoOpenCoreException {
145  if (isPageInfoLoaded) {
146  return;
147  }
148 
149  this.numberPages = solrServer.queryNumFileChunks(this.objectId);
150 
151  if (artifact != null) {
152  loadPageInfoFromArtifact();
153  } else if (numberPages != 0) {
154  // if the file has chunks, get pages with hits, sorted
155  loadPageInfoFromHits();
156  } else {
157  //non-artifact, no chunks, everything is easy.
158  this.numberPages = 1;
159  this.currentPage = 1;
160  numberOfHitsPerPage.put(1, 0);
161  pages.add(1);
162  currentHitPerPage.put(1, 0);
163  isPageInfoLoaded = true;
164  }
165  }
166 
173  synchronized private void loadPageInfoFromArtifact() throws TskCoreException, KeywordSearchModuleException, NoOpenCoreException {
174  final String keyword = artifact.getAttribute(TSK_KEYWORD).getValueString();
175  this.keywords.add(keyword);
176 
177  //get the QueryType (if available)
178  final BlackboardAttribute queryTypeAttribute = artifact.getAttribute(TSK_KEYWORD_SEARCH_TYPE);
179  qt = (queryTypeAttribute != null)
180  ? KeywordSearch.QueryType.values()[queryTypeAttribute.getValueInt()] : null;
181 
182  Keyword keywordQuery = null;
183  switch (qt) {
184  case LITERAL:
185  case SUBSTRING:
186  keywordQuery = new Keyword(keyword, true, true);
187  break;
188  case REGEX:
189  String regexp = artifact.getAttribute(TSK_KEYWORD_REGEXP).getValueString();
190  keywordQuery = new Keyword(regexp, false, false);
191  break;
192  }
193  KeywordSearchQuery chunksQuery = KeywordSearchUtil.getQueryForKeyword(keywordQuery, new KeywordList(Arrays.asList(keywordQuery)));
194  // Run a query to figure out which chunks for the current object have
195  // hits for this keyword.
196 
197  chunksQuery.addFilter(new KeywordQueryFilter(FilterType.CHUNK, this.objectId));
198 
199  hits = chunksQuery.performQuery();
200  loadPageInfoFromHits();
201  }
202 
206  synchronized private void loadPageInfoFromHits() {
207  isLiteral = hits.getQuery().isLiteral();
208  //organize the hits by page, filter as needed
209  for (Keyword k : hits.getKeywords()) {
210  for (KeywordHit hit : hits.getResults(k)) {
211  int chunkID = hit.getChunkId();
212  if (artifact != null) {
213  if (chunkID != 0 && this.objectId == hit.getSolrObjectId()) {
214  String hit1 = hit.getHit();
215  if (keywords.stream().anyMatch(hit1::contains)) {
216  numberOfHitsPerPage.put(chunkID, 0); //unknown number of matches in the page
217  currentHitPerPage.put(chunkID, 0); //set current hit to 0th
218 
219  }
220  }
221  } else {
222  if (chunkID != 0 && this.objectId == hit.getSolrObjectId()) {
223 
224  numberOfHitsPerPage.put(chunkID, 0); //unknown number of matches in the page
225  currentHitPerPage.put(chunkID, 0); //set current hit to 0th
226 
227  if (StringUtils.isNotBlank(hit.getHit())) {
228  this.keywords.add(hit.getHit());
229  }
230  }
231  }
232  }
233  }
234 
235  //set page to first page having highlights
236  this.currentPage = pages.stream().findFirst().orElse(1);
237 
238  isPageInfoLoaded = true;
239  }
240 
249  static private String constructEscapedSolrQuery(String query) {
250  return LuceneQuery.HIGHLIGHT_FIELD + ":" + "\"" + KeywordSearchUtil.escapeLuceneQuery(query) + "\"";
251  }
252 
253  private int getIndexOfCurrentPage() {
254  return Iterators.indexOf(pages.iterator(), this.currentPage::equals);
255  }
256 
257  @Override
258  public int getNumberPages() {
259  //return number of pages that have hits
260  return this.numberPages;
261  }
262 
263  @Override
264  public int getCurrentPage() {
265  return this.currentPage;
266  }
267 
268  @Override
269  public boolean hasNextPage() {
270  return getIndexOfCurrentPage() < pages.size() - 1;
271  }
272 
273  @Override
274  public boolean hasPreviousPage() {
275  return getIndexOfCurrentPage() > 0;
276  }
277 
278  @Override
279  public int nextPage() {
280  if (hasNextPage()) {
281  currentPage = Iterators.get(pages.iterator(), getIndexOfCurrentPage() + 1);
282  return currentPage;
283  } else {
284  throw new IllegalStateException("No next page.");
285  }
286  }
287 
288  @Override
289  public int previousPage() {
290  if (hasPreviousPage()) {
291  currentPage = Iterators.get(pages.iterator(), getIndexOfCurrentPage() - 1);
292  return currentPage;
293  } else {
294  throw new IllegalStateException("No previous page.");
295  }
296  }
297 
298  @Override
299  public boolean hasNextItem() {
300  if (!this.currentHitPerPage.containsKey(currentPage)) {
301  return false;
302  }
303  return this.currentHitPerPage.get(currentPage) < this.numberOfHitsPerPage.get(currentPage);
304  }
305 
306  @Override
307  public boolean hasPreviousItem() {
308  if (!this.currentHitPerPage.containsKey(currentPage)) {
309  return false;
310  }
311  return this.currentHitPerPage.get(currentPage) > 1;
312  }
313 
314  @Override
315  public int nextItem() {
316  if (!hasNextItem()) {
317  throw new IllegalStateException("No next item.");
318  }
319  int cur = currentHitPerPage.get(currentPage) + 1;
320  currentHitPerPage.put(currentPage, cur);
321  return cur;
322  }
323 
324  @Override
325  public int previousItem() {
326  if (!hasPreviousItem()) {
327  throw new IllegalStateException("No previous item.");
328  }
329  int cur = currentHitPerPage.get(currentPage) - 1;
330  currentHitPerPage.put(currentPage, cur);
331  return cur;
332  }
333 
334  @Override
335  public int currentItem() {
336  if (!this.currentHitPerPage.containsKey(currentPage)) {
337  return 0;
338  }
339  return currentHitPerPage.get(currentPage);
340  }
341 
342  @Override
343  public String getText() {
344  try {
345  loadPageInfo(); //inits once
346  SolrQuery q = new SolrQuery();
347  q.setShowDebugInfo(DEBUG); //debug
348 
349  String contentIdStr = Long.toString(this.objectId);
350  if (numberPages != 0) {
351  final String chunkID = Integer.toString(this.currentPage);
352  contentIdStr += "0".equals(chunkID) ? "" : "_" + chunkID;
353  }
354  final String filterQuery = Server.Schema.ID.toString() + ":" + KeywordSearchUtil.escapeLuceneQuery(contentIdStr);
355 
356  double indexSchemaVersion = NumberUtils.toDouble(solrServer.getIndexInfo().getSchemaVersion());
357  //choose field to highlight based on isLiteral and Solr index schema version.
358  String highlightField = (isLiteral || (indexSchemaVersion < 2.0))
359  ? LuceneQuery.HIGHLIGHT_FIELD
360  : Server.Schema.CONTENT_STR.toString();
361  if (isLiteral) {
362  //if the query is literal try to get solr to do the highlighting
363  final String highlightQuery = keywords.stream()
364  .map(HighlightedText::constructEscapedSolrQuery)
365  .collect(Collectors.joining(" "));
366 
367  q.setQuery(highlightQuery);
368  q.addField(highlightField);
369  q.addFilterQuery(filterQuery);
370  q.addHighlightField(highlightField);
371  q.setHighlightFragsize(0); // don't fragment the highlight, works with original highlighter, or needs "single" list builder with FVH
372 
373  //tune the highlighter
374  q.setParam("hl.useFastVectorHighlighter", "on"); //fast highlighter scales better than standard one NON-NLS
375  q.setParam("hl.tag.pre", HIGHLIGHT_PRE); //makes sense for FastVectorHighlighter only NON-NLS
376  q.setParam("hl.tag.post", HIGHLIGHT_POST); //makes sense for FastVectorHighlighter only NON-NLS
377  q.setParam("hl.fragListBuilder", "single"); //makes sense for FastVectorHighlighter only NON-NLS
378 
379  //docs says makes sense for the original Highlighter only, but not really
380  q.setParam("hl.maxAnalyzedChars", Server.HL_ANALYZE_CHARS_UNLIMITED); //NON-NLS
381  } else {
382  /*
383  * if the query is not literal just pull back the text. We will
384  * do the highlighting in autopsy.
385  */
386  q.setQuery(filterQuery);
387  q.addField(highlightField);
388  }
389 
390  QueryResponse response = solrServer.query(q, METHOD.POST);
391 
392  // There should never be more than one document since there will
393  // either be a single chunk containing hits or we narrow our
394  // query down to the current page/chunk.
395  if (response.getResults().size() > 1) {
396  logger.log(Level.WARNING, "Unexpected number of results for Solr highlighting query: {0}", q); //NON-NLS
397  }
398  String highlightedContent;
399  Map<String, Map<String, List<String>>> responseHighlight = response.getHighlighting();
400 
401  if (responseHighlight == null) {
402  highlightedContent = attemptManualHighlighting(response.getResults(), highlightField, keywords);
403  } else {
404  Map<String, List<String>> responseHighlightID = responseHighlight.get(contentIdStr);
405 
406  if (responseHighlightID == null) {
407  highlightedContent = attemptManualHighlighting(response.getResults(), highlightField, keywords);
408  } else {
409  List<String> contentHighlights = responseHighlightID.get(LuceneQuery.HIGHLIGHT_FIELD);
410  if (contentHighlights == null) {
411  highlightedContent = attemptManualHighlighting(response.getResults(), highlightField, keywords);
412  } else {
413  // extracted content (minus highlight tags) is HTML-escaped
414  highlightedContent = contentHighlights.get(0).trim();
415  }
416  }
417  }
418  highlightedContent = insertAnchors(highlightedContent);
419 
420  return "<html><pre>" + highlightedContent + "</pre></html>"; //NON-NLS
421  } catch (TskCoreException | KeywordSearchModuleException | NoOpenCoreException ex) {
422  logger.log(Level.SEVERE, "Error getting highlighted text for " + objectId, ex); //NON-NLS
423  return NbBundle.getMessage(this.getClass(), "HighlightedMatchesSource.getMarkup.queryFailedMsg");
424  }
425  }
426 
427  @Override
428  public String toString() {
429  return NbBundle.getMessage(this.getClass(), "HighlightedMatchesSource.toString");
430  }
431 
432  @Override
433  public boolean isSearchable() {
434  return true;
435  }
436 
437  @Override
438  public String getAnchorPrefix() {
439  return ANCHOR_PREFIX;
440  }
441 
442  @Override
443  public int getNumberHits() {
444  if (!this.numberOfHitsPerPage.containsKey(this.currentPage)) {
445  return 0;
446  }
447  return this.numberOfHitsPerPage.get(this.currentPage);
448 
449  }
450 
464  static String attemptManualHighlighting(SolrDocumentList solrDocumentList, String highlightField, Collection<String> keywords) {
465  if (solrDocumentList.isEmpty()) {
466  return NbBundle.getMessage(HighlightedText.class, "HighlightedMatchesSource.getMarkup.noMatchMsg");
467  }
468 
469  // It doesn't make sense for there to be more than a single document in
470  // the list since this class presents a single page (document) of highlighted
471  // content at a time. Hence we can just use get(0).
472  String text = solrDocumentList.get(0).getOrDefault(highlightField, "").toString();
473 
474  // Escape any HTML content that may be in the text. This is needed in
475  // order to correctly display the text in the content viewer.
476  // Must be done before highlighting tags are added. If we were to
477  // perform HTML escaping after adding the highlighting tags we would
478  // not see highlighted text in the content viewer.
479  text = StringEscapeUtils.escapeHtml(text);
480 
481  TreeRangeSet<Integer> highlights = TreeRangeSet.create();
482 
483  //for each keyword find the locations of hits and record them in the RangeSet
484  for (String keyword : keywords) {
485  //we also need to escape the keyword so that it matches the escaped text
486  final String escapedKeyword = StringEscapeUtils.escapeHtml(keyword);
487  int searchOffset = 0;
488  int hitOffset = StringUtils.indexOfIgnoreCase(text, escapedKeyword, searchOffset);
489  while (hitOffset != -1) {
490  // Advance the search offset past the keyword.
491  searchOffset = hitOffset + escapedKeyword.length();
492 
493  //record the location of the hit, possibly merging it with other hits
494  highlights.add(Range.closedOpen(hitOffset, searchOffset));
495 
496  //look for next hit
497  hitOffset = StringUtils.indexOfIgnoreCase(text, escapedKeyword, searchOffset);
498  }
499  }
500 
501  StringBuilder highlightedText = new StringBuilder(text);
502  int totalHighLightLengthInserted = 0;
503  //for each range to be highlighted...
504  for (Range<Integer> highlightRange : highlights.asRanges()) {
505  int hStart = highlightRange.lowerEndpoint();
506  int hEnd = highlightRange.upperEndpoint();
507 
508  //insert the pre and post tag, adjusting indices for previously added tags
509  highlightedText.insert(hStart + totalHighLightLengthInserted, HIGHLIGHT_PRE);
510  totalHighLightLengthInserted += HIGHLIGHT_PRE.length();
511  highlightedText.insert(hEnd + totalHighLightLengthInserted, HIGHLIGHT_POST);
512  totalHighLightLengthInserted += HIGHLIGHT_POST.length();
513  }
514 
515  return highlightedText.toString();
516  }
517 
526  private String insertAnchors(String searchableContent) {
527  StringBuilder buf = new StringBuilder(searchableContent);
528  final String searchToken = HIGHLIGHT_PRE;
529  final int indexSearchTokLen = searchToken.length();
530  final String insertPre = "<a name='" + ANCHOR_PREFIX; //NON-NLS
531  final String insertPost = "'></a>"; //NON-NLS
532  int count = 0;
533  int searchOffset = 0;
534  int index = buf.indexOf(searchToken, searchOffset);
535  while (index >= 0) {
536  String insertString = insertPre + Integer.toString(count + 1) + insertPost;
537  int insertStringLen = insertString.length();
538  buf.insert(index, insertString);
539  searchOffset = index + indexSearchTokLen + insertStringLen; //next offset past this anchor
540  ++count;
541  index = buf.indexOf(searchToken, searchOffset);
542  }
543 
544  //store total hits for this page, now that we know it
545  this.numberOfHitsPerPage.put(this.currentPage, count);
546  if (this.currentItem() == 0 && this.hasNextItem()) {
547  this.nextItem();
548  }
549 
550  return buf.toString();
551  }
552 
553 }

Copyright © 2012-2016 Basis Technology. Generated on: Tue Jun 13 2017
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.