diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/Bundle.properties-MERGED index 317d7050de..07acb89612 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/Bundle.properties-MERGED @@ -61,6 +61,7 @@ DefaultArtifactContentViewer.copyMenuItem.text=Copy DefaultArtifactContentViewer.selectAllMenuItem.text=Select All MessageAccountPanel_button_create_label=Create MessageAccountPanel_button_view_label=View +MessageAccountPanel_contact_label=Contact: MessageAccountPanel_no_matches=No matches found. MessageAccountPanel_persona_label=Persona: MessageAccountPanel_unknown_label=Unknown diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageAccountPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageAccountPanel.java index 050fca83fe..e51577819a 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageAccountPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageAccountPanel.java @@ -16,7 +16,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.sleuthkit.autopsy.contentviewers.artifactviewers; import java.awt.event.ActionEvent; @@ -39,6 +38,7 @@ import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; import org.sleuthkit.autopsy.centralrepository.datamodel.Persona; import org.sleuthkit.autopsy.centralrepository.datamodel.PersonaAccount; import org.sleuthkit.autopsy.centralrepository.persona.PersonaDetailsDialog; @@ -46,8 +46,10 @@ import org.sleuthkit.autopsy.centralrepository.persona.PersonaDetailsDialogCallb import org.sleuthkit.autopsy.centralrepository.persona.PersonaDetailsMode; import org.sleuthkit.autopsy.centralrepository.persona.PersonaDetailsPanel; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.guiutils.ContactCache; import org.sleuthkit.datamodel.Account; import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.CommunicationsManager; import org.sleuthkit.datamodel.TskCoreException; @@ -113,13 +115,24 @@ final class MessageAccountPanel extends JPanel { return new ArrayList<>(); } - Collection personAccounts = PersonaAccount.getPersonaAccountsForAccount(account); - if (personAccounts != null && !personAccounts.isEmpty()) { - for (PersonaAccount personaAccount : PersonaAccount.getPersonaAccountsForAccount(account)) { - dataList.add(new AccountContainer(account, personaAccount)); + List contactList = ContactCache.getContacts(account); + BlackboardArtifact contact = null; + + if (contactList != null && !contactList.isEmpty()) { + contact = contactList.get(0); + } + + if (CentralRepository.isEnabled()) { + Collection personAccounts = PersonaAccount.getPersonaAccountsForAccount(account); + if (personAccounts != null && !personAccounts.isEmpty()) { + for (PersonaAccount personaAccount : PersonaAccount.getPersonaAccountsForAccount(account)) { + dataList.add(new AccountContainer(account, personaAccount, contact)); + } + } else { + dataList.add(new AccountContainer(account, null, contact)); } } else { - dataList.add(new AccountContainer(account, null)); + dataList.add(new AccountContainer(account, null, contact)); } } @@ -127,8 +140,7 @@ final class MessageAccountPanel extends JPanel { } @Messages({ - "MessageAccountPanel_no_matches=No matches found.", - }) + "MessageAccountPanel_no_matches=No matches found.",}) @Override protected void done() { try { @@ -199,6 +211,7 @@ final class MessageAccountPanel extends JPanel { for (AccountContainer o : data) { group.addGap(5) .addComponent(o.getAccountLabel()) + .addGroup(o.getContactLineVerticalGroup(layout)) .addGroup(o.getPersonLineVerticalGroup(layout)); } @@ -234,6 +247,7 @@ final class MessageAccountPanel extends JPanel { group.addGap(10); for (AccountContainer o : data) { pgroup.addGroup(o.getPersonaSequentialGroup(layout)); + pgroup.addGroup(o.getContactSequentialGroup(layout)); } group.addGap(10) .addGroup(pgroup) @@ -253,10 +267,13 @@ final class MessageAccountPanel extends JPanel { private final Account account; private Persona persona = null; + private final String contactName; private JLabel accountLabel; private JLabel personaHeader; private JLabel personaDisplayName; + private JLabel contactHeader; + private JLabel contactDisplayName; private JButton button; /** @@ -265,16 +282,22 @@ final class MessageAccountPanel extends JPanel { * @param account * @param personaAccount */ - AccountContainer(Account account, PersonaAccount personaAccount) { + AccountContainer(Account account, PersonaAccount personaAccount, BlackboardArtifact contactArtifact) throws TskCoreException { + if (contactArtifact != null && contactArtifact.getArtifactTypeID() != BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT.getTypeID()) { + throw new IllegalArgumentException("Failed to create AccountContainer object, passed in artifact was not a TSK_CONTACT"); + } + this.account = account; this.persona = personaAccount != null ? personaAccount.getPersona() : null; + this.contactName = getNameFromContactArtifact(contactArtifact); } @Messages({ "MessageAccountPanel_persona_label=Persona:", "MessageAccountPanel_unknown_label=Unknown", "MessageAccountPanel_button_view_label=View", - "MessageAccountPanel_button_create_label=Create" + "MessageAccountPanel_button_create_label=Create", + "MessageAccountPanel_contact_label=Contact:" }) /** * Swing components will not be initialized until this method is called. @@ -282,16 +305,29 @@ final class MessageAccountPanel extends JPanel { private void initalizeSwingControls() { accountLabel = new JLabel(); personaHeader = new JLabel(Bundle.MessageAccountPanel_persona_label()); + contactHeader = new JLabel(Bundle.MessageAccountPanel_contact_label()); personaDisplayName = new JLabel(); + contactDisplayName = new JLabel(); button = new JButton(); button.addActionListener(new PersonaButtonListener(this)); accountLabel.setText(account.getTypeSpecificID()); - + contactDisplayName.setText(contactName); personaDisplayName.setText(persona != null ? persona.getName() : Bundle.MessageAccountPanel_unknown_label()); button.setText(persona != null ? Bundle.MessageAccountPanel_button_view_label() : Bundle.MessageAccountPanel_button_create_label()); } + private String getNameFromContactArtifact(BlackboardArtifact contactArtifact) throws TskCoreException { + if (contactArtifact != null) { + BlackboardAttribute attribute = contactArtifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME)); + if (attribute != null) { + return attribute.getValueString(); + } + } + + return Bundle.MessageAccountPanel_unknown_label(); + } + /** * Sets a new persona for this object and update the controls. * @@ -365,6 +401,17 @@ final class MessageAccountPanel extends JPanel { return group; } + + private SequentialGroup getContactSequentialGroup(GroupLayout layout) { + SequentialGroup group = layout.createSequentialGroup(); + + group + .addComponent(contactHeader) + .addPreferredGap(ComponentPlacement.RELATED) + .addComponent(contactDisplayName); + + return group; + } /** * Generates the vertical layout code for the persona line. @@ -379,6 +426,12 @@ final class MessageAccountPanel extends JPanel { .addComponent(personaDisplayName) .addComponent(button); } + + private ParallelGroup getContactLineVerticalGroup(GroupLayout layout) { + return layout.createParallelGroup(Alignment.BASELINE) + .addComponent(contactHeader) + .addComponent(contactDisplayName); + } } /** diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageArtifactViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageArtifactViewer.java index f8257afb64..812fb7290d 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageArtifactViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageArtifactViewer.java @@ -447,10 +447,8 @@ public class MessageArtifactViewer extends javax.swing.JPanel implements Artifac resetComponent(); } - msgbodyTabbedPane.setEnabledAt(ACCT_TAB_INDEX, CentralRepository.isEnabled()); - if(CentralRepository.isEnabled()) { - accountsPanel.setArtifact(artifact); - } + msgbodyTabbedPane.setEnabledAt(ACCT_TAB_INDEX, true); + accountsPanel.setArtifact(artifact); } /** diff --git a/Core/src/org/sleuthkit/autopsy/guiutils/ContactCache.java b/Core/src/org/sleuthkit/autopsy/guiutils/ContactCache.java new file mode 100755 index 0000000000..f98d45edcb --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/guiutils/ContactCache.java @@ -0,0 +1,160 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.guiutils; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import java.beans.PropertyChangeListener; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.ingest.IngestManager; +import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.DATA_ADDED; +import org.sleuthkit.autopsy.ingest.ModuleDataEvent; +import org.sleuthkit.datamodel.Account; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * A singleton cache of the Contact artifacts for accounts. The map of account + * unique ids to list of contact artifacts is stored in a LoadingCache which + * expires after 10 of non-use. + * + */ +public final class ContactCache { + + private static final Logger logger = Logger.getLogger(ContactCache.class.getName()); + + private static ContactCache instance; + + private final LoadingCache>> accountMap; + + /** + * Returns the list of Contacts for the given Account. + * + * @param account Account instance. + * + * @return List of TSK_CONTACT artifacts that references the given Account. + * An empty list is returned if no contacts are found. + * + * @throws ExecutionException + */ + public static synchronized List getContacts(Account account) throws ExecutionException { + return getInstance().accountMap.get("realMap").get(account.getTypeSpecificID()); + } + + /** + * Force the cache to invalidate all entries. + */ + static synchronized void invalidateCache() { + getInstance().accountMap.invalidateAll(); + } + + /** + * Construct a new instance. + */ + private ContactCache() { + + accountMap = CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES).build( + new CacheLoader>>() { + @Override + public Map> load(String key) { + try { + return buildMap(); + } catch (SQLException | TskCoreException ex) { + logger.log(Level.WARNING, "Failed to build account to contact map", ex); + } + return new HashMap<>(); // Return an empty map if there is an exception to avoid NPE and continual trying. + } + }); + + PropertyChangeListener ingestListener = pce -> { + String eventType = pce.getPropertyName(); + if (eventType.equals(DATA_ADDED.toString())) { + ModuleDataEvent eventData = (ModuleDataEvent) pce.getOldValue(); + if (eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT.getTypeID()) { + invalidateCache(); + } + } + }; + + IngestManager.getInstance().addIngestModuleEventListener(EnumSet.of(DATA_ADDED), ingestListener); + } + + /** + * Returns the singleton instance of the cache object. + * + * @return AccountCache instance. + */ + private static synchronized ContactCache getInstance() { + if (instance == null) { + instance = new ContactCache(); + } + + return instance; + } + + /** + * Builds the map of account IDs to contacts that reference them. + * + * @return A map of account IDs to contact artifacts. + * + * @throws TskCoreException + * @throws SQLException + */ + private Map> buildMap() throws TskCoreException, SQLException { + Map> acctMap = new HashMap<>(); + List contactList = Case.getCurrentCase().getSleuthkitCase().getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT); + + for(BlackboardArtifact contactArtifact: contactList) { + List contactAttributes = contactArtifact.getAttributes(); + for(BlackboardAttribute attribute: contactAttributes) { + String typeName = attribute.getAttributeType().getTypeName(); + + if(typeName.startsWith("TSK_EMAIL") + || typeName.startsWith("TSK_PHONE") + || typeName.startsWith("TSK_NAME") + || typeName.startsWith("TSK_ID")) { + String accountID = attribute.getValueString(); + List artifactList = acctMap.get(accountID); + if(artifactList == null) { + artifactList = new ArrayList<>(); + acctMap.put(accountID, artifactList); + } + if(!artifactList.contains(contactArtifact)) { + artifactList.add(contactArtifact); + } + } + } + + } + + return acctMap; + } +}