Autopsy  4.21.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
Server.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2011-2021 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 com.google.common.util.concurrent.ThreadFactoryBuilder;
22 import java.awt.event.ActionEvent;
23 import java.beans.PropertyChangeListener;
24 import java.io.BufferedReader;
25 import java.io.BufferedWriter;
26 import java.io.File;
27 import java.io.FileOutputStream;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.InputStreamReader;
31 import java.io.OutputStream;
32 import java.io.OutputStreamWriter;
33 import java.net.ConnectException;
34 import java.net.DatagramSocket;
35 import java.net.ServerSocket;
36 import java.net.SocketException;
37 import java.nio.charset.Charset;
38 import java.nio.file.Files;
39 import java.nio.file.OpenOption;
40 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
41 import java.nio.file.Path;
42 import java.nio.file.Paths;
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.Collections;
46 import java.util.Iterator;
47 import java.util.List;
48 import java.util.Random;
49 import java.util.concurrent.ScheduledThreadPoolExecutor;
50 import java.util.concurrent.TimeUnit;
51 import java.util.concurrent.locks.ReentrantReadWriteLock;
52 import java.util.logging.Level;
53 import javax.swing.AbstractAction;
54 import org.apache.commons.io.FileUtils;
55 import java.util.concurrent.TimeoutException;
56 import java.util.concurrent.atomic.AtomicBoolean;
57 import java.util.concurrent.atomic.AtomicInteger;
58 import java.util.stream.Collectors;
59 import static java.util.stream.Collectors.toList;
60 import javax.swing.JOptionPane;
61 import org.apache.solr.client.solrj.SolrQuery;
62 import org.apache.solr.client.solrj.SolrRequest;
63 import org.apache.solr.client.solrj.SolrServerException;
64 import org.apache.solr.client.solrj.SolrClient;
65 import org.apache.solr.client.solrj.impl.HttpSolrClient;
66 import org.apache.solr.client.solrj.impl.CloudSolrClient;
67 import org.apache.solr.client.solrj.impl.ConcurrentUpdateSolrClient;
68 import org.apache.solr.client.solrj.impl.XMLResponseParser;
69 import org.apache.solr.client.solrj.request.CollectionAdminRequest;
70 import org.apache.solr.client.solrj.response.CollectionAdminResponse;
71 import org.apache.solr.client.solrj.request.CoreAdminRequest;
72 import org.apache.solr.client.solrj.response.CoreAdminResponse;
73 import org.apache.solr.client.solrj.impl.BaseHttpSolrClient.RemoteSolrException;
74 import org.apache.solr.client.solrj.request.QueryRequest;
75 import org.apache.solr.client.solrj.response.QueryResponse;
76 import org.apache.solr.client.solrj.response.TermsResponse;
77 import org.apache.solr.client.solrj.response.TermsResponse.Term;
78 import org.apache.solr.common.SolrDocument;
79 import org.apache.solr.common.SolrDocumentList;
80 import org.apache.solr.common.SolrException;
81 import org.apache.solr.common.SolrInputDocument;
82 import org.apache.solr.common.util.NamedList;
83 import org.openide.modules.InstalledFileLocator;
84 import org.openide.modules.Places;
85 import org.openide.util.NbBundle;
86 import org.openide.util.NbBundle.Messages;
87 import org.openide.windows.WindowManager;
105 
110 public class Server {
111 
115  public static enum Schema {
116 
117  ID {
118  @Override
119  public String toString() {
120  return "id"; //NON-NLS
121  }
122  },
123  IMAGE_ID {
124  @Override
125  public String toString() {
126  return "image_id"; //NON-NLS
127  }
128  },
129  // This is not stored or indexed. it is copied to text by the schema
130  CONTENT {
131  @Override
132  public String toString() {
133  return "content"; //NON-NLS
134  }
135  },
136  // String representation for regular expression searching
137  CONTENT_STR {
138  @Override
139  public String toString() {
140  return "content_str"; //NON-NLS
141  }
142  },
143  // default search field. Populated by schema
144  TEXT {
145  @Override
146  public String toString() {
147  return "text"; //NON-NLS
148  }
149  },
150  // no longer populated. Was used for regular expression searching.
151  // Should not be used.
152  CONTENT_WS {
153  @Override
154  public String toString() {
155  return "content_ws"; //NON-NLS
156  }
157  },
158  CONTENT_JA {
159  @Override
160  public String toString() {
161  return "content_ja"; //NON-NLS
162  }
163  },
164  LANGUAGE {
165  @Override
166  public String toString() {
167  return "language"; //NON-NLS
168  }
169  },
170  FILE_NAME {
171  @Override
172  public String toString() {
173  return "file_name"; //NON-NLS
174  }
175  },
176  // note that we no longer store or index this field
177  CTIME {
178  @Override
179  public String toString() {
180  return "ctime"; //NON-NLS
181  }
182  },
183  // note that we no longer store or index this field
184  ATIME {
185  @Override
186  public String toString() {
187  return "atime"; //NON-NLS
188  }
189  },
190  // note that we no longer store or index this field
191  MTIME {
192  @Override
193  public String toString() {
194  return "mtime"; //NON-NLS
195  }
196  },
197  // note that we no longer store or index this field
198  CRTIME {
199  @Override
200  public String toString() {
201  return "crtime"; //NON-NLS
202  }
203  },
204  NUM_CHUNKS {
205  @Override
206  public String toString() {
207  return "num_chunks"; //NON-NLS
208  }
209  },
210  CHUNK_SIZE {
211  @Override
212  public String toString() {
213  return "chunk_size"; //NON-NLS
214  }
215  },
221  TERMFREQ {
222  @Override
223  public String toString() {
224  return "termfreq"; //NON-NLS
225  }
226  }
227  };
228 
229  public static final String HL_ANALYZE_CHARS_UNLIMITED = "500000"; //max 1MB in a chunk. use -1 for unlimited, but -1 option may not be supported (not documented)
230  //max content size we can send to Solr
231  public static final long MAX_CONTENT_SIZE = 1L * 31 * 1024 * 1024;
232  private static final Logger logger = Logger.getLogger(Server.class.getName());
233  public static final String CORE_EVT = "CORE_EVT"; //NON-NLS
234  @Deprecated
235  public static final char ID_CHUNK_SEP = '_';
236  public static final String CHUNK_ID_SEPARATOR = "_";
237  private String javaPath = "java";
238  public static final Charset DEFAULT_INDEXED_TEXT_CHARSET = Charset.forName("UTF-8");
239  private Process curSolrProcess = null;
240  static final String PROPERTIES_FILE = KeywordSearchSettings.MODULE_NAME;
241  static final String PROPERTIES_CURRENT_SERVER_PORT = "IndexingServerPort"; //NON-NLS
242  static final String PROPERTIES_CURRENT_STOP_PORT = "IndexingServerStopPort"; //NON-NLS
243  private static final String KEY = "jjk#09s"; //NON-NLS
244  static final String DEFAULT_SOLR_SERVER_HOST = "localhost"; //NON-NLS
245  static final int DEFAULT_SOLR_SERVER_PORT = 23232;
246  static final int DEFAULT_SOLR_STOP_PORT = 34343;
247  private int localSolrServerPort = 0;
248  private int localSolrStopPort = 0;
250  private static final String SOLR = "solr";
251  private static final String CORE_PROPERTIES = "core.properties";
252  private static final boolean DEBUG = false;//(Version.getBuildType() == Version.Type.DEVELOPMENT);
253  private static final int NUM_COLLECTION_CREATION_RETRIES = 5;
254  private static final int NUM_EMBEDDED_SERVER_RETRIES = 12; // attempt to connect to embedded Solr server for 1 minute
255  private static final int EMBEDDED_SERVER_RETRY_WAIT_SEC = 5;
256 
257  public enum CORE_EVT_STATES {
258 
259  STOPPED, STARTED
260  };
261 
262  private enum SOLR_VERSION {
263 
264  SOLR8, SOLR4
265  };
266 
267  // A reference to the locally running Solr instance.
268  private HttpSolrClient localSolrServer = null;
269  private SOLR_VERSION localServerVersion = SOLR_VERSION.SOLR8; // start local Solr 8 by default
270 
271  // A reference to the remote/network running Solr instance.
272  private HttpSolrClient remoteSolrServer;
273 
274  private Collection currentCollection;
275  private final ReentrantReadWriteLock currentCoreLock;
276 
277  private final ServerAction serverAction;
279 
284  Server() {
285  initSettings();
286 
287  localSolrServer = getSolrClient("http://localhost:" + localSolrServerPort + "/solr");
288 
289  serverAction = new ServerAction();
290  File solr8Folder = InstalledFileLocator.getDefault().locate("solr", Server.class.getPackage().getName(), false); //NON-NLS
291  File solr4Folder = InstalledFileLocator.getDefault().locate("solr4", Server.class.getPackage().getName(), false); //NON-NLS
292 
293  // Figure out where Java is located. The Java home location
294  // will be passed as the SOLR_JAVA_HOME environment
295  // variable to the Solr script but it can be overridden by the user in
296  // either autopsy-solr.cmd or autopsy-solr-in.cmd.
297  javaPath = PlatformUtil.getJavaPath();
298 
299  Path solr8Home = Paths.get(PlatformUtil.getUserDirectory().getAbsolutePath(), "solr"); //NON-NLS
300  try {
301  // Always copy the config files, as they may have changed. Otherwise potentially stale Solr configuration is being used.
302  if (!solr8Home.toFile().exists()) {
303  Files.createDirectory(solr8Home);
304  } else {
305  // delete the configsets directory as the Autopsy configset could have changed
306  FileUtil.deleteDir(solr8Home.resolve("configsets").toFile());
307  }
308  Files.copy(Paths.get(solr8Folder.getAbsolutePath(), "server", "solr", "solr.xml"), solr8Home.resolve("solr.xml"), REPLACE_EXISTING); //NON-NLS
309  Files.copy(Paths.get(solr8Folder.getAbsolutePath(), "server", "solr", "zoo.cfg"), solr8Home.resolve("zoo.cfg"), REPLACE_EXISTING); //NON-NLS
310  FileUtils.copyDirectory(Paths.get(solr8Folder.getAbsolutePath(), "server", "solr", "configsets").toFile(), solr8Home.resolve("configsets").toFile()); //NON-NLS
311  } catch (IOException ex) {
312  logger.log(Level.SEVERE, "Failed to create Solr 8 home folder:", ex); //NON-NLS
313  }
314 
315  Path solr4Home = Paths.get(PlatformUtil.getUserDirectory().getAbsolutePath(), "solr4"); //NON-NLS
316  try {
317  // Always copy the config files, as they may have changed. Otherwise potentially stale Solr configuration is being used.
318  if (!solr4Home.toFile().exists()) {
319  Files.createDirectory(solr4Home);
320  }
321  Files.copy(Paths.get(solr4Folder.getAbsolutePath(), "solr", "solr.xml"), solr4Home.resolve("solr.xml"), REPLACE_EXISTING); //NON-NLS
322  Files.copy(Paths.get(solr4Folder.getAbsolutePath(), "solr", "zoo.cfg"), solr4Home.resolve("zoo.cfg"), REPLACE_EXISTING); //NON-NLS
323  } catch (IOException ex) {
324  logger.log(Level.SEVERE, "Failed to create Solr 4 home folder:", ex); //NON-NLS
325  }
326 
327  currentCoreLock = new ReentrantReadWriteLock(true);
328 
329  logger.log(Level.INFO, "Created Server instance using Java at {0}", javaPath); //NON-NLS
330  }
331 
332  private void initSettings() {
333 
334  if (ModuleSettings.settingExists(PROPERTIES_FILE, PROPERTIES_CURRENT_SERVER_PORT)) {
335  try {
336  localSolrServerPort = Integer.decode(ModuleSettings.getConfigSetting(PROPERTIES_FILE, PROPERTIES_CURRENT_SERVER_PORT));
337  } catch (NumberFormatException nfe) {
338  logger.log(Level.WARNING, "Could not decode indexing server port, value was not a valid port number, using the default. ", nfe); //NON-NLS
339  localSolrServerPort = DEFAULT_SOLR_SERVER_PORT;
340  }
341  } else {
342  localSolrServerPort = DEFAULT_SOLR_SERVER_PORT;
343  ModuleSettings.setConfigSetting(PROPERTIES_FILE, PROPERTIES_CURRENT_SERVER_PORT, String.valueOf(localSolrServerPort));
344  }
345 
346  if (ModuleSettings.settingExists(PROPERTIES_FILE, PROPERTIES_CURRENT_STOP_PORT)) {
347  try {
348  localSolrStopPort = Integer.decode(ModuleSettings.getConfigSetting(PROPERTIES_FILE, PROPERTIES_CURRENT_STOP_PORT));
349  } catch (NumberFormatException nfe) {
350  logger.log(Level.WARNING, "Could not decode indexing server stop port, value was not a valid port number, using default", nfe); //NON-NLS
351  localSolrStopPort = DEFAULT_SOLR_STOP_PORT;
352  }
353  } else {
354  localSolrStopPort = DEFAULT_SOLR_STOP_PORT;
355  ModuleSettings.setConfigSetting(PROPERTIES_FILE, PROPERTIES_CURRENT_STOP_PORT, String.valueOf(localSolrStopPort));
356  }
357  }
358 
359  private HttpSolrClient getSolrClient(String solrUrl) {
360  int connectionTimeoutMs = org.sleuthkit.autopsy.keywordsearch.UserPreferences.getConnectionTimeout();
361  return new HttpSolrClient.Builder(solrUrl)
362  .withSocketTimeout(connectionTimeoutMs)
363  .withConnectionTimeout(connectionTimeoutMs)
364  .withResponseParser(new XMLResponseParser())
365  .build();
366  }
367 
368  private ConcurrentUpdateSolrClient getConcurrentClient(String solrUrl) {
369  int numThreads = org.sleuthkit.autopsy.keywordsearch.UserPreferences.getNumThreads();
370  int numDocs = org.sleuthkit.autopsy.keywordsearch.UserPreferences.getDocumentsQueueSize();
371  int connectionTimeoutMs = org.sleuthkit.autopsy.keywordsearch.UserPreferences.getConnectionTimeout();
372  logger.log(Level.INFO, "Creating new ConcurrentUpdateSolrClient: {0}", solrUrl); //NON-NLS
373  logger.log(Level.INFO, "Queue size = {0}, Number of threads = {1}, Connection Timeout (ms) = {2}", new Object[]{numDocs, numThreads, connectionTimeoutMs}); //NON-NLS
374  ConcurrentUpdateSolrClient client = new ConcurrentUpdateSolrClient.Builder(solrUrl)
375  .withQueueSize(numDocs)
376  .withThreadCount(numThreads)
377  .withSocketTimeout(connectionTimeoutMs)
378  .withConnectionTimeout(connectionTimeoutMs)
379  .withResponseParser(new XMLResponseParser())
380  .build();
381 
382  return client;
383  }
384 
385  private CloudSolrClient getCloudSolrClient(String host, String port, String defaultCollectionName) throws KeywordSearchModuleException {
386  List<String> solrServerList = getSolrServerList(host, port);
387  List<String> solrUrls = new ArrayList<>();
388  for (String server : solrServerList) {
389  solrUrls.add("http://" + server + "/solr");
390  logger.log(Level.INFO, "Using Solr server: {0}", server);
391  }
392 
393  logger.log(Level.INFO, "Creating new CloudSolrClient"); //NON-NLS
394  int connectionTimeoutMs = org.sleuthkit.autopsy.keywordsearch.UserPreferences.getConnectionTimeout();
395  CloudSolrClient client = new CloudSolrClient.Builder(solrUrls)
396  .withConnectionTimeout(connectionTimeoutMs)
397  .withSocketTimeout(connectionTimeoutMs)
398  .build();
399  if (!defaultCollectionName.isEmpty()) {
400  client.setDefaultCollection(defaultCollectionName);
401  }
402  client.connect();
403  return client;
404  }
405 
406  @Override
407  public void finalize() throws java.lang.Throwable {
408  stop();
409  super.finalize();
410  }
411 
412  public void addServerActionListener(PropertyChangeListener l) {
413  serverAction.addPropertyChangeListener(l);
414  }
415 
416  int getLocalSolrServerPort() {
417  return localSolrServerPort;
418  }
419 
420  int getLocalSolrStopPort() {
421  return localSolrStopPort;
422  }
423 
427  private static class InputStreamPrinterThread extends Thread {
428 
429  InputStream stream;
430  OutputStream out;
431  volatile boolean doRun = true;
432 
433  InputStreamPrinterThread(InputStream stream, String type) {
434  this.stream = stream;
435  try {
436  final String log = Places.getUserDirectory().getAbsolutePath()
437  + File.separator + "var" + File.separator + "log" //NON-NLS
438  + File.separator + "solr.log." + type; //NON-NLS
439  File outputFile = new File(log.concat(".0"));
440  File first = new File(log.concat(".1"));
441  File second = new File(log.concat(".2"));
442  if (second.exists()) {
443  second.delete();
444  }
445  if (first.exists()) {
446  first.renameTo(second);
447  }
448  if (outputFile.exists()) {
449  outputFile.renameTo(first);
450  } else {
451  outputFile.createNewFile();
452  }
453  out = new FileOutputStream(outputFile);
454 
455  } catch (Exception ex) {
456  logger.log(Level.WARNING, "Failed to create solr log file", ex); //NON-NLS
457  }
458  }
459 
460  void stopRun() {
461  doRun = false;
462  }
463 
464  @Override
465  public void run() {
466 
467  try (InputStreamReader isr = new InputStreamReader(stream);
468  BufferedReader br = new BufferedReader(isr);
469  OutputStreamWriter osw = new OutputStreamWriter(out, PlatformUtil.getDefaultPlatformCharset());
470  BufferedWriter bw = new BufferedWriter(osw);) {
471 
472  String line = null;
473  while (doRun && (line = br.readLine()) != null) {
474  bw.write(line);
475  bw.newLine();
476  if (DEBUG) {
477  //flush buffers if dev version for debugging
478  bw.flush();
479  }
480  }
481  bw.flush();
482  } catch (IOException ex) {
483  logger.log(Level.SEVERE, "Error redirecting Solr output stream", ex); //NON-NLS
484  }
485  }
486  }
487 
497  private Process runLocalSolr8ControlCommand(List<String> solrArguments) throws IOException {
498  final String MAX_SOLR_MEM_MB_PAR = "-Xmx" + UserPreferences.getMaxSolrVMSize() + "m"; //NON-NLS
499 
500  // This is our customized version of the Solr batch script to start/stop Solr.
501  File solr8Folder = InstalledFileLocator.getDefault().locate("solr", Server.class.getPackage().getName(), false); //NON-NLS
502  Path solr8CmdPath;
504  solr8CmdPath = Paths.get(solr8Folder.getAbsolutePath(), "bin", "autopsy-solr.cmd"); //NON-NLS
505  } else {
506  solr8CmdPath = Paths.get(solr8Folder.getAbsolutePath(), "bin", "autopsy-solr"); //NON-NLS
507  }
508  Path solr8Home = Paths.get(PlatformUtil.getUserDirectory().getAbsolutePath(), "solr"); //NON-NLS
509 
510  List<String> commandLine = new ArrayList<>();
511  commandLine.add(solr8CmdPath.toString());
512  commandLine.addAll(solrArguments);
513 
514  ProcessBuilder solrProcessBuilder = new ProcessBuilder(commandLine);
515  solrProcessBuilder.directory(solr8Folder);
516 
517  // Redirect stdout and stderr to files to prevent blocking.
518  Path solrStdoutPath = Paths.get(Places.getUserDirectory().getAbsolutePath(), "var", "log", "solr.log.stdout"); //NON-NLS
519  solrProcessBuilder.redirectOutput(solrStdoutPath.toFile());
520 
521  Path solrStderrPath = Paths.get(Places.getUserDirectory().getAbsolutePath(), "var", "log", "solr.log.stderr"); //NON-NLS
522  solrProcessBuilder.redirectError(solrStderrPath.toFile());
523 
524  // get the path to the JRE folder. That's what Solr needs as SOLR_JAVA_HOME
525  String jreFolderPath = new File(javaPath).getParentFile().getParentFile().getAbsolutePath();
526 
527  solrProcessBuilder.environment().put("SOLR_JAVA_HOME", jreFolderPath); // NON-NLS
528  solrProcessBuilder.environment().put("SOLR_HOME", solr8Home.toString()); // NON-NLS
529  solrProcessBuilder.environment().put("STOP_KEY", KEY); // NON-NLS
530  solrProcessBuilder.environment().put("SOLR_JAVA_MEM", MAX_SOLR_MEM_MB_PAR); // NON-NLS
531  logger.log(Level.INFO, "Setting Solr 8 directory: {0}", solr8Folder.toString()); //NON-NLS
532  logger.log(Level.INFO, "Running Solr 8 command: {0} from {1}", new Object[]{solrProcessBuilder.command(), solr8Folder.toString()}); //NON-NLS
533  Process process = solrProcessBuilder.start();
534  logger.log(Level.INFO, "Finished running Solr 8 command"); //NON-NLS
535  return process;
536  }
537 
547  private Process runLocalSolr4ControlCommand(List<String> solrArguments) throws IOException {
548  final String MAX_SOLR_MEM_MB_PAR = "-Xmx" + UserPreferences.getMaxSolrVMSize() + "m"; //NON-NLS
549  File solr4Folder = InstalledFileLocator.getDefault().locate("solr4", Server.class.getPackage().getName(), false); //NON-NLS
550 
551  List<String> commandLine = new ArrayList<>();
552  commandLine.add(javaPath);
553  commandLine.add(MAX_SOLR_MEM_MB_PAR);
554  commandLine.add("-DSTOP.PORT=" + localSolrStopPort); //NON-NLS
555  commandLine.add("-Djetty.port=" + localSolrServerPort); //NON-NLS
556  commandLine.add("-DSTOP.KEY=" + KEY); //NON-NLS
557  commandLine.add("-jar"); //NON-NLS
558  commandLine.add("start.jar"); //NON-NLS
559 
560  commandLine.addAll(solrArguments);
561 
562  ProcessBuilder solrProcessBuilder = new ProcessBuilder(commandLine);
563  solrProcessBuilder.directory(solr4Folder);
564 
565  // Redirect stdout and stderr to files to prevent blocking.
566  Path solrStdoutPath = Paths.get(Places.getUserDirectory().getAbsolutePath(), "var", "log", "solr.log.stdout"); //NON-NLS
567  solrProcessBuilder.redirectOutput(solrStdoutPath.toFile());
568 
569  Path solrStderrPath = Paths.get(Places.getUserDirectory().getAbsolutePath(), "var", "log", "solr.log.stderr"); //NON-NLS
570  solrProcessBuilder.redirectError(solrStderrPath.toFile());
571 
572  logger.log(Level.INFO, "Running Solr 4 command: {0}", solrProcessBuilder.command()); //NON-NLS
573  Process process = solrProcessBuilder.start();
574  logger.log(Level.INFO, "Finished running Solr 4 command"); //NON-NLS
575  return process;
576  }
577 
583  List<Long> getSolrPIDs() {
584  List<Long> pids = new ArrayList<>();
585 
586  //NOTE: these needs to be in sync with process start string in start()
587  final String pidsQuery = "-DSTOP.KEY=" + KEY + "%start.jar"; //NON-NLS
588 
589  long[] pidsArr = PlatformUtil.getJavaPIDs(pidsQuery);
590  if (pidsArr != null) {
591  for (int i = 0; i < pidsArr.length; ++i) {
592  pids.add(pidsArr[i]);
593  }
594  }
595 
596  return pids;
597  }
598 
603  void killSolr() {
604  List<Long> solrPids = getSolrPIDs();
605  for (long pid : solrPids) {
606  logger.log(Level.INFO, "Trying to kill old Solr process, PID: {0}", pid); //NON-NLS
607  PlatformUtil.killProcess(pid);
608  }
609  }
610 
611  void start() throws KeywordSearchModuleException, SolrServerNoPortException, SolrServerException {
612  startLocalSolr(SOLR_VERSION.SOLR8);
613  }
614 
615  @Messages({
616  "# {0} - indexVersion",
617  "Server_configureSolrConnection_illegalSolrVersion=The solr version in the case: {0}, is not supported."
618  })
619  private void configureSolrConnection(Case theCase, Index index) throws KeywordSearchModuleException, SolrServerNoPortException {
620 
621  try {
622  if (theCase.getCaseType() == CaseType.SINGLE_USER_CASE) {
623 
624  // makes sure the proper local Solr server is running
625  if (!IndexFinder.getCurrentSolrVersion().equals(index.getSolrVersion())) {
626  throw new KeywordSearchModuleException(Bundle.Server_configureSolrConnection_illegalSolrVersion(index.getSolrVersion()));
627  }
628 
629  startLocalSolr(SOLR_VERSION.SOLR8);
630 
631  // check if the local Solr server is running
632  if (!this.isLocalSolrRunning()) {
633  logger.log(Level.SEVERE, "Local Solr server is not running"); //NON-NLS
634  throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.openCore.exception.msg"));
635  }
636  } else {
637  // create SolrJ client to connect to remore Solr server
638  remoteSolrServer = configureMultiUserConnection(theCase, index, "");
639 
640  // test the connection
641  connectToSolrServer(remoteSolrServer);
642  }
643  } catch (SolrServerException | IOException ex) {
644  throw new KeywordSearchModuleException(NbBundle.getMessage(Server.class, "Server.connect.exception.msg", ex.getLocalizedMessage()), ex);
645  }
646  }
647 
662  private HttpSolrClient configureMultiUserConnection(Case theCase, Index index, String name) throws KeywordSearchModuleException {
663 
664  // read Solr connection info from user preferences, unless "solrserver.txt" is present
666  if (properties.host.isEmpty() || properties.port.isEmpty()) {
667  throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.connectionInfoMissing.exception.msg", index.getSolrVersion()));
668  }
669  String solrUrl = "http://" + properties.host + ":" + properties.port + "/solr";
670 
671  if (!name.isEmpty()) {
672  solrUrl = solrUrl + "/" + name;
673  }
674 
675  // create SolrJ client to connect to remore Solr server
676  return getSolrClient(solrUrl);
677  }
678 
684  @NbBundle.Messages({
685  "Server.status.failed.msg=Local Solr server did not respond to status request. This may be because the server failed to start or is taking too long to initialize.",})
686  synchronized void startLocalSolr(SOLR_VERSION version) throws KeywordSearchModuleException, SolrServerNoPortException, SolrServerException {
687 
688  logger.log(Level.INFO, "Starting local Solr " + version + " server"); //NON-NLS
689  if (version == SOLR_VERSION.SOLR8) {
690  localSolrFolder = InstalledFileLocator.getDefault().locate("solr", Server.class.getPackage().getName(), false); //NON-NLS
691  } else {
692  throw new KeywordSearchModuleException(Bundle.Server_configureSolrConnection_illegalSolrVersion(version.name()));
693  }
694 
695  if (isLocalSolrRunning()) {
696  if (localServerVersion.equals(version)) {
697  // this version of local server is already running
698  logger.log(Level.INFO, "Local Solr " + version + " server is already running"); //NON-NLS
699  return;
700  } else {
701  // wrong version of local server is running, stop it
702  stop();
703  }
704  }
705 
706  // set which version of local server is currently running
707  localServerVersion = version;
708 
709  if (!isPortAvailable(localSolrServerPort)) {
710  // There is something already listening on our port. Let's see if
711  // this is from an earlier run that didn't successfully shut down
712  // and if so kill it.
713  final List<Long> pids = this.getSolrPIDs();
714 
715  // If the culprit listening on the port is not a Solr process
716  // we refuse to start.
717  if (pids.isEmpty()) {
718  throw new SolrServerNoPortException(localSolrServerPort);
719  }
720 
721  // Ok, we've tried to stop it above but there still appears to be
722  // a Solr process listening on our port so we forcefully kill it.
723  killSolr();
724 
725  // If either of the ports are still in use after our attempt to kill
726  // previously running processes we give up and throw an exception.
727  if (!isPortAvailable(localSolrServerPort)) {
728  throw new SolrServerNoPortException(localSolrServerPort);
729  }
730  if (!isPortAvailable(localSolrStopPort)) {
731  throw new SolrServerNoPortException(localSolrStopPort);
732  }
733  }
734 
735  if (isPortAvailable(localSolrServerPort)) {
736  logger.log(Level.INFO, "Port [{0}] available, starting Solr", localSolrServerPort); //NON-NLS
737  try {
738  if (version == SOLR_VERSION.SOLR8) {
739  logger.log(Level.INFO, "Starting Solr 8 server"); //NON-NLS
740  curSolrProcess = runLocalSolr8ControlCommand(new ArrayList<>(Arrays.asList("start", "-p", //NON-NLS
741  Integer.toString(localSolrServerPort)))); //NON-NLS
742  } else {
743  // solr4
744  logger.log(Level.INFO, "Starting Solr 4 server"); //NON-NLS
745  curSolrProcess = runLocalSolr4ControlCommand(new ArrayList<>(
746  Arrays.asList("-Dbootstrap_confdir=../solr/configsets/AutopsyConfig/conf", //NON-NLS
747  "-Dcollection.configName=AutopsyConfig"))); //NON-NLS
748  }
749 
750  // Wait for the Solr server to start and respond to a statusRequest request.
751  for (int numRetries = 0; numRetries < NUM_EMBEDDED_SERVER_RETRIES; numRetries++) {
752  if (isLocalSolrRunning()) {
753  final List<Long> pids = this.getSolrPIDs();
754  logger.log(Level.INFO, "New Solr process PID: {0}", pids); //NON-NLS
755  return;
756  }
757 
758  // Local Solr server did not respond so we sleep for
759  // 5 seconds before trying again.
760  try {
761  TimeUnit.SECONDS.sleep(EMBEDDED_SERVER_RETRY_WAIT_SEC);
762  } catch (InterruptedException ex) {
763  logger.log(Level.WARNING, "Timer interrupted"); //NON-NLS
764  }
765  }
766 
767  // If we get here the Solr server has not responded to connection
768  // attempts in a timely fashion.
769  logger.log(Level.WARNING, "Local Solr server failed to respond to status requests.");
770  WindowManager.getDefault().invokeWhenUIReady(new Runnable() {
771  @Override
772  public void run() {
773  MessageNotifyUtil.Notify.error(
774  NbBundle.getMessage(this.getClass(), "Installer.errorInitKsmMsg"),
775  Bundle.Server_status_failed_msg());
776  }
777  });
778  } catch (SecurityException ex) {
779  throw new KeywordSearchModuleException(
780  NbBundle.getMessage(this.getClass(), "Server.start.exception.cantStartSolr.msg"), ex);
781  } catch (IOException ex) {
782  throw new KeywordSearchModuleException(
783  NbBundle.getMessage(this.getClass(), "Server.start.exception.cantStartSolr.msg2"), ex);
784  }
785  }
786  }
787 
794  static boolean isPortAvailable(int port) {
795  // implementation taken from https://stackoverflow.com/a/435579
796  if (port < 1 || port > 65535) {
797  throw new IllegalArgumentException("Invalid start port: " + port);
798  }
799 
800  ServerSocket ss = null;
801  DatagramSocket ds = null;
802  try {
803  ss = new ServerSocket(port);
804  ss.setReuseAddress(true);
805  ds = new DatagramSocket(port);
806  ds.setReuseAddress(true);
807  return true;
808  } catch (IOException e) {
809  } finally {
810  if (ds != null) {
811  ds.close();
812  }
813 
814  if (ss != null) {
815  try {
816  ss.close();
817  } catch (IOException e) {
818  /* should not be thrown */
819  }
820  }
821  }
822 
823  return false;
824  }
825 
826 
832  void changeSolrServerPort(int port) {
833  localSolrServerPort = port;
834  ModuleSettings.setConfigSetting(PROPERTIES_FILE, PROPERTIES_CURRENT_SERVER_PORT, String.valueOf(port));
835  }
836 
842  void changeSolrStopPort(int port) {
843  localSolrStopPort = port;
844  ModuleSettings.setConfigSetting(PROPERTIES_FILE, PROPERTIES_CURRENT_STOP_PORT, String.valueOf(port));
845  }
846 
852  synchronized void stop() {
853 
854  try {
855  // Close any open core before stopping server
856  closeCore();
857  } catch (KeywordSearchModuleException e) {
858  logger.log(Level.WARNING, "Failed to close core: ", e); //NON-NLS
859  }
860 
861  stopLocalSolr();
862  }
863 
867  private void stopLocalSolr() {
868  try {
869  //try graceful shutdown
870  Process process;
871  if (localServerVersion == SOLR_VERSION.SOLR8) {
872  logger.log(Level.INFO, "Stopping Solr 8 server"); //NON-NLS
873  process = runLocalSolr8ControlCommand(new ArrayList<>(Arrays.asList("stop", "-k", KEY, "-p", Integer.toString(localSolrServerPort)))); //NON-NLS
874  } else {
875  // solr 4
876  logger.log(Level.INFO, "Stopping Solr 4 server"); //NON-NLS
877  process = runLocalSolr4ControlCommand(new ArrayList<>(Arrays.asList("--stop"))); //NON-NLS
878  }
879 
880  logger.log(Level.INFO, "Waiting for Solr server to stop"); //NON-NLS
881  process.waitFor();
882 
883  //if still running, forcefully stop it
884  if (curSolrProcess != null) {
885  curSolrProcess.destroy();
886  curSolrProcess = null;
887  }
888 
889  } catch (IOException | InterruptedException ex) {
890  logger.log(Level.WARNING, "Error while attempting to stop Solr server", ex);
891  } finally {
892  //stop Solr stream -> log redirect threads
893  try {
894  if (errorRedirectThread != null) {
895  errorRedirectThread.stopRun();
896  errorRedirectThread = null;
897  }
898  } finally {
899  //if still running, kill it
900  killSolr();
901  }
902 
903  logger.log(Level.INFO, "Finished stopping Solr server"); //NON-NLS
904  }
905  }
906 
914  synchronized boolean isLocalSolrRunning() throws KeywordSearchModuleException {
915  try {
916 
917  if (isPortAvailable(localSolrServerPort)) {
918  return false;
919  }
920 
921  // making a statusRequest request here instead of just doing solrServer.ping(), because
922  // that doesn't work when there are no cores
923  //TODO handle timeout in cases when some other type of server on that port
925 
926  logger.log(Level.INFO, "Solr server is running"); //NON-NLS
927  } catch (SolrServerException ex) {
928 
929  Throwable cause = ex.getRootCause();
930 
931  // TODO: check if SocketExceptions should actually happen (is
932  // probably caused by starting a connection as the server finishes
933  // shutting down)
934  if (cause instanceof ConnectException || cause instanceof SocketException) { //|| cause instanceof NoHttpResponseException) {
935  logger.log(Level.INFO, "Solr server is not running, cause: {0}", cause.getMessage()); //NON-NLS
936  return false;
937  } else {
938  throw new KeywordSearchModuleException(
939  NbBundle.getMessage(this.getClass(), "Server.isRunning.exception.errCheckSolrRunning.msg"), ex);
940  }
941  } catch (SolrException ex) {
942  // Just log 404 errors for now...
943  logger.log(Level.INFO, "Solr server is not running", ex); //NON-NLS
944  return false;
945  } catch (IOException ex) {
946  throw new KeywordSearchModuleException(
947  NbBundle.getMessage(this.getClass(), "Server.isRunning.exception.errCheckSolrRunning.msg2"), ex);
948  }
949 
950  return true;
951  }
952 
953  /*
954  * ** Convenience methods for use while we only open one case at a time ***
955  */
965  void openCoreForCase(Case theCase, Index index) throws KeywordSearchModuleException {
966  currentCoreLock.writeLock().lock();
967  try {
968  currentCollection = openCore(theCase, index);
969 
970  try {
971  // execute a test query. if it fails, an exception will be thrown
973  } catch (NoOpenCoreException ex) {
974  throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.openCore.exception.cantOpen.msg"), ex);
975  }
976 
977  serverAction.putValue(CORE_EVT, CORE_EVT_STATES.STARTED);
978  } finally {
979  currentCoreLock.writeLock().unlock();
980  }
981  }
982 
988  boolean coreIsOpen() {
989  currentCoreLock.readLock().lock();
990  try {
991  return (null != currentCollection);
992  } finally {
993  currentCoreLock.readLock().unlock();
994  }
995  }
996 
997  Index getIndexInfo() throws NoOpenCoreException {
998  currentCoreLock.readLock().lock();
999  try {
1000  if (null == currentCollection) {
1001  throw new NoOpenCoreException();
1002  }
1003  return currentCollection.getIndexInfo();
1004  } finally {
1005  currentCoreLock.readLock().unlock();
1006  }
1007  }
1008 
1009  void closeCore() throws KeywordSearchModuleException {
1010  currentCoreLock.writeLock().lock();
1011  try {
1012  if (null != currentCollection) {
1013  currentCollection.close();
1014  serverAction.putValue(CORE_EVT, CORE_EVT_STATES.STOPPED);
1015  }
1016  } finally {
1017  currentCollection = null;
1018  currentCoreLock.writeLock().unlock();
1019  }
1020  }
1021 
1022  void addDocument(SolrInputDocument doc) throws KeywordSearchModuleException, NoOpenCoreException {
1023  currentCoreLock.readLock().lock();
1024  try {
1025  if (null == currentCollection) {
1026  throw new NoOpenCoreException();
1027  }
1028  TimingMetric metric = HealthMonitor.getTimingMetric("Solr: Index chunk");
1029  currentCollection.addDocument(doc);
1030  HealthMonitor.submitTimingMetric(metric);
1031  } finally {
1032  currentCoreLock.readLock().unlock();
1033  }
1034  }
1035 
1044  @NbBundle.Messages({
1045  "# {0} - colelction name", "Server.deleteCore.exception.msg=Failed to delete Solr colelction {0}",})
1046  void deleteCollection(String coreName, CaseMetadata metadata) throws KeywordSearchServiceException, KeywordSearchModuleException {
1047  try {
1048  HttpSolrClient solrServer;
1049  if (metadata.getCaseType() == CaseType.SINGLE_USER_CASE) {
1050  solrServer = getSolrClient("http://localhost:" + localSolrServerPort + "/solr"); //NON-NLS
1051  CoreAdminResponse response = CoreAdminRequest.getStatus(coreName, solrServer);
1052  if (null != response.getCoreStatus(coreName).get("instanceDir")) { //NON-NLS
1053  /*
1054  * Send a core unload request to the Solr server, with the
1055  * parameter set that request deleting the index and the
1056  * instance directory (deleteInstanceDir = true). Note that
1057  * this removes everything related to the core on the server
1058  * (the index directory, the configuration files, etc.), but
1059  * does not delete the actual Solr text index because it is
1060  * currently stored in the case directory.
1061  */
1062  org.apache.solr.client.solrj.request.CoreAdminRequest.unloadCore(coreName, true, true, solrServer);
1063  }
1064  } else {
1065  IndexingServerProperties properties = getMultiUserServerProperties(metadata.getCaseDirectory());
1066  solrServer = getSolrClient("http://" + properties.getHost() + ":" + properties.getPort() + "/solr");
1067  connectToSolrServer(solrServer);
1068 
1069  CollectionAdminRequest.Delete deleteCollectionRequest = CollectionAdminRequest.deleteCollection(coreName);
1070  CollectionAdminResponse response = deleteCollectionRequest.process(solrServer);
1071  if (response.isSuccess()) {
1072  logger.log(Level.INFO, "Deleted collection {0}", coreName); //NON-NLS
1073  } else {
1074  logger.log(Level.WARNING, "Unable to delete collection {0}", coreName); //NON-NLS
1075  }
1076  }
1077  } catch (SolrServerException | IOException ex) {
1078  // We will get a RemoteSolrException with cause == null and detailsMessage
1079  // == "Already closed" if the core is not loaded. This is not an error in this scenario.
1080  if (!ex.getMessage().equals("Already closed")) { // NON-NLS
1081  throw new KeywordSearchServiceException(Bundle.Server_deleteCore_exception_msg(coreName), ex);
1082  }
1083  }
1084  }
1085 
1097  @NbBundle.Messages({
1098  "Server.exceptionMessage.unableToCreateCollection=Unable to create Solr collection",
1099  "Server.exceptionMessage.unableToBackupCollection=Unable to backup Solr collection",
1100  "Server.exceptionMessage.unableToRestoreCollection=Unable to restore Solr collection",
1101  })
1102  private Collection openCore(Case theCase, Index index) throws KeywordSearchModuleException {
1103 
1104  int numShardsToUse = 1;
1105  try {
1106  // connect to proper Solr server
1107  configureSolrConnection(theCase, index);
1108 
1109  if (theCase.getCaseType() == CaseType.MULTI_USER_CASE) {
1110  // select number of shards to use
1111  numShardsToUse = getNumShardsToUse();
1112  }
1113  } catch (Exception ex) {
1114  // intentional "catch all" as Solr is known to throw all kinds of Runtime exceptions
1115  throw new KeywordSearchModuleException(NbBundle.getMessage(Server.class, "Server.connect.exception.msg", ex.getLocalizedMessage()), ex);
1116  }
1117 
1118  try {
1119  String collectionName = index.getIndexName();
1120 
1121  if (theCase.getCaseType() == CaseType.MULTI_USER_CASE) {
1122  if (!collectionExists(collectionName)) {
1123  /*
1124  * The collection does not exist. Make a request that will cause the colelction to be created.
1125  */
1126  boolean doRetry = false;
1127  for (int reTryAttempt = 0; reTryAttempt < NUM_COLLECTION_CREATION_RETRIES; reTryAttempt++) {
1128  try {
1129  doRetry = false;
1130  createMultiUserCollection(collectionName, numShardsToUse);
1131  } catch (Exception ex) {
1132  if (reTryAttempt >= NUM_COLLECTION_CREATION_RETRIES) {
1133  logger.log(Level.SEVERE, "Unable to create Solr collection " + collectionName, ex); //NON-NLS
1134  throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.openCore.exception.cantOpen.msg"), ex);
1135  } else {
1136  logger.log(Level.SEVERE, "Unable to create Solr collection " + collectionName + ". Re-trying...", ex); //NON-NLS
1137  Thread.sleep(1000L);
1138  doRetry = true;
1139  }
1140  }
1141  if (!doRetry) {
1142  break;
1143  }
1144  }
1145  }
1146  } else {
1147  if (!coreIsLoaded(collectionName)) {
1148  // In single user mode, the index is stored in case output directory
1149  File dataDir = new File(new File(index.getIndexPath()).getParent()); // "data dir" is the parent of the index directory
1150  if (!dataDir.exists()) {
1151  dataDir.mkdirs();
1152  }
1153 
1154  // In single user mode, if there is a core.properties file already,
1155  // we've hit a solr bug. Compensate by deleting it.
1156  if (theCase.getCaseType() == CaseType.SINGLE_USER_CASE) {
1157  Path corePropertiesFile = Paths.get(localSolrFolder.toString(), SOLR, collectionName, CORE_PROPERTIES);
1158  if (corePropertiesFile.toFile().exists()) {
1159  try {
1160  corePropertiesFile.toFile().delete();
1161  } catch (Exception ex) {
1162  logger.log(Level.INFO, "Could not delete pre-existing core.properties prior to opening the core."); //NON-NLS
1163  }
1164  }
1165  }
1166 
1167  // for single user cases, we unload the core when we close the case. So we have to load the core again.
1168  CoreAdminRequest.Create createCoreRequest = new CoreAdminRequest.Create();
1169  createCoreRequest.setDataDir(dataDir.getAbsolutePath());
1170  createCoreRequest.setCoreName(collectionName);
1171  createCoreRequest.setConfigSet("AutopsyConfig"); //NON-NLS
1172  createCoreRequest.setIsLoadOnStartup(false);
1173  createCoreRequest.setIsTransient(true);
1174  localSolrServer.request(createCoreRequest);
1175 
1176  if (!coreIndexFolderExists(collectionName)) {
1177  throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.openCore.exception.noIndexDir.msg"));
1178  }
1179  }
1180  }
1181 
1182  return new Collection(collectionName, theCase, index);
1183 
1184  } catch (Exception ex) {
1185  logger.log(Level.SEVERE, "Exception during Solr collection creation.", ex); //NON-NLS
1186  throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.openCore.exception.cantOpen.msg"), ex);
1187  }
1188  }
1189 
1190  private int getNumShardsToUse() throws KeywordSearchModuleException {
1191 
1192  // if we want to use a specific sharding strategy, use that
1193  if (org.sleuthkit.autopsy.keywordsearch.UserPreferences.getMaxNumShards() > 0) {
1194  return org.sleuthkit.autopsy.keywordsearch.UserPreferences.getMaxNumShards();
1195  }
1196 
1197  // otherwise get list of all live Solr servers in the cluster
1198  List<String> solrServerList = getSolrServerList(remoteSolrServer);
1199  // shard across all available servers
1200  return solrServerList.size();
1201  }
1202 
1203  /*
1204  * Poll the remote Solr server for list of existing collections, and check if
1205  * the collection of interest exists.
1206  *
1207  * @param collectionName The name of the collection.
1208  *
1209  * @return True if the collection exists, false otherwise.
1210  *
1211  * @throws SolrServerException If there is a problem communicating with the
1212  * Solr server.
1213  * @throws IOException If there is a problem communicating with the Solr
1214  * server.
1215  */
1216  private boolean collectionExists(String collectionName) throws SolrServerException, IOException {
1217  CollectionAdminRequest.List req = new CollectionAdminRequest.List();
1218  CollectionAdminResponse response = req.process(remoteSolrServer);
1219  List<?> existingCollections = (List<?>) response.getResponse().get("collections");
1220  if (existingCollections == null) {
1221  existingCollections = new ArrayList<>();
1222  }
1223  return existingCollections.contains(collectionName);
1224  }
1225 
1226  /* NOTE: Keeping this code for reference, since it works.
1227  private boolean collectionExists(String collectionName) throws SolrServerException, IOException {
1228 
1229  // TODO we could potentially use this API. Currently set exception "Solr instance is not running in SolrCloud mode"
1230  // List<String> list = CollectionAdminRequest.listCollections(localSolrServer);
1231 
1232  CollectionAdminRequest.ClusterStatus statusRequest = CollectionAdminRequest.getClusterStatus().setCollectionName(collectionName);
1233  CollectionAdminResponse statusResponse;
1234  try {
1235  statusResponse = statusRequest.process(remoteSolrServer);
1236  } catch (RemoteSolrException ex) {
1237  // collection doesn't exist
1238  return false;
1239  }
1240 
1241  if (statusResponse == null) {
1242  return false;
1243  }
1244 
1245  NamedList error = (NamedList) statusResponse.getResponse().get("error");
1246  if (error != null) {
1247  return false;
1248  }
1249 
1250  // For some reason this returns info about all collections even though it's supposed to only return about the one we are requesting
1251  NamedList cluster = (NamedList) statusResponse.getResponse().get("cluster");
1252  NamedList collections = (NamedList) cluster.get("collections");
1253  if (collections != null) {
1254  Object collection = collections.get(collectionName);
1255  return (collection != null);
1256  } else {
1257  return false;
1258  }
1259  }*/
1260 
1261  private void createMultiUserCollection(String collectionName, int numShardsToUse) throws KeywordSearchModuleException, SolrServerException, IOException {
1262  /*
1263  * The core either does not exist or it is not loaded. Make a
1264  * request that will cause the core to be created if it does not
1265  * exist or loaded if it already exists.
1266  */
1267 
1268  Integer numShards = numShardsToUse;
1269  logger.log(Level.INFO, "numShardsToUse: {0}", numShardsToUse);
1270  Integer numNrtReplicas = 1;
1271  Integer numTlogReplicas = 0;
1272  Integer numPullReplicas = 0;
1273  CollectionAdminRequest.Create createCollectionRequest = CollectionAdminRequest.createCollection(collectionName, "AutopsyConfig", numShards, numNrtReplicas, numTlogReplicas, numPullReplicas);
1274 
1275  CollectionAdminResponse createResponse = createCollectionRequest.process(remoteSolrServer);
1276  if (createResponse.isSuccess()) {
1277  logger.log(Level.INFO, "Collection {0} successfully created.", collectionName);
1278  } else {
1279  logger.log(Level.SEVERE, "Unable to create Solr collection {0}", collectionName); //NON-NLS
1280  throw new KeywordSearchModuleException(Bundle.Server_exceptionMessage_unableToCreateCollection());
1281  }
1282 
1283  /* If we need core name:
1284  Map<String, NamedList<Integer>> status = createResponse.getCollectionCoresStatus();
1285  existingCoreName = status.keySet().iterator().next();*/
1286  if (!collectionExists(collectionName)) {
1287  throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.openCore.exception.noIndexDir.msg"));
1288  }
1289  }
1290 
1291  private void backupCollection(String collectionName, String backupName, String pathToBackupLocation) throws SolrServerException, IOException, KeywordSearchModuleException {
1292  CollectionAdminRequest.Backup backup = CollectionAdminRequest.backupCollection(collectionName, backupName)
1293  .setLocation(pathToBackupLocation);
1294 
1295  CollectionAdminResponse backupResponse = backup.process(remoteSolrServer);
1296  if (backupResponse.isSuccess()) {
1297  logger.log(Level.INFO, "Collection {0} successfully backep up.", collectionName);
1298  } else {
1299  logger.log(Level.SEVERE, "Unable to back up Solr collection {0}", collectionName); //NON-NLS
1300  throw new KeywordSearchModuleException(Bundle.Server_exceptionMessage_unableToBackupCollection());
1301  }
1302  }
1303 
1304  private void restoreCollection(String backupName, String restoreCollectionName, String pathToBackupLocation) throws SolrServerException, IOException, KeywordSearchModuleException {
1305 
1306  CollectionAdminRequest.Restore restore = CollectionAdminRequest.restoreCollection(restoreCollectionName, backupName)
1307  .setLocation(pathToBackupLocation);
1308 
1309  CollectionAdminResponse restoreResponse = restore.process(remoteSolrServer);
1310  if (restoreResponse.isSuccess()) {
1311  logger.log(Level.INFO, "Collection {0} successfully resored.", restoreCollectionName);
1312  } else {
1313  logger.log(Level.SEVERE, "Unable to restore Solr collection {0}", restoreCollectionName); //NON-NLS
1314  throw new KeywordSearchModuleException(Bundle.Server_exceptionMessage_unableToRestoreCollection());
1315  }
1316  }
1317 
1332  private boolean coreIsLoaded(String coreName) throws SolrServerException, IOException {
1333  CoreAdminResponse response = CoreAdminRequest.getStatus(coreName, localSolrServer);
1334  return response.getCoreStatus(coreName).get("instanceDir") != null; //NON-NLS
1335  }
1336 
1349  private boolean coreIndexFolderExists(String coreName) throws SolrServerException, IOException {
1350  CoreAdminResponse response = CoreAdminRequest.getStatus(coreName, localSolrServer);
1351  Object dataDirPath = response.getCoreStatus(coreName).get("dataDir"); //NON-NLS
1352  if (null != dataDirPath) {
1353  File indexDir = Paths.get((String) dataDirPath, "index").toFile(); //NON-NLS
1354  return indexDir.exists();
1355  } else {
1356  return false;
1357  }
1358  }
1359 
1371  public static IndexingServerProperties getMultiUserServerProperties(String caseDirectory) {
1372 
1373  // if "solrserver.txt" is present, use those connection settings
1374  Path serverFilePath = Paths.get(caseDirectory, "solrserver.txt"); //NON-NLS
1375  if (serverFilePath.toFile().exists()) {
1376  try {
1377  List<String> lines = Files.readAllLines(serverFilePath);
1378  if (lines.isEmpty()) {
1379  logger.log(Level.SEVERE, "solrserver.txt file does not contain any data"); //NON-NLS
1380  } else if (!lines.get(0).contains(",")) {
1381  logger.log(Level.SEVERE, "solrserver.txt file is corrupt - could not read host/port from " + lines.get(0)); //NON-NLS
1382  } else {
1383  String[] parts = lines.get(0).split(",");
1384  if (parts.length != 2) {
1385  logger.log(Level.SEVERE, "solrserver.txt file is corrupt - could not read host/port from " + lines.get(0)); //NON-NLS
1386  } else {
1387  return new IndexingServerProperties(parts[0], parts[1]);
1388  }
1389  }
1390  } catch (IOException ex) {
1391  logger.log(Level.SEVERE, "solrserver.txt file could not be read", ex); //NON-NLS
1392  }
1393  }
1394 
1395  // otherwise (or if an error occurred) determine Solr version of the current case
1396  List<Index> indexes = new ArrayList<>();
1397  try {
1398  IndexMetadata indexMetadata = new IndexMetadata(caseDirectory);
1399  indexes = indexMetadata.getIndexes();
1400  } catch (IndexMetadata.TextIndexMetadataException ex) {
1401  logger.log(Level.SEVERE, "Unable to read text index metadata file: " + caseDirectory, ex);
1402 
1403  // default to using the latest Solr version settings
1404  String host = UserPreferences.getIndexingServerHost();
1405  String port = UserPreferences.getIndexingServerPort();
1406  return new IndexingServerProperties(host, port);
1407  }
1408 
1409  // select which index to use. In practice, all cases always have only one
1410  // index but there is support for having multiple indexes.
1411  Index indexToUse = IndexFinder.identifyIndexToUse(indexes);
1412  if (indexToUse == null) {
1413  // unable to find index that can be used
1414  logger.log(Level.SEVERE, "Unable to find index that can be used for case: {0}", caseDirectory);
1415 
1416  // default to using the latest Solr version settings
1417  String host = UserPreferences.getIndexingServerHost();
1418  String port = UserPreferences.getIndexingServerPort();
1419  return new IndexingServerProperties(host, port);
1420  }
1421 
1422  // return connection info for the Solr version of the current index
1423  if (IndexFinder.getCurrentSolrVersion().equals(indexToUse.getSolrVersion())) {
1424  // Solr 8
1425  String host = UserPreferences.getIndexingServerHost();
1426  String port = UserPreferences.getIndexingServerPort();
1427  return new IndexingServerProperties(host, port);
1428  } else {
1429  // Solr 4
1430  String host = UserPreferences.getSolr4ServerHost().trim();
1431  String port = UserPreferences.getSolr4ServerPort().trim();
1432  return new IndexingServerProperties(host, port);
1433  }
1434  }
1435 
1447  public static void selectSolrServerForCase(Path rootOutputDirectory, Path caseDirectoryPath) throws KeywordSearchModuleException {
1448  // Look for the solr server list file
1449  String serverListName = "solrServerList.txt"; //NON-NLS
1450  Path serverListPath = Paths.get(rootOutputDirectory.toString(), serverListName);
1451  if (serverListPath.toFile().exists()) {
1452 
1453  // Read the list of solr servers
1454  List<String> lines;
1455  try {
1456  lines = Files.readAllLines(serverListPath);
1457  } catch (IOException ex) {
1458  throw new KeywordSearchModuleException(serverListName + " could not be read", ex); //NON-NLS
1459  }
1460 
1461  // Remove any lines that don't contain a comma (these are likely just whitespace)
1462  for (Iterator<String> iterator = lines.iterator(); iterator.hasNext();) {
1463  String line = iterator.next();
1464  if (!line.contains(",")) {
1465  // Remove the current element from the iterator and the list.
1466  iterator.remove();
1467  }
1468  }
1469  if (lines.isEmpty()) {
1470  throw new KeywordSearchModuleException(serverListName + " had no valid server information"); //NON-NLS
1471  }
1472 
1473  // Choose which server to use
1474  int rnd = new Random().nextInt(lines.size());
1475  String[] parts = lines.get(rnd).split(",");
1476  if (parts.length != 2) {
1477  throw new KeywordSearchModuleException("Invalid server data: " + lines.get(rnd)); //NON-NLS
1478  }
1479 
1480  // Split it up just to do a sanity check on the data
1481  String host = parts[0];
1482  String port = parts[1];
1483  if (host.isEmpty() || port.isEmpty()) {
1484  throw new KeywordSearchModuleException("Invalid server data: " + lines.get(rnd)); //NON-NLS
1485  }
1486 
1487  // Write the server data to a file
1488  Path serverFile = Paths.get(caseDirectoryPath.toString(), "solrserver.txt"); //NON-NLS
1489  try {
1490  caseDirectoryPath.toFile().mkdirs();
1491  if (!caseDirectoryPath.toFile().exists()) {
1492  throw new KeywordSearchModuleException("Case directory " + caseDirectoryPath.toString() + " does not exist"); //NON-NLS
1493  }
1494  Files.write(serverFile, lines.get(rnd).getBytes());
1495  } catch (IOException ex) {
1496  throw new KeywordSearchModuleException(serverFile.toString() + " could not be written", ex); //NON-NLS
1497  }
1498  }
1499  }
1500 
1504  public static class IndexingServerProperties {
1505 
1506  private final String host;
1507  private final String port;
1508 
1509  IndexingServerProperties(String host, String port) {
1510  this.host = host;
1511  this.port = port;
1512  }
1513 
1519  public String getHost() {
1520  return host;
1521  }
1522 
1528  public String getPort() {
1529  return port;
1530  }
1531  }
1532 
1538  void commit() throws SolrServerException, NoOpenCoreException {
1539  currentCoreLock.readLock().lock();
1540  try {
1541  if (null == currentCollection) {
1542  throw new NoOpenCoreException();
1543  }
1544  currentCollection.commit();
1545  } finally {
1546  currentCoreLock.readLock().unlock();
1547  }
1548  }
1549 
1550  NamedList<Object> request(SolrRequest<?> request) throws SolrServerException, RemoteSolrException, NoOpenCoreException {
1551  currentCoreLock.readLock().lock();
1552  try {
1553  if (null == currentCollection) {
1554  throw new NoOpenCoreException();
1555  }
1556  return currentCollection.request(request);
1557  } finally {
1558  currentCoreLock.readLock().unlock();
1559  }
1560  }
1561 
1572  public int queryNumIndexedFiles() throws KeywordSearchModuleException, NoOpenCoreException {
1573  currentCoreLock.readLock().lock();
1574  try {
1575  if (null == currentCollection) {
1576  throw new NoOpenCoreException();
1577  }
1578  try {
1579  return currentCollection.queryNumIndexedFiles();
1580  } catch (Exception ex) {
1581  // intentional "catch all" as Solr is known to throw all kinds of Runtime exceptions
1582  throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.queryNumIdxFiles.exception.msg"), ex);
1583  }
1584  } finally {
1585  currentCoreLock.readLock().unlock();
1586  }
1587  }
1588 
1598  public int queryNumIndexedChunks() throws KeywordSearchModuleException, NoOpenCoreException {
1599  currentCoreLock.readLock().lock();
1600  try {
1601  if (null == currentCollection) {
1602  throw new NoOpenCoreException();
1603  }
1604  try {
1605  return currentCollection.queryNumIndexedChunks();
1606  } catch (Exception ex) {
1607  // intentional "catch all" as Solr is known to throw all kinds of Runtime exceptions
1608  throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.queryNumIdxChunks.exception.msg"), ex);
1609  }
1610  } finally {
1611  currentCoreLock.readLock().unlock();
1612  }
1613  }
1614 
1624  public int queryNumIndexedDocuments() throws KeywordSearchModuleException, NoOpenCoreException {
1625  currentCoreLock.readLock().lock();
1626  try {
1627  if (null == currentCollection) {
1628  throw new NoOpenCoreException();
1629  }
1630  try {
1631  return currentCollection.queryNumIndexedDocuments();
1632  } catch (Exception ex) {
1633  // intentional "catch all" as Solr is known to throw all kinds of Runtime exceptions
1634  throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.queryNumIdxDocs.exception.msg"), ex);
1635  }
1636  } finally {
1637  currentCoreLock.readLock().unlock();
1638  }
1639  }
1640 
1651  public boolean queryIsFullyIndexed(long contentID) throws KeywordSearchModuleException, NoOpenCoreException {
1652  currentCoreLock.readLock().lock();
1653  try {
1654  if (null == currentCollection) {
1655  throw new NoOpenCoreException();
1656  }
1657  try {
1658  int totalNumChunks = currentCollection.queryTotalNumFileChunks(contentID);
1659  if (totalNumChunks == 0) {
1660  return false;
1661  }
1662 
1663  int numIndexedChunks = currentCollection.queryNumIndexedChunks(contentID);
1664  return numIndexedChunks == totalNumChunks;
1665  } catch (Exception ex) {
1666  // intentional "catch all" as Solr is known to throw all kinds of Runtime exceptions
1667  throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.queryIsIdxd.exception.msg"), ex);
1668  }
1669 
1670  } finally {
1671  currentCoreLock.readLock().unlock();
1672  }
1673  }
1674 
1686  public int queryNumFileChunks(long fileID) throws KeywordSearchModuleException, NoOpenCoreException {
1687  currentCoreLock.readLock().lock();
1688  try {
1689  if (null == currentCollection) {
1690  throw new NoOpenCoreException();
1691  }
1692  try {
1693  return currentCollection.queryTotalNumFileChunks(fileID);
1694  } catch (Exception ex) {
1695  // intentional "catch all" as Solr is known to throw all kinds of Runtime exceptions
1696  throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.queryNumFileChunks.exception.msg"), ex);
1697  }
1698  } finally {
1699  currentCoreLock.readLock().unlock();
1700  }
1701  }
1702 
1713  public QueryResponse query(SolrQuery sq) throws KeywordSearchModuleException, NoOpenCoreException, IOException {
1714  currentCoreLock.readLock().lock();
1715  try {
1716  if (null == currentCollection) {
1717  throw new NoOpenCoreException();
1718  }
1719  try {
1720  return currentCollection.query(sq);
1721  } catch (Exception ex) {
1722  // intentional "catch all" as Solr is known to throw all kinds of Runtime exceptions
1723  logger.log(Level.SEVERE, "Solr query failed: " + sq.getQuery(), ex); //NON-NLS
1724  throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.query.exception.msg", sq.getQuery()), ex);
1725  }
1726  } finally {
1727  currentCoreLock.readLock().unlock();
1728  }
1729  }
1730 
1742  public QueryResponse query(SolrQuery sq, SolrRequest.METHOD method) throws KeywordSearchModuleException, NoOpenCoreException {
1743  currentCoreLock.readLock().lock();
1744  try {
1745  if (null == currentCollection) {
1746  throw new NoOpenCoreException();
1747  }
1748  try {
1749  return currentCollection.query(sq, method);
1750  } catch (Exception ex) {
1751  // intentional "catch all" as Solr is known to throw all kinds of Runtime exceptions
1752  logger.log(Level.SEVERE, "Solr query failed: " + sq.getQuery(), ex); //NON-NLS
1753  throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.query2.exception.msg", sq.getQuery()), ex);
1754  }
1755  } finally {
1756  currentCoreLock.readLock().unlock();
1757  }
1758  }
1759 
1770  public TermsResponse queryTerms(SolrQuery sq) throws KeywordSearchModuleException, NoOpenCoreException {
1771  currentCoreLock.readLock().lock();
1772  try {
1773  if (null == currentCollection) {
1774  throw new NoOpenCoreException();
1775  }
1776  try {
1777  return currentCollection.queryTerms(sq);
1778  } catch (Exception ex) {
1779  // intentional "catch all" as Solr is known to throw all kinds of Runtime exceptions
1780  logger.log(Level.SEVERE, "Solr terms query failed: " + sq.getQuery(), ex); //NON-NLS
1781  throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.queryTerms.exception.msg", sq.getQuery()), ex);
1782  }
1783  } finally {
1784  currentCoreLock.readLock().unlock();
1785  }
1786  }
1787 
1795  void deleteDataSource(Long dataSourceId) throws IOException, KeywordSearchModuleException, NoOpenCoreException, SolrServerException {
1796  try {
1797  currentCoreLock.writeLock().lock();
1798  if (null == currentCollection) {
1799  throw new NoOpenCoreException();
1800  }
1801  currentCollection.deleteDataSource(dataSourceId);
1802  currentCollection.commit();
1803  } finally {
1804  currentCoreLock.writeLock().unlock();
1805  }
1806  }
1807 
1816  @NbBundle.Messages({
1817  "Server.getAllTerms.error=Extraction of all unique Solr terms failed:"})
1818  void extractAllTermsForDataSource(Path outputFile, ReportProgressPanel progressPanel) throws KeywordSearchModuleException, NoOpenCoreException {
1819  try {
1820  currentCoreLock.writeLock().lock();
1821  if (null == currentCollection) {
1822  throw new NoOpenCoreException();
1823  }
1824  try {
1825  currentCollection.extractAllTermsForDataSource(outputFile, progressPanel);
1826  } catch (Exception ex) {
1827  // intentional "catch all" as Solr is known to throw all kinds of Runtime exceptions
1828  logger.log(Level.SEVERE, "Extraction of all unique Solr terms failed: ", ex); //NON-NLS
1829  throw new KeywordSearchModuleException(Bundle.Server_getAllTerms_error(), ex);
1830  }
1831  } finally {
1832  currentCoreLock.writeLock().unlock();
1833  }
1834  }
1835 
1845  public String getSolrContent(final Content content) throws NoOpenCoreException {
1846  currentCoreLock.readLock().lock();
1847  try {
1848  if (null == currentCollection) {
1849  throw new NoOpenCoreException();
1850  }
1851  return currentCollection.getSolrContent(content.getId(), 0);
1852  } finally {
1853  currentCoreLock.readLock().unlock();
1854  }
1855  }
1856 
1869  public String getSolrContent(final Content content, int chunkID) throws NoOpenCoreException {
1870  currentCoreLock.readLock().lock();
1871  try {
1872  if (null == currentCollection) {
1873  throw new NoOpenCoreException();
1874  }
1875  return currentCollection.getSolrContent(content.getId(), chunkID);
1876  } finally {
1877  currentCoreLock.readLock().unlock();
1878  }
1879  }
1880 
1890  public String getSolrContent(final long objectID) throws NoOpenCoreException {
1891  currentCoreLock.readLock().lock();
1892  try {
1893  if (null == currentCollection) {
1894  throw new NoOpenCoreException();
1895  }
1896  return currentCollection.getSolrContent(objectID, 0);
1897  } finally {
1898  currentCoreLock.readLock().unlock();
1899  }
1900  }
1901 
1912  public String getSolrContent(final long objectID, final int chunkID) throws NoOpenCoreException {
1913  currentCoreLock.readLock().lock();
1914  try {
1915  if (null == currentCollection) {
1916  throw new NoOpenCoreException();
1917  }
1918  return currentCollection.getSolrContent(objectID, chunkID);
1919  } finally {
1920  currentCoreLock.readLock().unlock();
1921  }
1922  }
1923 
1933  public static String getChunkIdString(long parentID, int childID) {
1934  return Long.toString(parentID) + Server.CHUNK_ID_SEPARATOR + Integer.toString(childID);
1935  }
1936 
1943  private void connectToEmbeddedSolrServer() throws SolrServerException, IOException {
1944  TimingMetric metric = HealthMonitor.getTimingMetric("Solr: Connectivity check");
1945  CoreAdminRequest.getStatus(null, localSolrServer);
1947  }
1948 
1960  void connectToSolrServer(String host, String port) throws SolrServerException, IOException {
1961  try (HttpSolrClient solrServer = getSolrClient("http://" + host + ":" + port + "/solr")) {
1962  connectToSolrServer(solrServer);
1963  }
1964  }
1965 
1975  private void connectToSolrServer(HttpSolrClient solrServer) throws SolrServerException, IOException {
1976  TimingMetric metric = HealthMonitor.getTimingMetric("Solr: Connectivity check");
1977  CollectionAdminRequest.ClusterStatus statusRequest = CollectionAdminRequest.getClusterStatus();
1978  CollectionAdminResponse statusResponse = statusRequest.process(solrServer);
1979  int statusCode = Integer.valueOf(((NamedList) statusResponse.getResponse().get("responseHeader")).get("status").toString());
1980  if (statusCode != 0) {
1981  logger.log(Level.WARNING, "Could not connect to Solr server "); //NON-NLS
1982  } else {
1983  logger.log(Level.INFO, "Connected to Solr server "); //NON-NLS
1984  }
1986  }
1987 
1988  private List<String> getSolrServerList(String host, String port) throws KeywordSearchModuleException {
1989  HttpSolrClient solrServer = getSolrClient("http://" + host + ":" + port + "/solr");
1990  return getSolrServerList(solrServer);
1991  }
1992 
1993  private List<String> getSolrServerList(HttpSolrClient solrServer) throws KeywordSearchModuleException {
1994 
1995  try {
1996  CollectionAdminRequest.ClusterStatus statusRequest = CollectionAdminRequest.getClusterStatus();
1997  CollectionAdminResponse statusResponse;
1998  try {
1999  statusResponse = statusRequest.process(solrServer);
2000  } catch (RemoteSolrException ex) {
2001  // collection doesn't exist
2002  return Collections.emptyList();
2003  }
2004 
2005  if (statusResponse == null) {
2006  return Collections.emptyList();
2007  }
2008 
2009  NamedList<?> error = (NamedList) statusResponse.getResponse().get("error");
2010  if (error != null) {
2011  return Collections.emptyList();
2012  }
2013 
2014  NamedList<?> cluster = (NamedList) statusResponse.getResponse().get("cluster");
2015  @SuppressWarnings("unchecked")
2016  List<String> liveNodes = (ArrayList) cluster.get("live_nodes");
2017 
2018  if (liveNodes != null) {
2019  liveNodes = liveNodes.stream()
2020  .map(serverStr -> serverStr.endsWith("_solr")
2021  ? serverStr.substring(0, serverStr.length() - "_solr".length())
2022  : serverStr)
2023  .collect(Collectors.toList());
2024  }
2025  return liveNodes;
2026  } catch (Exception ex) {
2027  // intentional "catch all" as Solr is known to throw all kinds of Runtime exceptions
2028  throw new KeywordSearchModuleException(
2029  NbBundle.getMessage(this.getClass(), "Server.serverList.exception.msg", solrServer.getBaseURL()));
2030  }
2031  }
2032 
2033  class Collection {
2034 
2035  // handle to the collection in Solr
2036  private final String name;
2037 
2038  private final CaseType caseType;
2039 
2040  private final Index textIndex;
2041 
2042  // We use different Solr clients for different operations. HttpSolrClient is geared towards query performance.
2043  // ConcurrentUpdateSolrClient is geared towards batching solr documents for better indexing throughput. We
2044  // have implemented our own batching algorithm so we will probably not use ConcurrentUpdateSolrClient.
2045  // CloudSolrClient is geared towards SolrCloud deployments. These are only good for collection-specific operations.
2046  private HttpSolrClient queryClient;
2047  private SolrClient indexingClient;
2048 
2049  private final int maxBufferSize;
2050  private final List<SolrInputDocument> buffer;
2051  private final Object bufferLock;
2052 
2053  /* (JIRA-7521) Sometimes we get into a situation where Solr server is no longer able to index new data.
2054  * Typically main reason for this is Solr running out of memory. In this case we will stop trying to send new
2055  * data to Solr (for this collection) after certain number of consecutive batches have failed. */
2056  private static final int MAX_NUM_CONSECUTIVE_FAILURES = 5;
2057  private AtomicInteger numConsecutiveFailures = new AtomicInteger(0);
2058  private AtomicBoolean skipIndexing = new AtomicBoolean(false);
2059 
2060  private final ScheduledThreadPoolExecutor periodicTasksExecutor;
2061  private static final long PERIODIC_BATCH_SEND_INTERVAL_MINUTES = 10;
2062  private static final int NUM_BATCH_UPDATE_RETRIES = 10;
2063  private static final long SLEEP_BETWEEN_RETRIES_MS = 10000; // 10 seconds
2064 
2065  private Collection(String name, Case theCase, Index index) throws TimeoutException, InterruptedException, KeywordSearchModuleException {
2066  this.name = name;
2067  this.caseType = theCase.getCaseType();
2068  this.textIndex = index;
2069  bufferLock = new Object();
2070 
2071  if (caseType == CaseType.SINGLE_USER_CASE) {
2072  // get SolrJ client
2073  queryClient = getSolrClient("http://localhost:" + localSolrServerPort + "/solr/" + name); // HttpClient
2074  indexingClient = getSolrClient("http://localhost:" + localSolrServerPort + "/solr/" + name); // HttpClient
2075  } else {
2076  // read Solr connection info from user preferences, unless "solrserver.txt" is present
2077  queryClient = configureMultiUserConnection(theCase, index, name);
2078 
2079  // for MU cases, use CloudSolrClient for indexing. Indexing is only supported for Solr 8.
2080  if (IndexFinder.getCurrentSolrVersion().equals(index.getSolrVersion())) {
2081  IndexingServerProperties properties = getMultiUserServerProperties(theCase.getCaseDirectory());
2082  indexingClient = getCloudSolrClient(properties.getHost(), properties.getPort(), name); // CloudClient
2083  } else {
2084  indexingClient = configureMultiUserConnection(theCase, index, name); // HttpClient
2085  }
2086  }
2087 
2088  // document batching
2089  maxBufferSize = org.sleuthkit.autopsy.keywordsearch.UserPreferences.getDocumentsQueueSize();
2090  logger.log(Level.INFO, "Using Solr document queue size = {0}", maxBufferSize); //NON-NLS
2091  buffer = new ArrayList<>(maxBufferSize);
2092  periodicTasksExecutor = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat("periodic-batched-document-task-%d").build()); //NON-NLS
2093  periodicTasksExecutor.scheduleWithFixedDelay(new SendBatchedDocumentsTask(), PERIODIC_BATCH_SEND_INTERVAL_MINUTES, PERIODIC_BATCH_SEND_INTERVAL_MINUTES, TimeUnit.MINUTES);
2094  }
2095 
2102  private final class SendBatchedDocumentsTask implements Runnable {
2103 
2104  @Override
2105  public void run() {
2106 
2107  if (skipIndexing.get()) {
2108  return;
2109  }
2110 
2111  List<SolrInputDocument> clone;
2112  synchronized (bufferLock) {
2113 
2114  if (buffer.isEmpty()) {
2115  return;
2116  }
2117 
2118  // Buffer is full. Make a clone and release the lock, so that we don't
2119  // hold other ingest threads
2120  clone = buffer.stream().collect(toList());
2121  buffer.clear();
2122  }
2123 
2124  try {
2125  // send the cloned list to Solr
2126  sendBufferedDocs(clone);
2127  } catch (KeywordSearchModuleException ex) {
2128  logger.log(Level.SEVERE, "Periodic batched document update failed", ex); //NON-NLS
2129  }
2130  }
2131  }
2132 
2138  String getName() {
2139  return name;
2140  }
2141 
2142  private Index getIndexInfo() {
2143  return this.textIndex;
2144  }
2145 
2146  private QueryResponse query(SolrQuery sq) throws SolrServerException, IOException {
2147  return queryClient.query(sq);
2148  }
2149 
2150  private NamedList<Object> request(SolrRequest<?> request) throws SolrServerException, RemoteSolrException {
2151  try {
2152  return queryClient.request(request);
2153  } catch (Exception e) {
2154  // intentional "catch all" as Solr is known to throw all kinds of Runtime exceptions
2155  logger.log(Level.WARNING, "Could not issue Solr request. ", e); //NON-NLS
2156  throw new SolrServerException(
2157  NbBundle.getMessage(this.getClass(), "Server.request.exception.exception.msg"), e);
2158  }
2159 
2160  }
2161 
2162  private QueryResponse query(SolrQuery sq, SolrRequest.METHOD method) throws SolrServerException, IOException {
2163  return queryClient.query(sq, method);
2164  }
2165 
2166  private TermsResponse queryTerms(SolrQuery sq) throws SolrServerException, IOException {
2167  QueryResponse qres = queryClient.query(sq);
2168  return qres.getTermsResponse();
2169  }
2170 
2171  private void commit() throws SolrServerException {
2172  List<SolrInputDocument> clone;
2173  synchronized (bufferLock) {
2174  // Make a clone and release the lock, so that we don't
2175  // hold other ingest threads
2176  clone = buffer.stream().collect(toList());
2177  buffer.clear();
2178  }
2179 
2180  try {
2181  sendBufferedDocs(clone);
2182  } catch (KeywordSearchModuleException ex) {
2183  throw new SolrServerException(NbBundle.getMessage(this.getClass(), "Server.commit.exception.msg"), ex);
2184  }
2185 
2186  try {
2187  //commit and block
2188  indexingClient.commit(true, true);
2189  } catch (Exception e) {
2190  // intentional "catch all" as Solr is known to throw all kinds of Runtime exceptions
2191  logger.log(Level.WARNING, "Could not commit index. ", e); //NON-NLS
2192  throw new SolrServerException(NbBundle.getMessage(this.getClass(), "Server.commit.exception.msg"), e);
2193  }
2194  }
2195 
2196  private void deleteDataSource(Long dsObjId) throws IOException, SolrServerException {
2197  String dataSourceId = Long.toString(dsObjId);
2198  String deleteQuery = "image_id:" + dataSourceId;
2199 
2200  queryClient.deleteByQuery(deleteQuery);
2201  }
2202 
2214  @NbBundle.Messages({
2215  "# {0} - Number of extracted terms",
2216  "ExtractAllTermsReport.numberExtractedTerms=Extracted {0} terms..."
2217  })
2218  private void extractAllTermsForDataSource(Path outputFile, ReportProgressPanel progressPanel) throws IOException, SolrServerException, NoCurrentCaseException, KeywordSearchModuleException {
2219 
2220  Files.deleteIfExists(outputFile);
2221  OpenOption[] options = new OpenOption[] { java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND };
2222 
2223  // step through the terms
2224  int termStep = 1000;
2225  long numExtractedTerms = 0;
2226  String firstTerm = "";
2227  while (true) {
2228  SolrQuery query = new SolrQuery();
2229  query.setRequestHandler("/terms");
2230  query.setTerms(true);
2231  query.setTermsLimit(termStep);
2232  query.setTermsLower(firstTerm);
2233  query.setTermsLowerInclusive(false);
2234 
2235  // Returned terms sorted by "index" order, which is the fastest way. Per Solr documentation:
2236  // "Retrieving terms in index order is very fast since the implementation directly uses Lucene’s TermEnum to iterate over the term dictionary."
2237  // All other sort criteria return very inconsistent and overlapping resuts.
2238  query.setTermsSortString("index");
2239 
2240  // "text" field is the schema field that we populate with (lowercased) terms
2241  query.addTermsField(Server.Schema.TEXT.toString());
2242  query.setTermsMinCount(0);
2243 
2244  // Unfortunatelly Solr "terms queries" do not support any filtering so we can't filter by data source this way.
2245  // query.addFilterQuery(Server.Schema.IMAGE_ID.toString() + ":" + dataSourceId);
2246 
2247  QueryRequest request = new QueryRequest(query);
2248  TermsResponse response = request.process(queryClient).getTermsResponse();
2249  List<Term> terms = response.getTerms(Server.Schema.TEXT.toString());
2250 
2251  if (terms == null || terms.isEmpty()) {
2252  numExtractedTerms += terms.size();
2253  progressPanel.updateStatusLabel(Bundle.ExtractAllTermsReport_numberExtractedTerms(numExtractedTerms));
2254  break;
2255  }
2256 
2257  // set the first term for the next query
2258  firstTerm = terms.get(terms.size()-1).getTerm();
2259 
2260  List<String> listTerms = terms.stream().map(Term::getTerm).collect(Collectors.toList());
2261  Files.write(outputFile, listTerms, options);
2262 
2263  numExtractedTerms += termStep;
2264  progressPanel.updateStatusLabel(Bundle.ExtractAllTermsReport_numberExtractedTerms(numExtractedTerms));
2265  }
2266  }
2267 
2276  void addDocument(SolrInputDocument doc) throws KeywordSearchModuleException {
2277 
2278  if (skipIndexing.get()) {
2279  return;
2280  }
2281 
2282  List<SolrInputDocument> clone;
2283  synchronized (bufferLock) {
2284  buffer.add(doc);
2285  // buffer documents if the buffer is not full
2286  if (buffer.size() < maxBufferSize) {
2287  return;
2288  }
2289 
2290  // Buffer is full. Make a clone and release the lock, so that we don't
2291  // hold other ingest threads
2292  clone = buffer.stream().collect(toList());
2293  buffer.clear();
2294  }
2295 
2296  // send the cloned list to Solr
2297  sendBufferedDocs(clone);
2298  }
2299 
2307  @NbBundle.Messages({
2308  "Collection.unableToIndexData.error=Unable to add data to text index. All future text indexing for the current case will be skipped.",
2309 
2310  })
2311  private void sendBufferedDocs(List<SolrInputDocument> docBuffer) throws KeywordSearchModuleException {
2312 
2313  if (docBuffer.isEmpty()) {
2314  return;
2315  }
2316 
2317  try {
2318  boolean success = true;
2319  for (int reTryAttempt = 0; reTryAttempt < NUM_BATCH_UPDATE_RETRIES; reTryAttempt++) {
2320  try {
2321  success = true;
2322  indexingClient.add(docBuffer);
2323  } catch (Exception ex) {
2324  success = false;
2325  if (reTryAttempt < NUM_BATCH_UPDATE_RETRIES - 1) {
2326  logger.log(Level.WARNING, "Unable to send document batch to Solr. Re-trying...", ex); //NON-NLS
2327  try {
2328  Thread.sleep(SLEEP_BETWEEN_RETRIES_MS);
2329  } catch (InterruptedException ignore) {
2330  throw new KeywordSearchModuleException(
2331  NbBundle.getMessage(this.getClass(), "Server.addDocBatch.exception.msg"), ex); //NON-NLS
2332  }
2333  }
2334  }
2335  if (success) {
2336  numConsecutiveFailures.set(0);
2337  if (reTryAttempt > 0) {
2338  logger.log(Level.INFO, "Batch update suceeded after {0} re-try", reTryAttempt); //NON-NLS
2339  }
2340  return;
2341  }
2342  }
2343  // if we are here, it means all re-try attempts failed
2344  logger.log(Level.SEVERE, "Unable to send document batch to Solr. All re-try attempts failed!"); //NON-NLS
2345  throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.addDocBatch.exception.msg")); //NON-NLS
2346  } catch (Exception ex) {
2347  // Solr throws a lot of unexpected exception types
2348  numConsecutiveFailures.incrementAndGet();
2349  logger.log(Level.SEVERE, "Could not add batched documents to index", ex); //NON-NLS
2350 
2351  // display message to user that that a document batch is missing from the index
2352  MessageNotifyUtil.Notify.error(
2353  NbBundle.getMessage(this.getClass(), "Server.addDocBatch.exception.msg"),
2354  NbBundle.getMessage(this.getClass(), "Server.addDocBatch.exception.msg"));
2355  throw new KeywordSearchModuleException(
2356  NbBundle.getMessage(this.getClass(), "Server.addDocBatch.exception.msg"), ex); //NON-NLS
2357  } finally {
2358  if (numConsecutiveFailures.get() >= MAX_NUM_CONSECUTIVE_FAILURES) {
2359  // skip all future indexing
2360  skipIndexing.set(true);
2361  logger.log(Level.SEVERE, "Unable to add data to text index. All future text indexing for the current case will be skipped!"); //NON-NLS
2362 
2363  // display message to user that no more data will be added to the index
2364  MessageNotifyUtil.Notify.error(
2365  NbBundle.getMessage(this.getClass(), "Server.addDocBatch.exception.msg"),
2366  Bundle.Collection_unableToIndexData_error());
2367  if (RuntimeProperties.runningWithGUI()) {
2368  MessageNotifyUtil.Message.error(Bundle.Collection_unableToIndexData_error());
2369  }
2370  }
2371  docBuffer.clear();
2372  }
2373  }
2374 
2385  private String getSolrContent(long contentID, int chunkID) {
2386  final SolrQuery q = new SolrQuery();
2387  q.setQuery("*:*");
2388  String filterQuery = Schema.ID.toString() + ":" + KeywordSearchUtil.escapeLuceneQuery(Long.toString(contentID));
2389  if (chunkID != 0) {
2390  filterQuery = filterQuery + Server.CHUNK_ID_SEPARATOR + chunkID;
2391  }
2392  q.addFilterQuery(filterQuery);
2393  q.setFields(Schema.TEXT.toString());
2394  try {
2395  // Get the first result.
2396  SolrDocumentList solrDocuments = queryClient.query(q).getResults();
2397 
2398  if (!solrDocuments.isEmpty()) {
2399  SolrDocument solrDocument = solrDocuments.get(0);
2400  if (solrDocument != null) {
2401  java.util.Collection<Object> fieldValues = solrDocument.getFieldValues(Schema.TEXT.toString());
2402  if (fieldValues.size() == 1) // The indexed text field for artifacts will only have a single value.
2403  {
2404  return fieldValues.toArray(new String[0])[0];
2405  } else // The indexed text for files has 2 values, the file name and the file content.
2406  // We return the file content value.
2407  {
2408  return fieldValues.toArray(new String[0])[1];
2409  }
2410  }
2411  }
2412  } catch (Exception ex) {
2413  // intentional "catch all" as Solr is known to throw all kinds of Runtime exceptions
2414  logger.log(Level.SEVERE, "Error getting content from Solr. Solr document id " + contentID + ", chunk id " + chunkID + ", query: " + filterQuery, ex); //NON-NLS
2415  return null;
2416  }
2417 
2418  return null;
2419  }
2420 
2421  synchronized void close() throws KeywordSearchModuleException {
2422  try {
2423 
2424  // stop the periodic batch update task. If the task is already running,
2425  // allow it to finish.
2426  ThreadUtils.shutDownTaskExecutor(periodicTasksExecutor);
2427 
2428  // We only unload cores for "single-user" cases.
2429  if (this.caseType == CaseType.MULTI_USER_CASE) {
2430  return;
2431  }
2432 
2433  CoreAdminRequest.unloadCore(this.name, localSolrServer);
2434  } catch (Exception ex) {
2435  // intentional "catch all" as Solr is known to throw all kinds of Runtime exceptions
2436  throw new KeywordSearchModuleException(
2437  NbBundle.getMessage(this.getClass(), "Server.close.exception.msg"), ex);
2438  } finally {
2439  try {
2440  queryClient.close();
2441  queryClient = null;
2442  indexingClient.close();
2443  indexingClient = null;
2444  } catch (IOException ex) {
2445  throw new KeywordSearchModuleException(
2446  NbBundle.getMessage(this.getClass(), "Server.close.exception.msg2"), ex);
2447  }
2448  }
2449  }
2450 
2460  private int queryNumIndexedFiles() throws SolrServerException, IOException {
2462  }
2463 
2473  private int queryNumIndexedChunks() throws SolrServerException, IOException {
2474  SolrQuery q = new SolrQuery(Server.Schema.ID + ":*" + Server.CHUNK_ID_SEPARATOR + "*");
2475  q.setRows(0);
2476  int numChunks = (int) query(q).getResults().getNumFound();
2477  return numChunks;
2478  }
2479 
2490  private int queryNumIndexedDocuments() throws SolrServerException, IOException {
2491  SolrQuery q = new SolrQuery("*:*");
2492  q.setRows(0);
2493  return (int) query(q).getResults().getNumFound();
2494  }
2495 
2505  private boolean queryIsIndexed(long contentID) throws SolrServerException, IOException {
2506  String id = KeywordSearchUtil.escapeLuceneQuery(Long.toString(contentID));
2507  SolrQuery q = new SolrQuery("*:*");
2508  q.addFilterQuery(Server.Schema.ID.toString() + ":" + id);
2509  //q.setFields(Server.Schema.ID.toString());
2510  q.setRows(0);
2511  return (int) query(q).getResults().getNumFound() != 0;
2512  }
2513 
2528  private int queryTotalNumFileChunks(long contentID) throws SolrServerException, IOException {
2529  final SolrQuery q = new SolrQuery();
2530  q.setQuery("*:*");
2531  String filterQuery = Schema.ID.toString() + ":" + KeywordSearchUtil.escapeLuceneQuery(Long.toString(contentID));
2532  q.addFilterQuery(filterQuery);
2533  q.setFields(Schema.NUM_CHUNKS.toString());
2534  try {
2535  SolrDocumentList solrDocuments = query(q).getResults();
2536  if (!solrDocuments.isEmpty()) {
2537  SolrDocument solrDocument = solrDocuments.get(0);
2538  if (solrDocument != null && !solrDocument.isEmpty()) {
2539  Object fieldValue = solrDocument.getFieldValue(Schema.NUM_CHUNKS.toString());
2540  return (Integer)fieldValue;
2541  }
2542  }
2543  } catch (Exception ex) {
2544  // intentional "catch all" as Solr is known to throw all kinds of Runtime exceptions
2545  logger.log(Level.SEVERE, "Error getting content from Solr. Solr document id " + contentID + ", query: " + filterQuery, ex); //NON-NLS
2546  return 0;
2547  }
2548  // File not indexed
2549  return 0;
2550  }
2551 
2563  int queryNumIndexedChunks(long contentID) throws SolrServerException, IOException {
2564  SolrQuery q = new SolrQuery(Server.Schema.ID + ":" + KeywordSearchUtil.escapeLuceneQuery(Long.toString(contentID)) + Server.CHUNK_ID_SEPARATOR + "*");
2565  q.setRows(0);
2566  int numChunks = (int) query(q).getResults().getNumFound();
2567  return numChunks;
2568  }
2569  }
2570 
2571  class ServerAction extends AbstractAction {
2572 
2573  private static final long serialVersionUID = 1L;
2574 
2575  @Override
2576  public void actionPerformed(ActionEvent e) {
2577  logger.log(Level.INFO, e.paramString().trim());
2578  }
2579  }
2580 
2584  class SolrServerNoPortException extends SocketException {
2585 
2586  private static final long serialVersionUID = 1L;
2587 
2591  private final int port;
2592 
2593  SolrServerNoPortException(int port) {
2594  super(NbBundle.getMessage(Server.class, "Server.solrServerNoPortException.msg", port,
2595  Server.PROPERTIES_CURRENT_SERVER_PORT));
2596  this.port = port;
2597  }
2598 
2599  int getPortNumber() {
2600  return port;
2601  }
2602  }
2603 }
static synchronized String getConfigSetting(String moduleName, String settingName)
String getSolrContent(final long objectID)
Definition: Server.java:1890
static synchronized long[] getJavaPIDs(String argsSubQuery)
final ReentrantReadWriteLock currentCoreLock
Definition: Server.java:275
ConcurrentUpdateSolrClient getConcurrentClient(String solrUrl)
Definition: Server.java:368
boolean queryIsFullyIndexed(long contentID)
Definition: Server.java:1651
static IndexingServerProperties getMultiUserServerProperties(String caseDirectory)
Definition: Server.java:1371
Collection openCore(Case theCase, Index index)
Definition: Server.java:1102
void connectToSolrServer(HttpSolrClient solrServer)
Definition: Server.java:1975
boolean coreIsLoaded(String coreName)
Definition: Server.java:1332
Process runLocalSolr8ControlCommand(List< String > solrArguments)
Definition: Server.java:497
List< String > getSolrServerList(String host, String port)
Definition: Server.java:1988
static final int NUM_COLLECTION_CREATION_RETRIES
Definition: Server.java:253
void backupCollection(String collectionName, String backupName, String pathToBackupLocation)
Definition: Server.java:1291
void configureSolrConnection(Case theCase, Index index)
Definition: Server.java:619
CloudSolrClient getCloudSolrClient(String host, String port, String defaultCollectionName)
Definition: Server.java:385
boolean collectionExists(String collectionName)
Definition: Server.java:1216
static TimingMetric getTimingMetric(String name)
static void selectSolrServerForCase(Path rootOutputDirectory, Path caseDirectoryPath)
Definition: Server.java:1447
void addServerActionListener(PropertyChangeListener l)
Definition: Server.java:412
static synchronized boolean settingExists(String moduleName, String settingName)
static final String HL_ANALYZE_CHARS_UNLIMITED
Definition: Server.java:229
String getSolrContent(final Content content)
Definition: Server.java:1845
boolean coreIndexFolderExists(String coreName)
Definition: Server.java:1349
void restoreCollection(String backupName, String restoreCollectionName, String pathToBackupLocation)
Definition: Server.java:1304
static synchronized void setConfigSetting(String moduleName, String settingName, String settingVal)
HttpSolrClient configureMultiUserConnection(Case theCase, Index index, String name)
Definition: Server.java:662
static final Charset DEFAULT_INDEXED_TEXT_CHARSET
default Charset to index text as
Definition: Server.java:238
InputStreamPrinterThread errorRedirectThread
Definition: Server.java:278
Process runLocalSolr4ControlCommand(List< String > solrArguments)
Definition: Server.java:547
List< String > getSolrServerList(HttpSolrClient solrServer)
Definition: Server.java:1993
QueryResponse query(SolrQuery sq)
Definition: Server.java:1713
static synchronized String getJavaPath()
static void submitTimingMetric(TimingMetric metric)
TermsResponse queryTerms(SolrQuery sq)
Definition: Server.java:1770
void createMultiUserCollection(String collectionName, int numShardsToUse)
Definition: Server.java:1261
String getSolrContent(final Content content, int chunkID)
Definition: Server.java:1869
String getSolrContent(final long objectID, final int chunkID)
Definition: Server.java:1912
HttpSolrClient getSolrClient(String solrUrl)
Definition: Server.java:359
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
static String getChunkIdString(long parentID, int childID)
Definition: Server.java:1933
static boolean deleteDir(File dirPath)
Definition: FileUtil.java:47
String toString(boolean preserveState)
QueryResponse query(SolrQuery sq, SolrRequest.METHOD method)
Definition: Server.java:1742

Copyright © 2012-2024 Sleuth Kit Labs. Generated on: Mon Mar 17 2025
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.