19 package org.sleuthkit.autopsy.keywordsearch;
21 import com.google.common.collect.Iterators;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.Optional;
28 import java.util.TreeMap;
29 import java.util.logging.Level;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32 import javax.annotation.concurrent.GuardedBy;
33 import org.apache.commons.lang3.StringUtils;
34 import org.apache.solr.client.solrj.SolrQuery;
35 import org.apache.solr.client.solrj.SolrRequest.METHOD;
36 import org.apache.solr.client.solrj.response.QueryResponse;
37 import org.openide.util.NbBundle;
53 class AccountsText
implements IndexedText {
58 private static final String HIGHLIGHT_PRE =
"<span style='background:yellow'>";
59 private static final String ANCHOR_NAME_PREFIX = AccountsText.class.getName() +
"_";
61 private static final String INSERT_PREFIX =
"<a name='" + ANCHOR_NAME_PREFIX;
62 private static final String INSERT_POSTFIX =
"'></a>$0";
63 private static final Pattern ANCHOR_DETECTION_PATTERN = Pattern.compile(HIGHLIGHT_PRE);
65 private static final BlackboardAttribute.Type TSK_KEYWORD_SEARCH_DOCUMENT_ID =
new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_SEARCH_DOCUMENT_ID);
66 private static final BlackboardAttribute.Type TSK_CARD_NUMBER =
new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_CARD_NUMBER);
67 private static final BlackboardAttribute.Type TSK_KEYWORD =
new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD);
69 private static final String FIELD =
Server.
Schema.CONTENT_STR.toString();
73 private final long solrObjectId;
74 private final Collection<? extends BlackboardArtifact> artifacts;
75 private final Set<String> accountNumbers =
new HashSet<>();
76 private final String title;
79 private boolean isPageInfoLoaded =
false;
80 private int numberPagesForFile = 0;
81 private Integer currentPage = 0;
86 private final TreeMap<Integer, Integer> numberOfHitsPerPage =
new TreeMap<>();
91 private final Set<Integer> pages = numberOfHitsPerPage.keySet();
95 private final HashMap<Integer, Integer> currentHitPerPage =
new HashMap<>();
97 AccountsText(
long objectID, BlackboardArtifact artifact) {
98 this(objectID, Arrays.asList(artifact));
103 "AccountsText.creditCardNumber=Credit Card Number",
104 "AccountsText.creditCardNumbers=Credit Card Numbers"})
105 AccountsText(
long objectID, Collection<? extends BlackboardArtifact> artifacts) {
106 this.solrObjectId = objectID;
107 this.artifacts = artifacts;
108 title = artifacts.size() == 1
109 ? Bundle.AccountsText_creditCardNumber()
110 : Bundle.AccountsText_creditCardNumbers();
114 return this.solrObjectId;
118 public int getNumberPages() {
119 return this.numberPagesForFile;
123 public int getCurrentPage() {
124 return this.currentPage;
128 public boolean hasNextPage() {
129 return getIndexOfCurrentPage() < pages.size() - 1;
134 public boolean hasPreviousPage() {
135 return getIndexOfCurrentPage() > 0;
139 @NbBundle.Messages(
"AccountsText.nextPage.exception.msg=No next page.")
140 public int nextPage() {
142 currentPage = Iterators.get(pages.iterator(), getIndexOfCurrentPage() + 1);
145 throw new IllegalStateException(Bundle.AccountsText_nextPage_exception_msg());
150 @NbBundle.Messages(
"AccountsText.previousPage.exception.msg=No previous page.")
151 public int previousPage() {
152 if (hasPreviousPage()) {
153 currentPage = Iterators.get(pages.iterator(), getIndexOfCurrentPage() - 1);
156 throw new IllegalStateException(Bundle.AccountsText_previousPage_exception_msg());
160 private int getIndexOfCurrentPage() {
161 return Iterators.indexOf(pages.iterator(), this.currentPage::equals);
165 public boolean hasNextItem() {
166 if (this.currentHitPerPage.containsKey(currentPage)) {
167 return this.currentHitPerPage.get(currentPage) < this.numberOfHitsPerPage.get(currentPage);
174 public boolean hasPreviousItem() {
175 if (this.currentHitPerPage.containsKey(currentPage)) {
176 return this.currentHitPerPage.get(currentPage) > 1;
183 @NbBundle.Messages(
"AccountsText.nextItem.exception.msg=No next item.")
184 public int nextItem() {
186 return currentHitPerPage.merge(currentPage, 1, Integer::sum);
188 throw new IllegalStateException(Bundle.AccountsText_nextItem_exception_msg());
193 @NbBundle.Messages(
"AccountsText.previousItem.exception.msg=No previous item.")
194 public int previousItem() {
195 if (hasPreviousItem()) {
196 return currentHitPerPage.merge(currentPage, -1, Integer::sum);
198 throw new IllegalStateException(Bundle.AccountsText_previousItem_exception_msg());
203 public int currentItem() {
204 if (this.currentHitPerPage.containsKey(currentPage)) {
205 return currentHitPerPage.get(currentPage);
216 if (isPageInfoLoaded) {
222 boolean needsQuery =
false;
224 for (BlackboardArtifact artifact : artifacts) {
225 if (solrObjectId != artifact.getObjectID()) {
226 throw new IllegalStateException(
"not all artifacts are from the same object!");
230 BlackboardAttribute attribute = artifact.getAttribute(TSK_KEYWORD);
231 if (attribute != null) {
232 this.accountNumbers.add(attribute.getValueString());
234 attribute = artifact.getAttribute(TSK_CARD_NUMBER);
235 if (attribute != null) {
236 this.accountNumbers.add(attribute.getValueString());
240 Optional<Integer> chunkID =
241 Optional.ofNullable(artifact.getAttribute(TSK_KEYWORD_SEARCH_DOCUMENT_ID))
242 .map(BlackboardAttribute::getValueString)
245 .map(Integer::valueOf);
246 if (chunkID.isPresent()) {
247 numberOfHitsPerPage.put(chunkID.get(), 0);
248 currentHitPerPage.put(chunkID.get(), 0);
257 Keyword queryKeyword =
new Keyword(CCN_REGEX,
false,
false);
258 KeywordSearchQuery chunksQuery = KeywordSearchUtil.getQueryForKeyword(queryKeyword,
new KeywordList(Arrays.asList(queryKeyword)));
259 chunksQuery.addFilter(
new KeywordQueryFilter(KeywordQueryFilter.FilterType.CHUNK,
this.solrObjectId));
261 loadPageInfoFromHits(chunksQuery.performQuery());
264 this.currentPage = pages.stream().findFirst().orElse(1);
266 isPageInfoLoaded =
true;
268 private static final String CCN_REGEX =
"(%?)(B?)([0-9][ \\-]*?){12,19}(\\^?)";
273 synchronized private void loadPageInfoFromHits(QueryResults hits) {
275 for (Keyword k : hits.getKeywords()) {
276 for (KeywordHit hit : hits.getResults(k)) {
277 int chunkID = hit.getChunkId();
278 if (chunkID != 0 && this.solrObjectId == hit.getSolrObjectId()) {
279 String hitString = hit.getHit();
280 if (accountNumbers.stream().anyMatch(hitString::contains)) {
281 numberOfHitsPerPage.put(chunkID, 0);
282 currentHitPerPage.put(chunkID, 0);
290 @NbBundle.Messages({
"AccountsText.getMarkup.noMatchMsg="
291 +
"<html><pre><span style\\\\='background\\\\:yellow'>There were no keyword hits on this page. <br />"
292 +
"The keyword could have been in the file name."
293 +
" <br />Advance to another page if present, or to view the original text, choose File Text"
294 +
" <br />in the drop down menu to the right...</span></pre></html>",
295 "AccountsText.getMarkup.queryFailedMsg="
296 +
"<html><pre><span style\\\\='background\\\\:yellow'>Failed to retrieve keyword hit results."
297 +
" <br />Confirm that Autopsy can connect to the Solr server. "
298 +
"<br /></span></pre></html>"})
299 public String getText() {
303 SolrQuery q =
new SolrQuery();
304 q.setShowDebugInfo(DEBUG);
307 final String filterQuery =
Server.
Schema.ID.toString() +
":" + contentIdStr;
309 q.setQuery(filterQuery);
312 QueryResponse queryResponse = solrServer.
query(q, METHOD.POST);
314 String highlightedText =
315 HighlightedText.attemptManualHighlighting(
316 queryResponse.getResults(),
321 highlightedText = insertAnchors(highlightedText);
324 return "<html><pre>" + highlightedText +
"</pre></html>";
325 }
catch (Exception ex) {
326 logger.log(Level.SEVERE,
"Error getting highlighted text for Solr doc id " +
this.solrObjectId +
", chunkID " +
this.currentPage , ex);
327 return Bundle.AccountsText_getMarkup_queryFailedMsg();
339 private String insertAnchors(String searchableContent) {
344 Matcher m = ANCHOR_DETECTION_PATTERN.matcher(searchableContent);
345 StringBuffer sb =
new StringBuffer(searchableContent.length());
349 m.appendReplacement(sb, INSERT_PREFIX + count + INSERT_POSTFIX);
353 this.numberOfHitsPerPage.put(this.currentPage, count);
354 if (this.currentItem() == 0 && this.hasNextItem()) {
357 return sb.toString();
361 public String toString() {
366 public boolean isSearchable() {
371 public String getAnchorPrefix() {
372 return ANCHOR_NAME_PREFIX;
376 public int getNumberHits() {
377 if (!this.numberOfHitsPerPage.containsKey(
this.currentPage)) {
380 return this.numberOfHitsPerPage.get(this.currentPage);
static Version.Type getBuildType()
static synchronized Server getServer()
static final String CHUNK_ID_SEPARATOR
QueryResponse query(SolrQuery sq)
synchronized static Logger getLogger(String name)
int queryNumFileChunks(long fileID)