Autopsy  4.19.1
Graphical digital forensics platform for The Sleuth Kit and other tools.
KeywordSearchGlobalSearchSettingsPanel.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2012-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.keywordsearch;
20 
21 import java.awt.EventQueue;
22 import java.beans.PropertyChangeEvent;
23 import java.beans.PropertyChangeListener;
24 import java.util.logging.Level;
25 import org.netbeans.spi.options.OptionsPanelController;
26 import org.openide.util.NbBundle;
32 
36 @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives
37 class KeywordSearchGlobalSearchSettingsPanel extends javax.swing.JPanel implements OptionsPanel {
38 
39  private static final long serialVersionUID = 1L;
40  private final Logger logger = Logger.getLogger(KeywordSearchGlobalSearchSettingsPanel.class.getName());
41 
45  KeywordSearchGlobalSearchSettingsPanel() {
46  initComponents();
47  customizeComponents();
48  }
49 
50  private void activateWidgets() {
51  skipNSRLCheckBox.setSelected(KeywordSearchSettings.getSkipKnown());
52  showSnippetsCB.setSelected(KeywordSearchSettings.getShowSnippets());
53  boolean ingestRunning = IngestManager.getInstance().isIngestRunning();
54  ingestWarningLabel.setVisible(ingestRunning);
55  skipNSRLCheckBox.setEnabled(!ingestRunning);
56  setTimeSettingEnabled(!ingestRunning);
57 
58  final UpdateFrequency curFreq = KeywordSearchSettings.getUpdateFrequency();
59  switch (curFreq) {
60  case FAST:
61  timeRadioButton1.setSelected(true);
62  break;
63  case AVG:
64  timeRadioButton2.setSelected(true);
65  break;
66  case SLOW:
67  timeRadioButton3.setSelected(true);
68  break;
69  case SLOWEST:
70  timeRadioButton4.setSelected(true);
71  break;
72  case NONE:
73  timeRadioButton5.setSelected(true);
74  break;
75  case DEFAULT:
76  default:
77  // default value
78  timeRadioButton3.setSelected(true);
79  break;
80  }
81  }
82 
88  @SuppressWarnings("unchecked")
89  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
90  private void initComponents() {
91 
92  timeGroup = new javax.swing.ButtonGroup();
93  skipNSRLCheckBox = new javax.swing.JCheckBox();
94  filesIndexedLabel = new javax.swing.JLabel();
95  filesIndexedValue = new javax.swing.JLabel();
96  chunksLabel = new javax.swing.JLabel();
97  chunksValLabel = new javax.swing.JLabel();
98  settingsLabel = new javax.swing.JLabel();
99  informationLabel = new javax.swing.JLabel();
100  settingsSeparator = new javax.swing.JSeparator();
101  informationSeparator = new javax.swing.JSeparator();
102  frequencyLabel = new javax.swing.JLabel();
103  timeRadioButton1 = new javax.swing.JRadioButton();
104  timeRadioButton2 = new javax.swing.JRadioButton();
105  timeRadioButton3 = new javax.swing.JRadioButton();
106  timeRadioButton4 = new javax.swing.JRadioButton();
107  showSnippetsCB = new javax.swing.JCheckBox();
108  timeRadioButton5 = new javax.swing.JRadioButton();
109  ingestWarningLabel = new javax.swing.JLabel();
110 
111  skipNSRLCheckBox.setText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.skipNSRLCheckBox.text")); // NOI18N
112  skipNSRLCheckBox.setToolTipText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.skipNSRLCheckBox.toolTipText")); // NOI18N
113  skipNSRLCheckBox.addActionListener(new java.awt.event.ActionListener() {
114  public void actionPerformed(java.awt.event.ActionEvent evt) {
115  skipNSRLCheckBoxActionPerformed(evt);
116  }
117  });
118 
119  filesIndexedLabel.setText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.filesIndexedLabel.text")); // NOI18N
120 
121  filesIndexedValue.setText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.filesIndexedValue.text")); // NOI18N
122 
123  chunksLabel.setText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.chunksLabel.text")); // NOI18N
124 
125  chunksValLabel.setText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.chunksValLabel.text")); // NOI18N
126 
127  settingsLabel.setText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.settingsLabel.text")); // NOI18N
128 
129  informationLabel.setText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.informationLabel.text")); // NOI18N
130 
131  frequencyLabel.setText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.frequencyLabel.text")); // NOI18N
132 
133  timeRadioButton1.setText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.timeRadioButton1.text")); // NOI18N
134  timeRadioButton1.setToolTipText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.timeRadioButton1.toolTipText")); // NOI18N
135  timeRadioButton1.addActionListener(new java.awt.event.ActionListener() {
136  public void actionPerformed(java.awt.event.ActionEvent evt) {
137  timeRadioButton1ActionPerformed(evt);
138  }
139  });
140 
141  timeRadioButton2.setText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.timeRadioButton2.text")); // NOI18N
142  timeRadioButton2.setToolTipText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.timeRadioButton2.toolTipText")); // NOI18N
143  timeRadioButton2.addActionListener(new java.awt.event.ActionListener() {
144  public void actionPerformed(java.awt.event.ActionEvent evt) {
145  timeRadioButton2ActionPerformed(evt);
146  }
147  });
148 
149  timeRadioButton3.setText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.timeRadioButton3.text")); // NOI18N
150  timeRadioButton3.setToolTipText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.timeRadioButton3.toolTipText")); // NOI18N
151  timeRadioButton3.addActionListener(new java.awt.event.ActionListener() {
152  public void actionPerformed(java.awt.event.ActionEvent evt) {
153  timeRadioButton3ActionPerformed(evt);
154  }
155  });
156 
157  timeRadioButton4.setText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.timeRadioButton4.text_1")); // NOI18N
158  timeRadioButton4.setToolTipText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.timeRadioButton4.toolTipText")); // NOI18N
159  timeRadioButton4.addActionListener(new java.awt.event.ActionListener() {
160  public void actionPerformed(java.awt.event.ActionEvent evt) {
161  timeRadioButton4ActionPerformed(evt);
162  }
163  });
164 
165  showSnippetsCB.setText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.showSnippetsCB.text")); // NOI18N
166  showSnippetsCB.addActionListener(new java.awt.event.ActionListener() {
167  public void actionPerformed(java.awt.event.ActionEvent evt) {
168  showSnippetsCBActionPerformed(evt);
169  }
170  });
171 
172  timeRadioButton5.setText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.timeRadioButton5.text")); // NOI18N
173  timeRadioButton5.setToolTipText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.timeRadioButton5.toolTipText")); // NOI18N
174  timeRadioButton5.addActionListener(new java.awt.event.ActionListener() {
175  public void actionPerformed(java.awt.event.ActionEvent evt) {
176  timeRadioButton5ActionPerformed(evt);
177  }
178  });
179 
180  ingestWarningLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/modules/hashdatabase/warning16.png"))); // NOI18N
181  ingestWarningLabel.setText(org.openide.util.NbBundle.getMessage(KeywordSearchGlobalSearchSettingsPanel.class, "KeywordSearchGlobalSearchSettingsPanel.ingestWarningLabel.text")); // NOI18N
182 
183  javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
184  this.setLayout(layout);
185  layout.setHorizontalGroup(
186  layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
187  .addGroup(layout.createSequentialGroup()
188  .addContainerGap()
189  .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
190  .addGroup(layout.createSequentialGroup()
191  .addGap(16, 16, 16)
192  .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
193  .addComponent(skipNSRLCheckBox)
194  .addComponent(showSnippetsCB))
195  .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
196  .addGroup(layout.createSequentialGroup()
197  .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
198  .addComponent(ingestWarningLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
199  .addGroup(layout.createSequentialGroup()
200  .addComponent(informationLabel)
201  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
202  .addComponent(informationSeparator, javax.swing.GroupLayout.PREFERRED_SIZE, 309, javax.swing.GroupLayout.PREFERRED_SIZE))
203  .addGroup(layout.createSequentialGroup()
204  .addGap(16, 16, 16)
205  .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
206  .addGroup(layout.createSequentialGroup()
207  .addComponent(filesIndexedLabel)
208  .addGap(18, 18, 18)
209  .addComponent(filesIndexedValue))
210  .addComponent(frequencyLabel)
211  .addGroup(layout.createSequentialGroup()
212  .addComponent(chunksLabel)
213  .addGap(18, 18, 18)
214  .addComponent(chunksValLabel))
215  .addGroup(layout.createSequentialGroup()
216  .addGap(16, 16, 16)
217  .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
218  .addComponent(timeRadioButton2)
219  .addComponent(timeRadioButton1)
220  .addComponent(timeRadioButton3)
221  .addComponent(timeRadioButton4)
222  .addComponent(timeRadioButton5))))))
223  .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
224  .addGroup(layout.createSequentialGroup()
225  .addComponent(settingsLabel)
226  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
227  .addComponent(settingsSeparator, javax.swing.GroupLayout.PREFERRED_SIZE, 326, javax.swing.GroupLayout.PREFERRED_SIZE)
228  .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))))
229  );
230 
231  layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {chunksLabel, filesIndexedLabel});
232 
233  layout.setVerticalGroup(
234  layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
235  .addGroup(layout.createSequentialGroup()
236  .addContainerGap()
237  .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
238  .addComponent(settingsLabel)
239  .addComponent(settingsSeparator, javax.swing.GroupLayout.PREFERRED_SIZE, 6, javax.swing.GroupLayout.PREFERRED_SIZE))
240  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
241  .addComponent(skipNSRLCheckBox)
242  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
243  .addComponent(showSnippetsCB)
244  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
245  .addComponent(frequencyLabel)
246  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
247  .addComponent(timeRadioButton1)
248  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
249  .addComponent(timeRadioButton2)
250  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
251  .addComponent(timeRadioButton3)
252  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
253  .addComponent(timeRadioButton4)
254  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
255  .addComponent(timeRadioButton5)
256  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
257  .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
258  .addComponent(informationLabel)
259  .addComponent(informationSeparator, javax.swing.GroupLayout.PREFERRED_SIZE, 7, javax.swing.GroupLayout.PREFERRED_SIZE))
260  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
261  .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
262  .addComponent(filesIndexedLabel)
263  .addComponent(filesIndexedValue))
264  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
265  .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
266  .addComponent(chunksLabel)
267  .addComponent(chunksValLabel))
268  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
269  .addComponent(ingestWarningLabel)
270  .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
271  );
272  }// </editor-fold>//GEN-END:initComponents
273 
274  private void timeRadioButton5ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_timeRadioButton5ActionPerformed
275  firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null);
276  }//GEN-LAST:event_timeRadioButton5ActionPerformed
277 
278  private void skipNSRLCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_skipNSRLCheckBoxActionPerformed
279  firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null);
280  }//GEN-LAST:event_skipNSRLCheckBoxActionPerformed
281 
282  private void showSnippetsCBActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_showSnippetsCBActionPerformed
283  firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null);
284  }//GEN-LAST:event_showSnippetsCBActionPerformed
285 
286  private void timeRadioButton1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_timeRadioButton1ActionPerformed
287  firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null);
288  }//GEN-LAST:event_timeRadioButton1ActionPerformed
289 
290  private void timeRadioButton2ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_timeRadioButton2ActionPerformed
291  firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null);
292  }//GEN-LAST:event_timeRadioButton2ActionPerformed
293 
294  private void timeRadioButton3ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_timeRadioButton3ActionPerformed
295  firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null);
296  }//GEN-LAST:event_timeRadioButton3ActionPerformed
297 
298  private void timeRadioButton4ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_timeRadioButton4ActionPerformed
299  firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null);
300  }//GEN-LAST:event_timeRadioButton4ActionPerformed
301 
302  // Variables declaration - do not modify//GEN-BEGIN:variables
303  private javax.swing.JLabel chunksLabel;
304  private javax.swing.JLabel chunksValLabel;
305  private javax.swing.JLabel filesIndexedLabel;
306  private javax.swing.JLabel filesIndexedValue;
307  private javax.swing.JLabel frequencyLabel;
308  private javax.swing.JLabel informationLabel;
309  private javax.swing.JSeparator informationSeparator;
310  private javax.swing.JLabel ingestWarningLabel;
311  private javax.swing.JLabel settingsLabel;
312  private javax.swing.JSeparator settingsSeparator;
313  private javax.swing.JCheckBox showSnippetsCB;
314  private javax.swing.JCheckBox skipNSRLCheckBox;
315  private javax.swing.ButtonGroup timeGroup;
316  private javax.swing.JRadioButton timeRadioButton1;
317  private javax.swing.JRadioButton timeRadioButton2;
318  private javax.swing.JRadioButton timeRadioButton3;
319  private javax.swing.JRadioButton timeRadioButton4;
320  private javax.swing.JRadioButton timeRadioButton5;
321  // End of variables declaration//GEN-END:variables
322 
323  @Override
324  public void store() {
325  KeywordSearchSettings.setSkipKnown(skipNSRLCheckBox.isSelected());
326  KeywordSearchSettings.setUpdateFrequency(getSelectedTimeValue());
327  KeywordSearchSettings.setShowSnippets(showSnippetsCB.isSelected());
328  }
329 
330  @Override
331  public void load() {
332  activateWidgets();
333  }
334 
335  private void setTimeSettingEnabled(boolean enabled) {
336  timeRadioButton1.setEnabled(enabled);
337  timeRadioButton2.setEnabled(enabled);
338  timeRadioButton3.setEnabled(enabled);
339  timeRadioButton4.setEnabled(enabled);
340  timeRadioButton5.setEnabled(enabled);
341  frequencyLabel.setEnabled(enabled);
342  }
343 
344  private UpdateFrequency getSelectedTimeValue() {
345  if (timeRadioButton1.isSelected()) {
346  return UpdateFrequency.FAST;
347  } else if (timeRadioButton2.isSelected()) {
348  return UpdateFrequency.AVG;
349  } else if (timeRadioButton3.isSelected()) {
350  return UpdateFrequency.SLOW;
351  } else if (timeRadioButton4.isSelected()) {
352  return UpdateFrequency.SLOWEST;
353  } else if (timeRadioButton5.isSelected()) {
354  return UpdateFrequency.NONE;
355  }
356  return UpdateFrequency.DEFAULT;
357  }
358 
359  @NbBundle.Messages({"KeywordSearchGlobalSearchSettingsPanel.customizeComponents.windowsOCR=Enable Optical Character Recognition (OCR) (Requires Windows 64-bit)",
360  "KeywordSearchGlobalSearchSettingsPanel.customizeComponents.windowsLimitedOCR=Only process images which are over 100KB in size or extracted from a document. (Beta) (Requires Windows 64-bit)"})
361  private void customizeComponents() {
362 
363  timeGroup.add(timeRadioButton1);
364  timeGroup.add(timeRadioButton2);
365  timeGroup.add(timeRadioButton3);
366  timeGroup.add(timeRadioButton4);
367  timeGroup.add(timeRadioButton5);
368 
369  this.skipNSRLCheckBox.setSelected(KeywordSearchSettings.getSkipKnown());
370 
371  try {
372  filesIndexedValue.setText(Integer.toString(KeywordSearch.getServer().queryNumIndexedFiles()));
373  chunksValLabel.setText(Integer.toString(KeywordSearch.getServer().queryNumIndexedChunks()));
374  } catch (KeywordSearchModuleException | NoOpenCoreException ex) {
375  logger.log(Level.WARNING, "Could not get number of indexed files/chunks"); //NON-NLS
376  }
377 
378  KeywordSearch.addNumIndexedFilesChangeListener(
379  new PropertyChangeListener() {
380  @Override
381  public void propertyChange(PropertyChangeEvent evt) {
382  String changed = evt.getPropertyName();
383  Object newValue = evt.getNewValue();
384 
385  if (changed.equals(KeywordSearch.NUM_FILES_CHANGE_EVT)) {
386  int newFilesIndexed = ((Integer) newValue);
387  filesIndexedValue.setText(Integer.toString(newFilesIndexed));
388  try {
389  chunksValLabel.setText(Integer.toString(KeywordSearch.getServer().queryNumIndexedChunks()));
390  } catch (KeywordSearchModuleException | NoOpenCoreException ex) {
391  logger.log(Level.WARNING, "Could not get number of indexed chunks"); //NON-NLS
392 
393  }
394  }
395  }
396  });
397 
398  //allow panel to toggle its enabled status while it is open based on ingest events
399  IngestManager.getInstance().addIngestJobEventListener(new PropertyChangeListener() {
400  @Override
401  public void propertyChange(PropertyChangeEvent evt) {
402  Object source = evt.getSource();
403  if (source instanceof String && ((String) source).equals("LOCAL")) { //NON-NLS
404  EventQueue.invokeLater(() -> {
405  activateWidgets();
406  });
407  }
408  }
409  });
410  }
411 }

Copyright © 2012-2021 Basis Technology. Generated on: Thu Sep 30 2021
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.