Autopsy  4.14.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
HealthMonitorDashboard.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2018 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.healthmonitor;
20 
21 import java.awt.Container;
22 import java.awt.Cursor;
23 import java.awt.Dimension;
24 import java.util.Set;
25 import java.util.HashSet;
26 import java.util.HashMap;
27 import java.util.Arrays;
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.awt.event.ActionEvent;
31 import java.awt.event.ActionListener;
32 import java.io.File;
33 import javax.swing.Box;
34 import javax.swing.JButton;
35 import javax.swing.JDialog;
36 import javax.swing.JComboBox;
37 import javax.swing.JSeparator;
38 import javax.swing.JCheckBox;
39 import javax.swing.JLabel;
40 import javax.swing.JPanel;
41 import javax.swing.JScrollPane;
42 import javax.swing.BorderFactory;
43 import java.util.Map;
44 import javax.swing.BoxLayout;
45 import java.awt.GridLayout;
46 import java.nio.file.Paths;
47 import java.util.logging.Level;
48 import java.util.stream.Collectors;
49 import org.openide.util.NbBundle;
53 
57 public class HealthMonitorDashboard {
58 
59  private final static Logger logger = Logger.getLogger(HealthMonitorDashboard.class.getName());
60 
61  private final static String ADMIN_ACCESS_FILE_NAME = "admin"; // NON-NLS
62  private final static String ADMIN_ACCESS_FILE_PATH = Paths.get(PlatformUtil.getUserConfigDirectory(), ADMIN_ACCESS_FILE_NAME).toString();
63 
64  Map<String, List<HealthMonitor.DatabaseTimingResult>> timingData;
65  List<HealthMonitor.UserData> userData;
66 
67  private JComboBox<String> timingDateComboBox = null;
68  private JComboBox<String> timingHostComboBox = null;
69  private JCheckBox timingHostCheckBox = null;
70  private JCheckBox timingShowTrendLineCheckBox = null;
71  private JCheckBox timingSkipOutliersCheckBox = null;
72  private JPanel timingGraphPanel = null;
73  private JComboBox<String> userDateComboBox = null;
74  private JPanel userGraphPanel = null;
75  private JDialog dialog = null;
76  private final Container parentWindow;
77 
83  public HealthMonitorDashboard(Container parent) {
84  timingData = new HashMap<>();
85  userData = new ArrayList<>();
86  parentWindow = parent;
87  }
88 
92  @NbBundle.Messages({"HealthMonitorDashboard.display.errorCreatingDashboard=Error creating health monitor dashboard",
93  "HealthMonitorDashboard.display.dashboardTitle=Health Monitor"})
94  public void display() {
95 
96  // Update the enabled status and get the timing data, then create all
97  // the sub panels.
98  JPanel timingPanel;
99  JPanel userPanel;
100  JPanel adminPanel;
101  try {
102  updateData();
103  timingPanel = createTimingPanel();
104  userPanel = createUserPanel();
105  adminPanel = createAdminPanel();
106  } catch (HealthMonitorException ex) {
107  logger.log(Level.SEVERE, "Error creating panels for health monitor dashboard", ex);
108  MessageNotifyUtil.Message.error(Bundle.HealthMonitorDashboard_display_errorCreatingDashboard());
109  return;
110  }
111 
112  // Create the main panel for the dialog
113  JPanel mainPanel = new JPanel();
114  mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
115 
116  // Add the timing panel
117  mainPanel.add(timingPanel);
118 
119  // Add the user panel
120  mainPanel.add(userPanel);
121 
122  // Add the admin panel if the admin file is present
123  File adminFile = new File(ADMIN_ACCESS_FILE_PATH);
124  if(adminFile.exists()) {
125  mainPanel.add(adminPanel);
126  }
127 
128  // Create and show the dialog
129  dialog = new JDialog();
130  dialog.setTitle(Bundle.HealthMonitorDashboard_display_dashboardTitle());
131  dialog.add(mainPanel);
132  dialog.pack();
133  dialog.setLocationRelativeTo(parentWindow);
134  dialog.setVisible(true);
135  }
136 
141  private void redisplay() {
142  if (dialog != null) {
143  dialog.setVisible(false);
144  dialog.dispose();
145  }
146  display();
147  }
148 
153  private void updateData() throws HealthMonitorException {
154 
155  // Update the monitor status
156  HealthMonitor.getInstance().updateFromGlobalEnabledStatus();
157 
158  if(HealthMonitor.monitorIsEnabled()) {
159  // Get a copy of the timing data from the database
160  timingData = HealthMonitor.getInstance().getTimingMetricsFromDatabase(DateRange.getMaximumTimestampRange());
161 
162  // Get a copy of the user data from the database
163  userData = HealthMonitor.getInstance().getUserMetricsFromDatabase(DateRange.getMaximumTimestampRange());
164  }
165  }
166 
172  @NbBundle.Messages({"HealthMonitorDashboard.createTimingPanel.noData=No data to display - monitor is not enabled",
173  "HealthMonitorDashboard.createTimingPanel.timingMetricsTitle=Timing Metrics"})
174  private JPanel createTimingPanel() throws HealthMonitorException {
175 
176  // If the monitor isn't enabled, just add a message
177  if(! HealthMonitor.monitorIsEnabled()) {
178  //timingMetricPanel.setPreferredSize(new Dimension(400,100));
179  JPanel emptyTimingMetricPanel = new JPanel();
180  emptyTimingMetricPanel.add(new JLabel(Bundle.HealthMonitorDashboard_createTimingPanel_timingMetricsTitle()));
181  emptyTimingMetricPanel.add(new JLabel(" "));
182  emptyTimingMetricPanel.add(new JLabel(Bundle.HealthMonitorDashboard_createTimingPanel_noData()));
183 
184  return emptyTimingMetricPanel;
185  }
186 
187  JPanel timingMetricPanel = new JPanel();
188  timingMetricPanel.setLayout(new BoxLayout(timingMetricPanel, BoxLayout.PAGE_AXIS));
189  timingMetricPanel.setBorder(BorderFactory.createEtchedBorder());
190 
191  // Add title
192  JLabel timingMetricTitle = new JLabel(Bundle.HealthMonitorDashboard_createTimingPanel_timingMetricsTitle());
193  timingMetricPanel.add(timingMetricTitle);
194  timingMetricPanel.add(new JSeparator());
195 
196  // Add the controls
197  timingMetricPanel.add(createTimingControlPanel());
198  timingMetricPanel.add(new JSeparator());
199 
200  // Create panel to hold graphs
201  timingGraphPanel = new JPanel();
202  timingGraphPanel.setLayout(new GridLayout(0,2));
203 
204  // Update the graph panel, put it in a scroll pane, and add to the timing metric panel
206  JScrollPane scrollPane = new JScrollPane(timingGraphPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
207  timingMetricPanel.add(scrollPane);
208  timingMetricPanel.revalidate();
209  timingMetricPanel.repaint();
210 
211  return timingMetricPanel;
212  }
213 
218  @NbBundle.Messages({"HealthMonitorDashboard.createTimingControlPanel.filterByHost=Filter by host",
219  "HealthMonitorDashboard.createTimingControlPanel.maxDays=Max days to display",
220  "HealthMonitorDashboard.createTimingControlPanel.skipOutliers=Do not plot outliers",
221  "HealthMonitorDashboard.createTimingControlPanel.showTrendLine=Show trend line"})
222  private JPanel createTimingControlPanel() {
223  JPanel timingControlPanel = new JPanel();
224 
225  // If the monitor is not enabled, don't add any components
226  if(! HealthMonitor.monitorIsEnabled()) {
227  return timingControlPanel;
228  }
229 
230  // Create the combo box for selecting how much data to display
231  String[] dateOptionStrings = Arrays.stream(DateRange.values()).map(e -> e.getLabel()).toArray(String[]::new);
232  timingDateComboBox = new JComboBox<>(dateOptionStrings);
233  timingDateComboBox.setSelectedItem(DateRange.ONE_DAY.getLabel());
234 
235  // Set up the listener on the date combo box
236  timingDateComboBox.addActionListener(new ActionListener() {
237  @Override
238  public void actionPerformed(ActionEvent arg0) {
239  try {
241  } catch (HealthMonitorException ex) {
242  logger.log(Level.SEVERE, "Error updating timing metric panel", ex);
243  }
244  }
245  });
246 
247  // Create an array of host names
248  Set<String> hostNameSet = new HashSet<>();
249  for(String metricType:timingData.keySet()) {
250  for(HealthMonitor.DatabaseTimingResult result: timingData.get(metricType)) {
251  hostNameSet.add(result.getHostName());
252  }
253  }
254 
255  // Load the host names into the combo box
256  timingHostComboBox = new JComboBox<>(hostNameSet.toArray(new String[hostNameSet.size()]));
257 
258  // Set up the listener on the combo box
259  timingHostComboBox.addActionListener(new ActionListener() {
260  @Override
261  public void actionPerformed(ActionEvent arg0) {
262  try {
263  if((timingHostCheckBox != null) && timingHostCheckBox.isSelected()) {
265  }
266  } catch (HealthMonitorException ex) {
267  logger.log(Level.SEVERE, "Error populating timing metric panel", ex);
268  }
269  }
270  });
271 
272  // Create the host checkbox
273  timingHostCheckBox = new JCheckBox(Bundle.HealthMonitorDashboard_createTimingControlPanel_filterByHost());
274  timingHostCheckBox.setSelected(false);
275  timingHostComboBox.setEnabled(false);
276 
277  // Set up the listener on the checkbox
278  timingHostCheckBox.addActionListener(new ActionListener() {
279  @Override
280  public void actionPerformed(ActionEvent arg0) {
281  try {
282  timingHostComboBox.setEnabled(timingHostCheckBox.isSelected());
284  } catch (HealthMonitorException ex) {
285  logger.log(Level.SEVERE, "Error populating timing metric panel", ex);
286  }
287  }
288  });
289 
290  // Create the checkbox for showing the trend line
291  timingShowTrendLineCheckBox = new JCheckBox(Bundle.HealthMonitorDashboard_createTimingControlPanel_showTrendLine());
292  timingShowTrendLineCheckBox.setSelected(true);
293 
294  // Set up the listener on the checkbox
295  timingShowTrendLineCheckBox.addActionListener(new ActionListener() {
296  @Override
297  public void actionPerformed(ActionEvent arg0) {
298  try {
300  } catch (HealthMonitorException ex) {
301  logger.log(Level.SEVERE, "Error populating timing metric panel", ex);
302  }
303  }
304  });
305 
306  // Create the checkbox for omitting outliers
307  timingSkipOutliersCheckBox = new JCheckBox(Bundle.HealthMonitorDashboard_createTimingControlPanel_skipOutliers());
308  timingSkipOutliersCheckBox.setSelected(false);
309 
310  // Set up the listener on the checkbox
311  timingSkipOutliersCheckBox.addActionListener(new ActionListener() {
312  @Override
313  public void actionPerformed(ActionEvent arg0) {
314  try {
316  } catch (HealthMonitorException ex) {
317  logger.log(Level.SEVERE, "Error populating timing metric panel", ex);
318  }
319  }
320  });
321 
322  // Add the date range combo box and label to the panel
323  timingControlPanel.add(new JLabel(Bundle.HealthMonitorDashboard_createTimingControlPanel_maxDays()));
324  timingControlPanel.add(timingDateComboBox);
325 
326  // Put some space between the elements
327  timingControlPanel.add(Box.createHorizontalStrut(100));
328 
329  // Add the host combo box and checkbox to the panel
330  timingControlPanel.add(timingHostCheckBox);
331  timingControlPanel.add(timingHostComboBox);
332 
333  // Put some space between the elements
334  timingControlPanel.add(Box.createHorizontalStrut(100));
335 
336  // Add the skip outliers checkbox
337  timingControlPanel.add(this.timingShowTrendLineCheckBox);
338 
339  // Put some space between the elements
340  timingControlPanel.add(Box.createHorizontalStrut(100));
341 
342  // Add the skip outliers checkbox
343  timingControlPanel.add(this.timingSkipOutliersCheckBox);
344 
345  return timingControlPanel;
346  }
347 
352  @NbBundle.Messages({"HealthMonitorDashboard.updateTimingMetricGraphs.noData=No data to display"})
353  private void updateTimingMetricGraphs() throws HealthMonitorException {
354 
355  // Clear out any old graphs
356  timingGraphPanel.removeAll();
357 
358  if(timingData.keySet().isEmpty()) {
359  // There are no timing metrics in the database
360  timingGraphPanel.add(new JLabel(Bundle.HealthMonitorDashboard_updateTimingMetricGraphs_noData()));
361  return;
362  }
363 
364  for(String metricName:timingData.keySet()) {
365 
366  // If necessary, trim down the list of results to fit the selected time range
367  List<HealthMonitor.DatabaseTimingResult> intermediateTimingDataForDisplay;
368  if(timingDateComboBox.getSelectedItem() != null) {
369  DateRange selectedDateRange = DateRange.fromLabel(timingDateComboBox.getSelectedItem().toString());
370  long threshold = System.currentTimeMillis() - selectedDateRange.getTimestampRange();
371  intermediateTimingDataForDisplay = timingData.get(metricName).stream()
372  .filter(t -> t.getTimestamp() > threshold)
373  .collect(Collectors.toList());
374  } else {
375  intermediateTimingDataForDisplay = timingData.get(metricName);
376  }
377 
378  // Get the name of the selected host, if there is one.
379  // The graph always uses the data from all hosts to generate the x and y scales
380  // so we don't filter anything out here.
381  String hostToDisplay = null;
382  if(timingHostCheckBox.isSelected() && (timingHostComboBox.getSelectedItem() != null)) {
383  hostToDisplay = timingHostComboBox.getSelectedItem().toString();
384  }
385 
386  // Generate the graph
387  TimingMetricGraphPanel singleTimingGraphPanel = new TimingMetricGraphPanel(intermediateTimingDataForDisplay,
388  hostToDisplay, true, metricName, timingSkipOutliersCheckBox.isSelected(), timingShowTrendLineCheckBox.isSelected());
389  singleTimingGraphPanel.setPreferredSize(new Dimension(700,200));
390 
391  timingGraphPanel.add(singleTimingGraphPanel);
392  }
393  timingGraphPanel.revalidate();
394  timingGraphPanel.repaint();
395  }
396 
402  @NbBundle.Messages({"HealthMonitorDashboard.createUserPanel.noData=No data to display - monitor is not enabled",
403  "HealthMonitorDashboard.createUserPanel.userMetricsTitle=User Metrics"})
404  private JPanel createUserPanel() throws HealthMonitorException {
405  // If the monitor isn't enabled, just add a message
406  if(! HealthMonitor.monitorIsEnabled()) {
407  JPanel emptyUserMetricPanel = new JPanel();
408  emptyUserMetricPanel.add(new JLabel(Bundle.HealthMonitorDashboard_createUserPanel_userMetricsTitle()));
409  emptyUserMetricPanel.add(new JLabel(" "));
410  emptyUserMetricPanel.add(new JLabel(Bundle.HealthMonitorDashboard_createUserPanel_noData()));
411 
412  return emptyUserMetricPanel;
413  }
414 
415  JPanel userMetricPanel = new JPanel();
416  userMetricPanel.setLayout(new BoxLayout(userMetricPanel, BoxLayout.PAGE_AXIS));
417  userMetricPanel.setBorder(BorderFactory.createEtchedBorder());
418 
419  // Add title
420  JLabel userMetricTitle = new JLabel(Bundle.HealthMonitorDashboard_createUserPanel_userMetricsTitle());
421  userMetricPanel.add(userMetricTitle);
422  userMetricPanel.add(new JSeparator());
423 
424  // Add the controls
425  userMetricPanel.add(createUserControlPanel());
426  userMetricPanel.add(new JSeparator());
427 
428  // Create panel to hold graphs
429  userGraphPanel = new JPanel();
430  userGraphPanel.setLayout(new GridLayout(0,2));
431 
432  // Update the graph panel, put it in a scroll pane, and add to the timing metric panel
434  JScrollPane scrollPane = new JScrollPane(userGraphPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
435  userMetricPanel.add(scrollPane);
436  userMetricPanel.revalidate();
437  userMetricPanel.repaint();
438 
439  return userMetricPanel;
440  }
441 
446  @NbBundle.Messages({"HealthMonitorDashboard.createUserControlPanel.maxDays=Max days to display"})
447  private JPanel createUserControlPanel() {
448  JPanel userControlPanel = new JPanel();
449 
450  // If the monitor is not enabled, don't add any components
451  if(! HealthMonitor.monitorIsEnabled()) {
452  return userControlPanel;
453  }
454 
455  // Create the combo box for selecting how much data to display
456  String[] dateOptionStrings = Arrays.stream(DateRange.values()).map(e -> e.getLabel()).toArray(String[]::new);
457  userDateComboBox = new JComboBox<>(dateOptionStrings);
458  userDateComboBox.setSelectedItem(DateRange.ONE_DAY.getLabel());
459 
460  // Set up the listener on the date combo box
461  userDateComboBox.addActionListener(new ActionListener() {
462  @Override
463  public void actionPerformed(ActionEvent arg0) {
464  try {
466  } catch (HealthMonitorException ex) {
467  logger.log(Level.SEVERE, "Error updating user metric panel", ex);
468  }
469  }
470  });
471 
472  // Add the date range combo box and label to the panel
473  userControlPanel.add(new JLabel(Bundle.HealthMonitorDashboard_createUserControlPanel_maxDays()));
474  userControlPanel.add(userDateComboBox);
475 
476  return userControlPanel;
477  }
478 
483  @NbBundle.Messages({"HealthMonitorDashboard.updateUserMetricGraphs.noData=No data to display"})
484  private void updateUserMetricGraphs() throws HealthMonitorException {
485 
486  // Clear out any old graphs
487  userGraphPanel.removeAll();
488 
489  if(userData.isEmpty()) {
490  // There are no user metrics in the database
491  userGraphPanel.add(new JLabel(Bundle.HealthMonitorDashboard_updateUserMetricGraphs_noData()));
492  return;
493  }
494 
495  // Calculate the minimum timestamp for the graph.
496  // Unlike the timing graphs, we do not filter the list of user metrics here.
497  // This is because even if we're only displaying one day, the
498  // last metric for a host may be that it logged on two days ago, so we would want
499  // to show that node as logged on.
500  long timestampThreshold;
501  if(userDateComboBox.getSelectedItem() != null) {
502  DateRange selectedDateRange = DateRange.fromLabel(userDateComboBox.getSelectedItem().toString());
503  timestampThreshold = System.currentTimeMillis() - selectedDateRange.getTimestampRange();
504 
505  } else {
506  timestampThreshold = System.currentTimeMillis() - DateRange.getMaximumTimestampRange();
507  }
508 
509  // Generate the graphs
510  UserMetricGraphPanel caseGraphPanel = new UserMetricGraphPanel(userData, timestampThreshold, true);
511  caseGraphPanel.setPreferredSize(new Dimension(700,200));
512 
513  UserMetricGraphPanel logonGraphPanel = new UserMetricGraphPanel(userData, timestampThreshold, false);
514  logonGraphPanel.setPreferredSize(new Dimension(700,200));
515 
516  userGraphPanel.add(caseGraphPanel);
517  userGraphPanel.add(logonGraphPanel);
518  userGraphPanel.revalidate();
519  userGraphPanel.repaint();
520  }
521 
527  @NbBundle.Messages({"HealthMonitorDashboard.createAdminPanel.enableButton=Enable monitor",
528  "HealthMonitorDashboard.createAdminPanel.disableButton=Disable monitor"})
529  private JPanel createAdminPanel() {
530 
531  JPanel adminPanel = new JPanel();
532  adminPanel.setBorder(BorderFactory.createEtchedBorder());
533 
534  // Create the buttons for enabling/disabling the monitor
535  JButton enableButton = new JButton(Bundle.HealthMonitorDashboard_createAdminPanel_enableButton());
536  JButton disableButton = new JButton(Bundle.HealthMonitorDashboard_createAdminPanel_disableButton());
537 
538  boolean isEnabled = HealthMonitor.monitorIsEnabled();
539  enableButton.setEnabled(! isEnabled);
540  disableButton.setEnabled(isEnabled);
541 
542  // Set up a listener on the enable button
543  enableButton.addActionListener(new ActionListener() {
544  @Override
545  public void actionPerformed(ActionEvent arg0) {
546  try {
547  dialog.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
548  HealthMonitor.setEnabled(true);
549  redisplay();
550  } catch (HealthMonitorException ex) {
551  logger.log(Level.SEVERE, "Error enabling monitoring", ex);
552  } finally {
553  dialog.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
554  }
555  }
556  });
557 
558  // Set up a listener on the disable button
559  disableButton.addActionListener(new ActionListener() {
560  @Override
561  public void actionPerformed(ActionEvent arg0) {
562  try {
563  dialog.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
564  HealthMonitor.setEnabled(false);
565  redisplay();
566  } catch (HealthMonitorException ex) {
567  logger.log(Level.SEVERE, "Error disabling monitoring", ex);
568  } finally {
569  dialog.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
570  }
571  }
572  });
573 
574  // Add the buttons
575  adminPanel.add(enableButton);
576  adminPanel.add(Box.createHorizontalStrut(25));
577  adminPanel.add(disableButton);
578 
579  return adminPanel;
580  }
581 
585  @NbBundle.Messages({"HealthMonitorDashboard.DateRange.oneMonth=One month",
586  "HealthMonitorDashboard.DateRange.twoWeeks=Two weeks",
587  "HealthMonitorDashboard.DateRange.oneWeek=One week",
588  "HealthMonitorDashboard.DateRange.oneDay=One day"})
589  private enum DateRange {
590  ONE_DAY(Bundle.HealthMonitorDashboard_DateRange_oneDay(), 1),
591  ONE_WEEK(Bundle.HealthMonitorDashboard_DateRange_oneWeek(), 7),
592  TWO_WEEKS(Bundle.HealthMonitorDashboard_DateRange_twoWeeks(), 14),
593  ONE_MONTH(Bundle.HealthMonitorDashboard_DateRange_oneMonth(), 31);
594 
595  private final String label;
596  private final long numberOfDays;
597  private static final long MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;
598 
599  DateRange(String label, long numberOfDays) {
600  this.label = label;
601  this.numberOfDays = numberOfDays;
602  }
603 
608  String getLabel() {
609  return label;
610  }
611 
619  if (numberOfDays > 0) {
620  return numberOfDays * MILLISECONDS_PER_DAY;
621  } else {
622  return Long.MAX_VALUE;
623  }
624  }
625 
631  static long getMaximumTimestampRange() {
632  long maxRange = Long.MIN_VALUE;
633  for (DateRange dateRange : DateRange.values()) {
634  if (dateRange.getTimestampRange() > maxRange) {
635  maxRange = dateRange.getTimestampRange();
636  }
637  }
638  return maxRange;
639  }
640 
641  static DateRange fromLabel(String text) {
642  for (DateRange dateRange : DateRange.values()) {
643  if (dateRange.label.equalsIgnoreCase(text)) {
644  return dateRange;
645  }
646  }
647  return ONE_DAY; // If the comparison failed, return a default
648  }
649  }
650 
651 }
synchronized static Logger getLogger(String name)
Definition: Logger.java:124

Copyright © 2012-2020 Basis Technology. Generated on: Wed Apr 8 2020
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.