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)