/* * Autopsy Forensic Browser * * Copyright 2011-2014 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> 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.ingest; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellRenderer; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.ingest.IngestMessage.*; import org.sleuthkit.autopsy.ingest.IngestMessage.MessageType; import org.sleuthkit.datamodel.BlackboardArtifact; /** * Notification window showing messages from modules to user * */ class IngestMessagePanel extends JPanel implements TableModelListener { private final MessageTableModel tableModel; private MessageTableRenderer renderer; private final IngestMessageMainPanel mainPanel; private static final Color ERROR_COLOR = new Color(255, 90, 90); private volatile int lastRowSelected = -1; private volatile long totalMessages = 0; private static final Logger logger = Logger.getLogger(IngestMessagePanel.class.getName()); private static final PropertyChangeSupport messagePcs = new PropertyChangeSupport(IngestMessagePanel.class); static final String TOTAL_NUM_MESSAGES_CHANGED = "TOTAL_NUM_MESSAGES_CHANGED"; // total number of messages changed NON-NLS static final String MESSAGES_BOX_CLEARED = "MESSAGES_BOX_CLEARED"; // all messaged in inbox were cleared NON-NLS static final String TOTAL_NUM_NEW_MESSAGES_CHANGED = "TOTAL_NUM_NEW_MESSAGES_CHANGED"; // total number of new messages changed NON-NLS /** * Creates new form IngestMessagePanel */ public IngestMessagePanel(IngestMessageMainPanel mainPanel) { this.mainPanel = mainPanel; tableModel = new MessageTableModel(); initComponents(); customizeComponents(); } public void markAllSeen() { tableModel.markAllSeen(); } int getLastRowSelected() { return this.lastRowSelected; } synchronized IngestMessageGroup getSelectedMessage() { if (lastRowSelected < 0) { return null; } return tableModel.getMessageGroup(lastRowSelected); } synchronized IngestMessageGroup getMessageGroup(int rowNumber) { return tableModel.getMessageGroup(rowNumber); } synchronized static void addPropertyChangeSupportListener(PropertyChangeListener l) { messagePcs.addPropertyChangeListener(l); } /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always * regenerated by the Form Editor. */ @SuppressWarnings("unchecked") // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents private void initComponents() { jScrollPane1 = new javax.swing.JScrollPane(); messageTable = new javax.swing.JTable(); controlPanel = new javax.swing.JPanel(); sortByLabel = new javax.swing.JLabel(); sortByComboBox = new javax.swing.JComboBox<>(); totalMessagesNameLabel = new javax.swing.JLabel(); totalMessagesNameVal = new javax.swing.JLabel(); totalUniqueMessagesNameLabel = new javax.swing.JLabel(); totalUniqueMessagesNameVal = new javax.swing.JLabel(); setOpaque(false); jScrollPane1.setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 1, 1, 1)); jScrollPane1.setOpaque(false); jScrollPane1.setPreferredSize(new java.awt.Dimension(32767, 32767)); messageTable.setBackground(new java.awt.Color(221, 221, 235)); messageTable.setFont(messageTable.getFont().deriveFont(messageTable.getFont().getStyle() & ~java.awt.Font.BOLD, 12)); messageTable.setModel(tableModel); messageTable.setAutoResizeMode(javax.swing.JTable.AUTO_RESIZE_OFF); messageTable.setAutoscrolls(false); messageTable.setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR)); messageTable.setGridColor(new java.awt.Color(204, 204, 204)); messageTable.setOpaque(false); messageTable.setSelectionForeground(new java.awt.Color(0, 0, 0)); messageTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); messageTable.setShowHorizontalLines(false); messageTable.setShowVerticalLines(false); messageTable.getTableHeader().setReorderingAllowed(false); jScrollPane1.setViewportView(messageTable); sortByLabel.setText(org.openide.util.NbBundle.getMessage(IngestMessagePanel.class, "IngestMessagePanel.sortByLabel.text")); // NOI18N sortByComboBox.setFont(sortByComboBox.getFont().deriveFont(sortByComboBox.getFont().getStyle() & ~java.awt.Font.BOLD, 11)); sortByComboBox.setToolTipText(org.openide.util.NbBundle.getMessage(IngestMessagePanel.class, "IngestMessagePanel.sortByComboBox.toolTipText")); // NOI18N sortByComboBox.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { sortByComboBoxActionPerformed(evt); } }); totalMessagesNameLabel.setFont(totalMessagesNameLabel.getFont().deriveFont(totalMessagesNameLabel.getFont().getStyle() & ~java.awt.Font.BOLD, 11)); totalMessagesNameLabel.setText(org.openide.util.NbBundle.getMessage(IngestMessagePanel.class, "IngestMessagePanel.totalMessagesNameLabel.text")); // NOI18N totalMessagesNameVal.setFont(totalMessagesNameVal.getFont().deriveFont(totalMessagesNameVal.getFont().getStyle() & ~java.awt.Font.BOLD, 11)); totalMessagesNameVal.setText(org.openide.util.NbBundle.getMessage(IngestMessagePanel.class, "IngestMessagePanel.totalMessagesNameVal.text")); // NOI18N totalUniqueMessagesNameLabel.setFont(totalUniqueMessagesNameLabel.getFont().deriveFont(totalUniqueMessagesNameLabel.getFont().getStyle() & ~java.awt.Font.BOLD, 11)); totalUniqueMessagesNameLabel.setText(org.openide.util.NbBundle.getMessage(IngestMessagePanel.class, "IngestMessagePanel.totalUniqueMessagesNameLabel.text")); // NOI18N totalUniqueMessagesNameVal.setFont(totalUniqueMessagesNameVal.getFont().deriveFont(totalUniqueMessagesNameVal.getFont().getStyle() & ~java.awt.Font.BOLD, 11)); totalUniqueMessagesNameVal.setText(org.openide.util.NbBundle.getMessage(IngestMessagePanel.class, "IngestMessagePanel.totalUniqueMessagesNameVal.text")); // NOI18N javax.swing.GroupLayout controlPanelLayout = new javax.swing.GroupLayout(controlPanel); controlPanel.setLayout(controlPanelLayout); controlPanelLayout.setHorizontalGroup( controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(controlPanelLayout.createSequentialGroup() .addGap(10, 10, 10) .addComponent(sortByLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(sortByComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addGap(101, 101, 101) .addComponent(totalMessagesNameLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(totalMessagesNameVal, javax.swing.GroupLayout.DEFAULT_SIZE, 24, Short.MAX_VALUE) .addGap(22, 22, 22) .addComponent(totalUniqueMessagesNameLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(totalUniqueMessagesNameVal, javax.swing.GroupLayout.DEFAULT_SIZE, 24, Short.MAX_VALUE) .addGap(22, 22, 22)) ); controlPanelLayout.setVerticalGroup( controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(sortByComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(sortByLabel) .addComponent(totalUniqueMessagesNameLabel) .addComponent(totalUniqueMessagesNameVal) .addComponent(totalMessagesNameLabel) .addComponent(totalMessagesNameVal)) ); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(controlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 357, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 201, Short.MAX_VALUE) .addGap(0, 0, 0) .addComponent(controlPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) ); }// </editor-fold>//GEN-END:initComponents private void sortByComboBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_sortByComboBoxActionPerformed synchronized (this) { if (sortByComboBox.getSelectedIndex() == 0) { tableModel.reSort(true); } else { tableModel.reSort(false); } } }//GEN-LAST:event_sortByComboBoxActionPerformed // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JPanel controlPanel; private javax.swing.JScrollPane jScrollPane1; private javax.swing.JTable messageTable; private javax.swing.JComboBox<String> sortByComboBox; private javax.swing.JLabel sortByLabel; private javax.swing.JLabel totalMessagesNameLabel; private javax.swing.JLabel totalMessagesNameVal; private javax.swing.JLabel totalUniqueMessagesNameLabel; private javax.swing.JLabel totalUniqueMessagesNameVal; // End of variables declaration//GEN-END:variables private void customizeComponents() { mainPanel.setOpaque(true); jScrollPane1.setOpaque(true); messageTable.setOpaque(false); /** * It is not possible to internationalize the list of options in a ComboBox * inside of the generated form code. So, it is done here. */ sortByComboBox.setModel(new javax.swing.DefaultComboBoxModel<String>(new String[] { NbBundle.getMessage(this.getClass(), "IngestMessagePanel.sortByComboBox.model.time"), NbBundle.getMessage(this.getClass(), "IngestMessagePanel.sortByComboBox.model.priority")})); jScrollPane1.setWheelScrollingEnabled(true); messageTable.setAutoscrolls(false); messageTable.setShowHorizontalLines(false); messageTable.setShowVerticalLines(false); messageTable.getParent().setBackground(messageTable.getBackground()); renderer = new MessageTableRenderer(); int numCols = messageTable.getColumnCount(); // add the cell renderer to all columns for (int i = 0; i < numCols; ++i) { messageTable.getColumnModel().getColumn(i).setCellRenderer(renderer); } messageTable.setCellSelectionEnabled(false); messageTable.setColumnSelectionAllowed(false); messageTable.setRowSelectionAllowed(true); messageTable.getSelectionModel().addListSelectionListener(new MessageVisitedSelection()); //this should be done at the end to make it easy to initialize before events are handled tableModel.addTableModelListener(this); } @Override public void paint(Graphics g) { super.paint(g); //workaround to force table resize when window resizes. Use better layout instead? setTableSize(messageTable.getParent().getSize()); } @Override public void setPreferredSize(Dimension dmnsn) { super.setPreferredSize(dmnsn); setTableSize(messageTable.getParent().getSize()); } void setTableSize(Dimension d) { double[] columnWidths = new double[]{0.20, 0.08, 0.08, 0.49, 0.15}; int numCols = messageTable.getColumnCount(); for (int i = 0; i < numCols; ++i) { messageTable.getColumnModel().getColumn(i).setPreferredWidth((int) (d.width * columnWidths[i])); } } public synchronized void addMessage(IngestMessage m) { tableModel.addMessage(m); //update total individual messages count ++totalMessages; final int newMsgUnreadUnique = tableModel.getNumberUnreadGroups(); try { messagePcs.firePropertyChange(TOTAL_NUM_MESSAGES_CHANGED, 0, newMsgUnreadUnique); } catch (Exception e) { logger.log(Level.SEVERE, "IngestMessagePanel listener threw exception", e); //NON-NLS MessageNotifyUtil.Notify.show(NbBundle.getMessage(this.getClass(), "IngestMessagePanel.moduleErr"), NbBundle.getMessage(this.getClass(), "IngestMessagePanel.moduleErr.errListenUpdates.text"), MessageNotifyUtil.MessageType.ERROR); } //update labels this.totalMessagesNameVal.setText(Long.toString(totalMessages)); final int totalMessagesUnique = tableModel.getNumberGroups(); this.totalUniqueMessagesNameVal.setText(Integer.toString(totalMessagesUnique)); } public synchronized void clearMessages() { final int origMsgGroups = tableModel.getNumberUnreadGroups(); totalMessages = 0; tableModel.clearMessages(); totalMessagesNameVal.setText("-"); totalUniqueMessagesNameVal.setText("-"); try { messagePcs.firePropertyChange(MESSAGES_BOX_CLEARED, origMsgGroups, 0); } catch (Exception e) { logger.log(Level.SEVERE, "IngestMessagePanel listener threw exception", e); //NON-NLS MessageNotifyUtil.Notify.show(NbBundle.getMessage(this.getClass(), "IngestMessagePanel.moduleErr"), NbBundle.getMessage(this.getClass(), "IngestMessagePanel.moduleErr.errListenUpdates.text"), MessageNotifyUtil.MessageType.ERROR); } } public synchronized int getMessagesCount() { return tableModel.getNumberMessages(); } private synchronized void setVisited(int rowNumber) { final int origMsgGroups = tableModel.getNumberUnreadGroups(); tableModel.setVisited(rowNumber); lastRowSelected = rowNumber; try { messagePcs.firePropertyChange(TOOL_TIP_TEXT_KEY, origMsgGroups, tableModel.getNumberUnreadGroups()); } catch (Exception e) { logger.log(Level.SEVERE, "IngestMessagePanel listener threw exception", e); //NON-NLS MessageNotifyUtil.Notify.show(NbBundle.getMessage(this.getClass(), "IngestMessagePanel.moduleErr"), NbBundle.getMessage(this.getClass(), "IngestMessagePanel.moduleErr.errListenUpdates.text"), MessageNotifyUtil.MessageType.ERROR); } } @Override public void tableChanged(TableModelEvent e) { int newMessages = tableModel.getNumberNewMessages(); try { messagePcs.firePropertyChange(new PropertyChangeEvent(tableModel, TOTAL_NUM_NEW_MESSAGES_CHANGED, -1, newMessages)); } catch (Exception ee) { logger.log(Level.SEVERE, "IngestMessagePanel listener threw exception", ee); //NON-NLS MessageNotifyUtil.Notify.show(NbBundle.getMessage(this.getClass(), "IngestMessagePanel.moduleErr"), NbBundle.getMessage(this.getClass(), "IngestMessagePanel.moduleErr.errListenUpdates.text"), MessageNotifyUtil.MessageType.ERROR); } } private class MessageTableModel extends AbstractTableModel { private final String[] columnNames = new String[]{ NbBundle.getMessage(this.getClass(), "IngestMessagePanel.MsgTableMod.colNames.module"), NbBundle.getMessage(this.getClass(), "IngestMessagePanel.MsgTableMod.colNames.num"), NbBundle.getMessage(this.getClass(), "IngestMessagePanel.MsgTableMod.colNames.new"), NbBundle.getMessage(this.getClass(), "IngestMessagePanel.MsgTableMod.colNames.subject"), NbBundle.getMessage(this.getClass(), "IngestMessagePanel.MsgTableMod.colNames.timestamp")}; private final List<TableEntry> messageData = new ArrayList<>(); //for keeping track of messages to group, per module, by uniqness private final Map<String, Map<String, List<IngestMessageGroup>>> groupings = new HashMap<>(); private boolean chronoSort = true; //chronological sort default private static final int MESSAGE_GROUP_THRESH = 3; //group messages after 3 messages per module with same uniqness private final Logger logger = Logger.getLogger(MessageTableModel.class.getName()); @Override public int getColumnCount() { return columnNames.length; } @Override public synchronized int getRowCount() { return getNumberGroups(); } public synchronized void markAllSeen() { for (TableEntry entry : messageData) { entry.hasBeenSeen(true); } fireTableChanged(new TableModelEvent(this)); } public synchronized int getNumberNewMessages() { int newMessages = 0; for (TableEntry entry : messageData) { if (!entry.hasBeenSeen()) { ++newMessages; } } return newMessages; } public synchronized int getNumberGroups() { return messageData.size(); } public synchronized int getNumberMessages() { int total = 0; for (TableEntry e : messageData) { total += e.messageGroup.getCount(); } return total; } public synchronized int getNumberUnreadMessages() { int total = 0; for (TableEntry e : messageData) { if (!e.hasBeenVisited) { total += e.messageGroup.getCount(); } } return total; } public synchronized int getNumberUnreadGroups() { int total = 0; for (TableEntry e : messageData) { if (!e.hasBeenVisited) { ++total; } } return total; } @Override public String getColumnName(int column) { return columnNames[column]; } @Override public synchronized Object getValueAt(int rowIndex, int columnIndex) { Object ret = null; int numMessages = messageData.size(); if (rowIndex > messageData.size() - 1 || columnIndex > columnNames.length - 1) { //temporary check if the rare case still occurrs //#messages is now lower after last regrouping, and gui event thinks it's not logger.log(Level.WARNING, "Requested inbox message at" + rowIndex, ", only have " + numMessages); //NON-NLS return ""; } TableEntry entry = messageData.get(rowIndex); switch (columnIndex) { case 0: ret = entry.messageGroup.getSource(); break; case 1: ret = entry.messageGroup.getCount(); break; case 2: ret = !entry.hasBeenSeen(); break; case 3: ret = entry.messageGroup.getSubject(); break; case 4: ret = entry.messageGroup.getDatePosted(); break; default: logger.log(Level.SEVERE, "Invalid table column index: {0}", columnIndex); //NON-NLS break; } return ret; } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { return false; } @Override public Class<?> getColumnClass(int c) { return getValueAt(0, c).getClass(); } private synchronized int getTableEntryIndex(String uniqueKey) { int ret = -1; int i = 0; for (TableEntry e : messageData) { if (e.messageGroup.getUniqueKey().equals(uniqueKey)) { ret = i; break; } ++i; } return ret; } public synchronized void addMessage(IngestMessage m) { //check how many messages per module with the same uniqness //and add to existing group or create a new group String moduleName = m.getSource(); IngestMessageGroup messageGroup = null; if (moduleName != null && m.getMessageType() == IngestMessage.MessageType.DATA) { //not a manager message, a data message, then group if (!groupings.containsKey(moduleName)) { groupings.put(moduleName, new HashMap<String, List<IngestMessageGroup>>()); } final Map<String, List<IngestMessageGroup>> groups = groupings.get(moduleName); //groups for this uniqueness final String uniqueness = m.getUniqueKey(); List<IngestMessageGroup> uniqGroups = groups.get(uniqueness); if (uniqGroups == null) { //first one with this uniqueness uniqGroups = new ArrayList<>(); messageGroup = new IngestMessageGroup(m); uniqGroups.add(messageGroup); groups.put(uniqueness, uniqGroups); } else { final int uniqueGroupsCount = uniqGroups.size(); if (uniqueGroupsCount > MESSAGE_GROUP_THRESH) { //merge them messageGroup = uniqGroups.get(0); for (int i = 1; i < uniqueGroupsCount; ++i) { messageGroup.addAll(uniqGroups.get(i)); } //add the new msg messageGroup.add(m); //remove merged groups uniqGroups.clear(); //add the group with all messages merged uniqGroups.add(messageGroup); //remove all rows with this uniquness, new merged row will be added to the bottom int toRemove; while ((toRemove = getTableEntryIndex(uniqueness)) != -1) { messageData.remove(toRemove); //remove the row, will be added to the bottom this.fireTableRowsDeleted(toRemove, toRemove); } } else if (uniqueGroupsCount == 1) { IngestMessageGroup first = uniqGroups.get(0); //one group with multiple messages if (first.getCount() > 1) { //had already been merged first.add(m); messageGroup = first; //move to bottom of table //remove from existing position int toRemove = 0; while ((toRemove = getTableEntryIndex(uniqueness)) != -1) { messageData.remove(toRemove); //remove the row, will be added to the bottom this.fireTableRowsDeleted(toRemove, toRemove); } } else { //one group with one message //create another group messageGroup = new IngestMessageGroup(m); uniqGroups.add(messageGroup); } } else { //multiple groups with 1 msg each //create another group, until need to merge messageGroup = new IngestMessageGroup(m); uniqGroups.add(messageGroup); //add to bottom } } } else { //manager or non-data message messageGroup = new IngestMessageGroup(m); } //add new or updated row to the bottom messageData.add(new TableEntry(messageGroup)); int newRowIndex = messageData.size() - 1; fireTableRowsInserted(newRowIndex, newRowIndex); //if priority sort, need to re-sort everything if (chronoSort == false) { Collections.sort(messageData); fireTableDataChanged(); } } public synchronized void clearMessages() { messageData.clear(); groupings.clear(); fireTableDataChanged(); } public synchronized void setVisited(int rowNumber) { messageData.get(rowNumber).hasBeenVisited(true); //repaint the cell fireTableCellUpdated(rowNumber, 2); } public synchronized void setVisitedAll() { int row = 0; for (TableEntry e : messageData) { if (!e.hasBeenVisited) { e.hasBeenVisited(true); fireTableCellUpdated(row, 2); } ++row; } } public synchronized boolean isVisited(int rowNumber) { if (rowNumber < messageData.size()) { return messageData.get(rowNumber).hasBeenVisited(); } else { return false; } } public synchronized MessageType getMessageType(int rowNumber) { if (rowNumber < messageData.size()) { return messageData.get(rowNumber).messageGroup.getMessageType(); } else { return null; } } public synchronized IngestMessageGroup getMessageGroup(int rowNumber) { if (rowNumber < messageData.size()) { return messageData.get(rowNumber).messageGroup; } else { return null; } } public synchronized void reSort(boolean chronoLogical) { if (chronoSort == chronoLogical) { return; } chronoSort = chronoLogical; Collections.sort(messageData); fireTableDataChanged(); } class TableEntry implements Comparable<TableEntry> { IngestMessageGroup messageGroup; boolean hasBeenVisited = false; boolean hasBeenSeen = false; public boolean hasBeenVisited() { return hasBeenVisited; } public void hasBeenVisited(boolean visited) { hasBeenVisited = visited; } public boolean hasBeenSeen() { return hasBeenSeen; } public void hasBeenSeen(boolean seen) { hasBeenSeen = seen; } TableEntry(IngestMessageGroup messageGroup) { this.messageGroup = messageGroup; } @Override public int compareTo(TableEntry o) { if (chronoSort == true) { return this.messageGroup.getDatePosted().compareTo(o.messageGroup.getDatePosted()); } else { return messageGroup.getCount() - o.messageGroup.getCount(); } } } } //represents grouping of similar messages //with the same uniqness static class IngestMessageGroup { static final Color VERY_HIGH_PRI_COLOR = new Color(164, 164, 202); //for a single message in a group static final Color HIGH_PRI_COLOR = new Color(180, 180, 211); static final Color MED_PRI_COLOR = new Color(199, 199, 222); static final Color LOW_PRI_COLOR = new Color(221, 221, 235); private final List<IngestMessage> messages; IngestMessageGroup(IngestMessage message) { messages = new ArrayList<>(); messages.add(message); } private List<IngestMessage> getMessages() { return messages; } synchronized void add(IngestMessage message) { messages.add(message); } //add all messages from another group synchronized void addAll(IngestMessageGroup group) { for (IngestMessage m : group.getMessages()) { messages.add(m); } } synchronized int getCount() { return messages.size(); } synchronized String getDetails() { StringBuilder b = new StringBuilder(""); for (IngestMessage m : messages) { String details = m.getDetails(); if (details == null || details.equals("")) { continue; } b.append(details); b.append("<br />"); //NON-NLS b.append("<hr />"); //NON-NLS } return b.toString(); } /** * return color corresp to priority * * @return */ synchronized Color getColor() { int count = messages.size(); if (count == 1) { return VERY_HIGH_PRI_COLOR; } else if (count < 5) { return HIGH_PRI_COLOR; } else if (count < 15) { return MED_PRI_COLOR; } else { return LOW_PRI_COLOR; } } /** * return date of the last message of the group used for chrono sort * * @return */ synchronized Date getDatePosted() { return messages.get(messages.size() - 1).getDatePosted(); } /** * get subject of the first message * * @return */ synchronized String getSubject() { return messages.get(0).getSubject(); } /* * return unique key, should be the same for all msgs */ synchronized String getUniqueKey() { return messages.get(0).getUniqueKey(); } /* * return source module, should be the same for all msgs */ synchronized String getSource() { return messages.get(0).getSource(); } /* * return data of the first message */ synchronized BlackboardArtifact getData() { return messages.get(0).getData(); } /* * return message type, should be the same for all msgs */ synchronized IngestMessage.MessageType getMessageType() { return messages.get(0).getMessageType(); } } /* * Main TableCellRenderer to be used for ingest message inbox. Delegates to * other TableCellRenderers based different factors such as column data type * or column number. */ private class MessageTableRenderer extends DefaultTableCellRenderer { private final TableCellRenderer booleanRenderer = new BooleanRenderer(); private final TableCellRenderer defaultRenderer = new DefaultRenderer(); private final TableCellRenderer dateRenderer = new DateRenderer(); protected int rowSelected; public void setRowSelected(int rowSelected) { this.rowSelected = rowSelected; } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { if (value instanceof Boolean) { return booleanRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); } else if (value instanceof Date) { return dateRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); } else { return defaultRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); } } } /* * TableCellRenderer used to render boolean values with a bullet point. */ private class BooleanRenderer extends DefaultTableCellRenderer { private final String bulletChar = new String(Character.toChars(0x2022)); @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { super.setForeground(table.getSelectionForeground()); super.setBackground(table.getSelectionBackground()); boolean boolVal; if (value instanceof Boolean) { boolVal = ((Boolean) value); } else { throw new RuntimeException(NbBundle.getMessage(this.getClass(), "IngestMessagePanel.BooleanRenderer.exception.nonBoolVal.msg")); } String aValue = boolVal ? bulletChar : ""; JLabel cell = (JLabel) super.getTableCellRendererComponent(table, aValue, isSelected, hasFocus, row, column); // center the bullet in the JLabel cell.setHorizontalAlignment(SwingConstants.CENTER); // increase the font size cell.setFont(cell.getFont().deriveFont(Font.PLAIN, 16)); final IngestMessageGroup messageGroup = tableModel.getMessageGroup(row); if (messageGroup != null) { MessageType mt = messageGroup.getMessageType(); if (mt == MessageType.ERROR) { cell.setBackground(ERROR_COLOR); } else if (mt == MessageType.WARNING) { cell.setBackground(Color.orange); } else { //cell.setBackground(table.getBackground()); cell.setBackground(messageGroup.getColor()); } } return cell; } } /** * bold font if not visited, colors for errors tooltips that show entire * query string, disable selection borders */ private class DefaultRenderer extends DefaultTableCellRenderer { @Override public Component getTableCellRendererComponent( JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Component cell = super.getTableCellRendererComponent( table, value, false, false, row, column); Font visitedFont = cell.getFont().deriveFont(Font.PLAIN, 12); Font notVisitedFont = cell.getFont().deriveFont(Font.BOLD, 12); if (column == 3) { String subject = (String) value; setToolTipText(subject); if (tableModel.isVisited(row)) { cell.setFont(visitedFont); } else { cell.setFont(notVisitedFont); } } final IngestMessageGroup messageGroup = tableModel.getMessageGroup(row); if (messageGroup != null) { MessageType mt = messageGroup.getMessageType(); if (mt == MessageType.ERROR) { cell.setBackground(ERROR_COLOR); } else if (mt == MessageType.WARNING) { cell.setBackground(Color.orange); } else { //cell.setBackground(table.getBackground()); cell.setBackground(messageGroup.getColor()); } } return cell; } } private class DateRenderer extends DefaultTableCellRenderer { @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { super.setForeground(table.getSelectionForeground()); super.setBackground(table.getSelectionBackground()); Object aValue = value; if (value instanceof Date) { Date date = (Date) value; DateFormat df = new SimpleDateFormat("HH:mm:ss"); aValue = df.format(date); } else { throw new RuntimeException(NbBundle.getMessage(this.getClass(), "IngestMessagePanel.DateRenderer.exception.nonDateVal.text")); } Component cell = super.getTableCellRendererComponent(table, aValue, isSelected, hasFocus, row, column); final IngestMessageGroup messageGroup = tableModel.getMessageGroup(row); if (messageGroup != null) { MessageType mt = messageGroup.getMessageType(); if (mt == MessageType.ERROR) { cell.setBackground(ERROR_COLOR); } else if (mt == MessageType.WARNING) { cell.setBackground(Color.orange); } else { //cell.setBackground(table.getBackground()); cell.setBackground(messageGroup.getColor()); } } return cell; } } /** * handle table selections / cell visitations */ private class MessageVisitedSelection implements ListSelectionListener { @Override public void valueChanged(ListSelectionEvent e) { ListSelectionModel selModel = (ListSelectionModel) e.getSource(); if (selModel.isSelectionEmpty() || selModel.getValueIsAdjusting()) { return; } final int minIndex = selModel.getMinSelectionIndex(); final int maxIndex = selModel.getMaxSelectionIndex(); int selected = -1; for (int i = minIndex; i <= maxIndex; i++) { if (selModel.isSelectedIndex(i)) { selected = i; break; } } selModel.clearSelection(); if (selected != -1) { setVisited(selected); messageTable.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); //check if has details IngestMessageGroup m = getMessageGroup(selected); if (m != null) { String details = m.getDetails(); if (details != null && !details.equals("")) { mainPanel.showDetails(selected); } } messageTable.setCursor(null); } } } }