Autopsy  4.1
Graphical digital forensics platform for The Sleuth Kit and other tools.
AccountsText.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.base.Predicate;
22 import com.google.common.collect.Iterators;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collection;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.LinkedHashMap;
29 import java.util.List;
30 import java.util.Set;
31 import java.util.TreeMap;
32 import java.util.TreeSet;
33 import java.util.logging.Level;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36 import javax.annotation.concurrent.GuardedBy;
37 import org.apache.commons.lang.StringUtils;
38 import org.apache.solr.client.solrj.SolrQuery;
39 import org.apache.solr.client.solrj.SolrRequest.METHOD;
40 import org.apache.solr.client.solrj.response.QueryResponse;
41 import org.openide.util.NbBundle;
47 
57 class AccountsText implements IndexedText {
58 
59  private static final Logger logger = Logger.getLogger(AccountsText.class.getName());
60  private static final boolean DEBUG = (Version.getBuildType() == Version.Type.DEVELOPMENT);
61 
62  private static final String HIGHLIGHT_PRE = "<span style='background:yellow'>"; //NON-NLS
63  private static final String ANCHOR_NAME_PREFIX = AccountsText.class.getName() + "_";
64 
65  private static final String INSERT_PREFIX = "<a name='" + ANCHOR_NAME_PREFIX; //NON-NLS
66  private static final String INSERT_POSTFIX = "'></a>$0"; //$0 will insert current regex match //NON-NLS
67  private static final Pattern ANCHOR_DETECTION_PATTERN = Pattern.compile(HIGHLIGHT_PRE);
68 
72 
73  private static final String FIELD = Server.Schema.CONTENT_STR.toString();
74 
75  private final Server solrServer = KeywordSearch.getServer();
76 
77  private final long solrObjectId;
78  private final Collection<? extends BlackboardArtifact> artifacts;
79  private final Set<String> accountNumbers = new HashSet<>();
80  private final String displayName;
81 
82  @GuardedBy("this")
83  private boolean isPageInfoLoaded = false;
84  private int numberPagesForFile = 0;
85  private Integer currentPage = 0;
86 
87  /*
88  * map from page/chunk to number of hits. value is 0 if not yet known.
89  */
90  private final TreeMap<Integer, Integer> numberOfHitsPerPage = new TreeMap<>();
91  /*
92  * set of pages, used for iterating back and forth. Only stores pages with hits
93  */
94  private final Set<Integer> pages = numberOfHitsPerPage.keySet();
95  /*
96  * map from page/chunk number to current hit on that page.
97  */
98  private final HashMap<Integer, Integer> currentHitPerPage = new HashMap<>();
99 
100  AccountsText(long objectID, BlackboardArtifact artifact) {
101  this(objectID, Arrays.asList(artifact));
102 
103  }
104 
105  @NbBundle.Messages({
106  "AccountsText.creditCardNumber=Credit Card Number",
107  "AccountsText.creditCardNumbers=Credit Card Numbers"})
108  AccountsText(long objectID, Collection<? extends BlackboardArtifact> artifacts) {
109  this.solrObjectId = objectID;
110  this.artifacts = artifacts;
111  displayName = artifacts.size() == 1
112  ? Bundle.AccountsText_creditCardNumber()
113  : Bundle.AccountsText_creditCardNumbers();
114  }
115 
116  long getObjectId() {
117  return this.solrObjectId;
118  }
119 
120  @Override
121  public int getNumberPages() {
122  return this.numberPagesForFile;
123  }
124 
125  @Override
126  public int getCurrentPage() {
127  return this.currentPage;
128  }
129 
130  @Override
131  public boolean hasNextPage() {
132  return getIndexOfCurrentPage() < pages.size() - 1;
133 
134  }
135 
136  @Override
137  public boolean hasPreviousPage() {
138  return getIndexOfCurrentPage() > 0;
139  }
140 
141  @Override
142  @NbBundle.Messages("AccountsText.nextPage.exception.msg=No next page.")
143  public int nextPage() {
144  if (hasNextPage()) {
145  currentPage =Iterators.get(pages.iterator(),getIndexOfCurrentPage() + 1);
146  return currentPage;
147  } else {
148  throw new IllegalStateException(Bundle.AccountsText_nextPage_exception_msg());
149  }
150  }
151 
152  @Override
153  @NbBundle.Messages("AccountsText.previousPage.exception.msg=No previous page.")
154  public int previousPage() {
155  if (hasPreviousPage()) {
156  currentPage = Iterators.get(pages.iterator(),getIndexOfCurrentPage() - 1);
157  return currentPage;
158  } else {
159  throw new IllegalStateException(Bundle.AccountsText_previousPage_exception_msg());
160  }
161  }
162 
163  private int getIndexOfCurrentPage() {
164  return Iterators.indexOf(pages.iterator(), this.currentPage::equals);
165  }
166 
167  @Override
168  public boolean hasNextItem() {
169  if (this.currentHitPerPage.containsKey(currentPage)) {
170  return this.currentHitPerPage.get(currentPage) < this.numberOfHitsPerPage.get(currentPage);
171  } else {
172  return false;
173  }
174  }
175 
176  @Override
177  public boolean hasPreviousItem() {
178  if (this.currentHitPerPage.containsKey(currentPage)) {
179  return this.currentHitPerPage.get(currentPage) > 1;
180  } else {
181  return false;
182  }
183  }
184 
185  @Override
186  @NbBundle.Messages("AccountsText.nextItem.exception.msg=No next item.")
187  public int nextItem() {
188  if (hasNextItem()) {
189  return currentHitPerPage.merge(currentPage, 1, Integer::sum);
190  } else {
191  throw new IllegalStateException(Bundle.AccountsText_nextItem_exception_msg());
192  }
193  }
194 
195  @Override
196  @NbBundle.Messages("AccountsText.previousItem.exception.msg=No previous item.")
197  public int previousItem() {
198  if (hasPreviousItem()) {
199  return currentHitPerPage.merge(currentPage, -1, Integer::sum);
200  } else {
201  throw new IllegalStateException(Bundle.AccountsText_previousItem_exception_msg());
202  }
203  }
204 
205  @Override
206  public int currentItem() {
207  if (this.currentHitPerPage.containsKey(currentPage)) {
208  return currentHitPerPage.get(currentPage);
209  } else {
210  return 0;
211  }
212  }
213 
218  synchronized private void loadPageInfo() throws IllegalStateException, TskCoreException {
219  if (isPageInfoLoaded) {
220  return;
221  }
222 
223  try {
224  this.numberPagesForFile = solrServer.queryNumFileChunks(this.solrObjectId);
226  logger.log(Level.WARNING, "Could not get number pages for content " + this.solrObjectId, ex); //NON-NLS
227  return;
228  }
229 
230  for (BlackboardArtifact artifact : artifacts) {
231  addToPagingInfo(artifact);
232  }
233 
234  this.currentPage = pages.stream().findFirst().orElse(1);
235 
236  isPageInfoLoaded = true;
237  }
238 
239  private void addToPagingInfo(BlackboardArtifact artifact) throws IllegalStateException, TskCoreException {
240  if (solrObjectId != artifact.getObjectID()) {
241  throw new IllegalStateException("not all artifacts are from the same object!");
242  }
243 
244  accountNumbers.add(artifact.getAttribute(TSK_CARD_NUMBER).getValueString());
245  final BlackboardAttribute keywordAttribute = artifact.getAttribute(TSK_KEYWORD);
246  if (keywordAttribute != null) {
247  accountNumbers.add(keywordAttribute.getValueString());
248  }
249  List<String> rawDocIDs = new ArrayList<>();
250 
251  final BlackboardAttribute docID = artifact.getAttribute(TSK_KEYWORD_SEARCH_DOCUMENT_ID);
252  if (docID != null) {
253  rawDocIDs.add(docID.getValueString());
254  }
255 
256  rawDocIDs.stream()
257  .map(String::trim)
258  .map(t -> StringUtils.substringAfterLast(t, Server.CHUNK_ID_SEPARATOR))
259  .map(Integer::valueOf)
260  .forEach(chunkID -> {
261  numberOfHitsPerPage.put(chunkID, 0);
262  currentHitPerPage.put(chunkID, 0);
263  });
264  }
265 
266  @Override
267  @NbBundle.Messages({"AccountsText.getMarkup.noMatchMsg="
268  + "<html><pre><span style\\\\='background\\\\:yellow'>There were no keyword hits on this page. <br />"
269  + "The keyword could have been in the file name."
270  + " <br />Advance to another page if present, or to view the original text, choose File Text"
271  + " <br />in the drop down menu to the right...</span></pre></html>",
272  "AccountsText.getMarkup.queryFailedMsg="
273  + "<html><pre><span style\\\\='background\\\\:yellow'>Failed to retrieve keyword hit results."
274  + " <br />Confirm that Autopsy can connect to the Solr server. "
275  + "<br /></span></pre></html>"})
276  public String getText() {
277  try {
278  loadPageInfo(); //inits once
279 
280  SolrQuery q = new SolrQuery();
281  q.setShowDebugInfo(DEBUG); //debug
282 
283  String contentIdStr = this.solrObjectId + Server.CHUNK_ID_SEPARATOR + this.currentPage;
284  final String filterQuery = Server.Schema.ID.toString() + ":" + contentIdStr;
285  //set the documentID filter
286  q.setQuery(filterQuery);
287  q.setFields(FIELD);
288 
289  QueryResponse queryResponse = solrServer.query(q, METHOD.POST);
290 
291  String highlightedText
292  = HighlightedText.attemptManualHighlighting(
293  queryResponse.getResults(),
294  Server.Schema.CONTENT_STR.toString(),
295  accountNumbers
296  ).trim();
297 
298  highlightedText = insertAnchors(highlightedText);
299 
300  // extracted content (minus highlight tags) is HTML-escaped
301  return "<html><pre>" + highlightedText + "</pre></html>"; //NON-NLS
302  } catch (Exception ex) {
303  logger.log(Level.WARNING, "Error getting highlighted text for " + solrObjectId, ex); //NON-NLS
304  return Bundle.AccountsText_getMarkup_queryFailedMsg();
305  }
306  }
307 
316  private String insertAnchors(String searchableContent) {
317  /*
318  * use regex matcher to iterate over occurences of HIGHLIGHT_PRE, and
319  * prepend them with an anchor tag.
320  */
321  Matcher m = ANCHOR_DETECTION_PATTERN.matcher(searchableContent);
322  StringBuffer sb = new StringBuffer(searchableContent.length());
323  int count = 0;
324  while (m.find()) {
325  count++;
326  m.appendReplacement(sb, INSERT_PREFIX + count + INSERT_POSTFIX);
327  }
328  m.appendTail(sb);
329  //store total hits for this page, now that we know it
330  this.numberOfHitsPerPage.put(this.currentPage, count);
331  if (this.currentItem() == 0 && this.hasNextItem()) {
332  this.nextItem();
333  }
334  return sb.toString();
335  }
336 
337  @Override
338  public String toString() {
339  return displayName;
340  }
341 
342  @Override
343  public boolean isSearchable() {
344  return true;
345  }
346 
347  @Override
348  public String getAnchorPrefix() {
349  return ANCHOR_NAME_PREFIX;
350  }
351 
352  @Override
353  public int getNumberHits() {
354  if (!this.numberOfHitsPerPage.containsKey(this.currentPage)) {
355  return 0;
356  }
357  return this.numberOfHitsPerPage.get(this.currentPage);
358  }
359 }
static Version.Type getBuildType()
Definition: Version.java:87
BlackboardAttribute getAttribute(BlackboardAttribute.Type attributeType)
QueryResponse query(SolrQuery sq)
Definition: Server.java:1008
synchronized static Logger getLogger(String name)
Definition: Logger.java:161

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