xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/index/Indexer.java (revision 059d25e52724652d1b991e7e17ffc72e2bf402fa)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * See LICENSE.txt included in this distribution for the specific
9  * language governing permissions and limitations under the License.
10  *
11  * When distributing Covered Code, include this CDDL HEADER in each
12  * file and include the License file at LICENSE.txt.
13  * If applicable, add the following below this CDDL HEADER, with the
14  * fields enclosed by brackets "[]" replaced with your own identifying
15  * information: Portions Copyright [yyyy] [name of copyright owner]
16  *
17  * CDDL HEADER END
18  */
19 
20 /*
21  * Copyright (c) 2005, 2022, Oracle and/or its affiliates. All rights reserved.
22  * Portions Copyright (c) 2011, Jens Elkner.
23  * Portions Copyright (c) 2017, 2020, Chris Fraire <cfraire@me.com>.
24  */
25 package org.opengrok.indexer.index;
26 
27 import java.io.BufferedReader;
28 import java.io.File;
29 import java.io.FileInputStream;
30 import java.io.IOException;
31 import java.io.InputStreamReader;
32 import java.io.PrintStream;
33 import java.io.UncheckedIOException;
34 import java.lang.reflect.Field;
35 import java.lang.reflect.InvocationTargetException;
36 import java.net.URI;
37 import java.net.URISyntaxException;
38 import java.nio.file.Files;
39 import java.nio.file.Path;
40 import java.nio.file.Paths;
41 import java.text.ParseException;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.Collection;
45 import java.util.Collections;
46 import java.util.HashMap;
47 import java.util.HashSet;
48 import java.util.List;
49 import java.util.Locale;
50 import java.util.Map;
51 import java.util.Map.Entry;
52 import java.util.Scanner;
53 import java.util.Set;
54 import java.util.TreeSet;
55 import java.util.concurrent.CountDownLatch;
56 import java.util.logging.Level;
57 import java.util.logging.Logger;
58 import java.util.stream.Collectors;
59 
60 import org.apache.commons.lang3.SystemUtils;
61 import org.opengrok.indexer.Info;
62 import org.opengrok.indexer.Metrics;
63 import org.opengrok.indexer.analysis.AnalyzerGuru;
64 import org.opengrok.indexer.analysis.AnalyzerGuruHelp;
65 import org.opengrok.indexer.analysis.Ctags;
66 import org.opengrok.indexer.configuration.CanonicalRootValidator;
67 import org.opengrok.indexer.configuration.CommandTimeoutType;
68 import org.opengrok.indexer.configuration.Configuration;
69 import org.opengrok.indexer.configuration.ConfigurationHelp;
70 import org.opengrok.indexer.configuration.LuceneLockName;
71 import org.opengrok.indexer.configuration.Project;
72 import org.opengrok.indexer.configuration.RuntimeEnvironment;
73 import org.opengrok.indexer.history.HistoryGuru;
74 import org.opengrok.indexer.history.RepositoriesHelp;
75 import org.opengrok.indexer.history.Repository;
76 import org.opengrok.indexer.history.RepositoryFactory;
77 import org.opengrok.indexer.history.RepositoryInfo;
78 import org.opengrok.indexer.logger.LoggerFactory;
79 import org.opengrok.indexer.logger.LoggerUtil;
80 import org.opengrok.indexer.util.CtagsUtil;
81 import org.opengrok.indexer.util.Executor;
82 import org.opengrok.indexer.util.HostUtil;
83 import org.opengrok.indexer.util.OptionParser;
84 import org.opengrok.indexer.util.Statistics;
85 
86 /**
87  * Creates and updates an inverted source index as well as generates Xref, file
88  * stats etc., if specified in the options.
89  *
90  * We shall use / as path delimiter in whole opengrok for uuids and paths
91  * from Windows systems, the path shall be converted when entering the index or web
92  * and converted back if needed* to access original file
93  *
94  * *Windows already supports opening /var/opengrok as C:\var\opengrok
95  */
96 @SuppressWarnings({"PMD.AvoidPrintStackTrace", "PMD.SystemPrintln"})
97 public final class Indexer {
98 
99     private static final Logger LOGGER = LoggerFactory.getLogger(Indexer.class);
100 
101     /* tunables for -r (history for remote repositories) */
102     private static final String ON = "on";
103     private static final String OFF = "off";
104     private static final String DIRBASED = "dirbased";
105     private static final String UIONLY = "uionly";
106 
107     //whole app uses this separator
108     public static final char PATH_SEPARATOR = '/';
109     public static final String PATH_SEPARATOR_STRING = Character.toString(PATH_SEPARATOR);
110 
111     private static final String HELP_OPT_1 = "--help";
112     private static final String HELP_OPT_2 = "-?";
113     private static final String HELP_OPT_3 = "-h";
114 
115     private static final Indexer indexer = new Indexer();
116     private static Configuration cfg = null;
117     private static boolean checkIndex = false;
118     private static boolean runIndex = true;
119     private static boolean optimizedChanged = false;
120     private static boolean addProjects = false;
121     private static boolean searchRepositories = false;
122     private static boolean bareConfig = false;
123     private static boolean awaitProfiler;
124 
125     private static boolean help;
126     private static String helpUsage;
127     private static HelpMode helpMode = HelpMode.DEFAULT;
128 
129     private static String configFilename = null;
130     private static int status = 0;
131 
132     private static final Set<String> repositories = new HashSet<>();
133     private static Set<String> searchPaths = new HashSet<>();
134     private static final HashSet<String> allowedSymlinks = new HashSet<>();
135     private static final HashSet<String> canonicalRoots = new HashSet<>();
136     private static final Set<String> defaultProjects = new TreeSet<>();
137     private static final HashSet<String> disabledRepositories = new HashSet<>();
138     private static RuntimeEnvironment env = null;
139     private static String webappURI = null;
140 
141     private static OptionParser optParser = null;
142     private static boolean verbose = false;
143 
144     private static final String[] ON_OFF = {ON, OFF};
145     private static final String[] REMOTE_REPO_CHOICES = {ON, OFF, DIRBASED, UIONLY};
146     private static final String[] LUCENE_LOCKS = {ON, OFF, "simple", "native"};
147     private static final String OPENGROK_JAR = "opengrok.jar";
148 
149     private static final int WEBAPP_CONNECT_TIMEOUT = 1000;  // in milliseconds
150 
getInstance()151     public static Indexer getInstance() {
152         return indexer;
153     }
154 
155     /**
156      * Program entry point.
157      *
158      * @param argv argument vector
159      */
160     @SuppressWarnings("PMD.UseStringBufferForStringAppends")
main(String[] argv)161     public static void main(String[] argv) {
162         Statistics stats = new Statistics(); //this won't count JVM creation though
163         boolean update = true;
164 
165         Executor.registerErrorHandler();
166         List<String> subFiles = RuntimeEnvironment.getInstance().getSubFiles();
167         Set<String> subFilesArgs = new HashSet<>();
168 
169         boolean createDict = false;
170 
171         try {
172             argv = parseOptions(argv);
173 
174             if (webappURI != null && !HostUtil.isReachable(webappURI, WEBAPP_CONNECT_TIMEOUT)) {
175                 System.err.println(webappURI + " is not reachable.");
176                 System.exit(1);
177             }
178 
179             /*
180              * Attend to disabledRepositories here in case exitWithHelp() will
181              * need to report about repos.
182              */
183             disabledRepositories.addAll(cfg.getDisabledRepositories());
184             cfg.setDisabledRepositories(disabledRepositories);
185             for (String repoName : disabledRepositories) {
186                 LOGGER.log(Level.FINEST, "Disabled {0}", repoName);
187             }
188 
189             if (help) {
190                 exitWithHelp();
191             }
192 
193             checkConfiguration();
194 
195             if (awaitProfiler) {
196                 pauseToAwaitProfiler();
197             }
198 
199             env = RuntimeEnvironment.getInstance();
200             env.setIndexer(true);
201 
202             // Complete the configuration of repository types.
203             List<Class<? extends Repository>> repositoryClasses = RepositoryFactory.getRepositoryClasses();
204             for (Class<? extends Repository> clazz : repositoryClasses) {
205                 // Set external repository binaries from System properties.
206                 try {
207                     Field f = clazz.getDeclaredField("CMD_PROPERTY_KEY");
208                     Object key = f.get(null);
209                     if (key != null) {
210                         cfg.setRepoCmd(clazz.getCanonicalName(),
211                                 System.getProperty(key.toString()));
212                     }
213                 } catch (Exception e) {
214                     // don't care
215                 }
216             }
217 
218             // Logging starts here.
219             if (verbose) {
220                 String fn = LoggerUtil.getFileHandlerPattern();
221                 if (fn != null) {
222                     System.out.println("Logging filehandler pattern: " + fn);
223                 }
224             }
225 
226             // automatically allow symlinks that are directly in source root
227             File sourceRootFile = new File(cfg.getSourceRoot());
228             File[] projectDirs = sourceRootFile.listFiles();
229             if (projectDirs != null) {
230                 for (File projectDir : projectDirs) {
231                     if (!projectDir.getCanonicalPath().equals(projectDir.getAbsolutePath())) {
232                         allowedSymlinks.add(projectDir.getAbsolutePath());
233                     }
234                 }
235             }
236 
237             allowedSymlinks.addAll(cfg.getAllowedSymlinks());
238             cfg.setAllowedSymlinks(allowedSymlinks);
239 
240             canonicalRoots.addAll(cfg.getCanonicalRoots());
241             cfg.setCanonicalRoots(canonicalRoots);
242 
243             // Assemble the unprocessed command line arguments (possibly a list of paths).
244             // This will be used to perform more fine-grained checking in invalidateRepositories()
245             // called from the setConfiguration() below.
246             for (String arg : argv) {
247                 String path = Paths.get(cfg.getSourceRoot(), arg).toString();
248                 subFilesArgs.add(path);
249             }
250 
251             // If a user used customizations for projects he perhaps just
252             // used the key value for project without a name but the code
253             // expects a name for the project. Therefore, we fill the name
254             // according to the project key which is the same.
255             for (Entry<String, Project> entry : cfg.getProjects().entrySet()) {
256                 if (entry.getValue().getName() == null) {
257                     entry.getValue().setName(entry.getKey());
258                 }
259             }
260 
261             // Check version of index(es) versus current Lucene version and exit
262             // with return code upon failure.
263             if (checkIndex) {
264                 if (cfg.getDataRoot() == null || cfg.getDataRoot().isEmpty()) {
265                     System.err.println("Need data root in configuration for index check (use -R)");
266                     System.exit(1);
267                 }
268 
269                 if (!IndexCheck.check(cfg, subFilesArgs)) {
270                     System.err.printf("Index check failed%n");
271                     System.err.print("You might want to remove " +
272                             (!subFilesArgs.isEmpty() ? "data for projects " + String.join(",", subFilesArgs) :
273                                     "all data") + " under the data root and reindex\n");
274                     System.exit(1);
275                 }
276 
277                 System.exit(0);
278             }
279 
280             // Set updated configuration in RuntimeEnvironment. This is called so that the tunables set
281             // via command line options are available.
282             env.setConfiguration(cfg, subFilesArgs, CommandTimeoutType.INDEXER);
283 
284             // Let repository types to add items to ignoredNames.
285             // This changes env so is called after the setConfiguration()
286             // call above.
287             RepositoryFactory.initializeIgnoredNames(env);
288 
289             if (bareConfig) {
290                 // Set updated configuration in RuntimeEnvironment.
291                 env.setConfiguration(cfg, subFilesArgs, CommandTimeoutType.INDEXER);
292 
293                 getInstance().sendToConfigHost(env, webappURI);
294                 writeConfigToFile(env, configFilename);
295                 System.exit(0);
296             }
297 
298             /*
299              * Add paths to directories under source root. If projects
300              * are enabled the path should correspond to a project because
301              * project path is necessary to correctly set index directory
302              * (otherwise the index files will end up in index data root
303              * directory and not per project data root directory).
304              * For the check we need to have 'env' already set.
305              */
306             for (String path : subFilesArgs) {
307                 String srcPath = env.getSourceRootPath();
308                 if (srcPath == null) {
309                     System.err.println("Error getting source root from environment. Exiting.");
310                     System.exit(1);
311                 }
312 
313                 path = path.substring(srcPath.length());
314                 if (env.hasProjects()) {
315                     // The paths need to correspond to a project.
316                     Project project;
317                     if ((project = Project.getProject(path)) != null) {
318                         subFiles.add(path);
319                         List<RepositoryInfo> repoList = env.getProjectRepositoriesMap().get(project);
320                         if (repoList != null) {
321                             repositories.addAll(repoList.
322                                     stream().map(RepositoryInfo::getDirectoryNameRelative).collect(Collectors.toSet()));
323                         }
324                     } else {
325                         System.err.println("The path " + path
326                                 + " does not correspond to a project");
327                     }
328                 } else {
329                     subFiles.add(path);
330                 }
331             }
332 
333             if (!subFilesArgs.isEmpty() && subFiles.isEmpty()) {
334                 System.err.println("None of the paths were added, exiting");
335                 System.exit(1);
336             }
337 
338             if (!subFiles.isEmpty() && configFilename != null) {
339                 LOGGER.log(Level.WARNING, "The collection of entries to process is non empty ({0}), seems like " +
340                         "the intention is to perform per project reindex, however the -W option is used. " +
341                         "This will likely not work.", subFiles);
342             }
343 
344             Metrics.updateSubFiles(subFiles);
345 
346             // If the webapp is running with a config that does not contain
347             // 'projectsEnabled' property (case of upgrade or transition
348             // from project-less config to one with projects), set the property
349             // so that the 'project/indexed' messages
350             // emitted during indexing do not cause validation error.
351             if (addProjects && webappURI != null) {
352                 try {
353                     IndexerUtil.enableProjects(webappURI);
354                 } catch (Exception e) {
355                     LOGGER.log(Level.SEVERE, String.format("Couldn't notify the webapp on %s.", webappURI), e);
356                     System.err.printf("Couldn't notify the webapp on %s: %s.%n", webappURI, e.getLocalizedMessage());
357                 }
358             }
359 
360             LOGGER.log(Level.INFO, "Indexer version {0} ({1}) running on Java {2}",
361                     new Object[]{Info.getVersion(), Info.getRevision(), System.getProperty("java.version")});
362 
363             // Create history cache first.
364             if (searchRepositories) {
365                 if (searchPaths.isEmpty()) {
366                     String[] dirs = env.getSourceRootFile().
367                             list((f, name) -> f.isDirectory() && env.getPathAccepter().accept(f));
368                     if (dirs != null) {
369                         searchPaths.addAll(Arrays.asList(dirs));
370                     }
371                 }
372 
373                 searchPaths = searchPaths.stream().
374                         map(t -> Paths.get(env.getSourceRootPath(), t).toString()).
375                         collect(Collectors.toSet());
376             }
377             getInstance().prepareIndexer(env, searchPaths, addProjects,
378                     createDict, runIndex, subFiles, new ArrayList<>(repositories));
379 
380             // Set updated configuration in RuntimeEnvironment. This is called so that repositories discovered
381             // in prepareIndexer() are stored in the Configuration used by RuntimeEnvironment.
382             env.setConfiguration(cfg, subFilesArgs, CommandTimeoutType.INDEXER);
383 
384             // prepareIndexer() populated the list of projects so now default projects can be set.
385             env.setDefaultProjectsFromNames(defaultProjects);
386 
387             // And now index it all.
388             if (runIndex || (optimizedChanged && env.isOptimizeDatabase())) {
389                 IndexChangedListener progress = new DefaultIndexChangedListener();
390                 getInstance().doIndexerExecution(update, subFiles, progress);
391             }
392 
393             writeConfigToFile(env, configFilename);
394 
395             // Finally, send new configuration to the web application in the case of full reindex.
396             if (webappURI != null && subFiles.isEmpty()) {
397                 getInstance().sendToConfigHost(env, webappURI);
398             }
399 
400             env.getIndexerParallelizer().bounce();
401         } catch (ParseException e) {
402             System.err.println("** " + e.getMessage());
403             System.exit(1);
404         } catch (IndexerException ex) {
405             LOGGER.log(Level.SEVERE, "Exception running indexer", ex);
406             System.err.println("Exception: " + ex.getLocalizedMessage());
407             System.err.println(optParser.getUsage());
408             System.exit(1);
409         } catch (Throwable e) {
410             LOGGER.log(Level.SEVERE, "Unexpected Exception", e);
411             System.err.println("Exception: " + e.getLocalizedMessage());
412             System.exit(1);
413         } finally {
414             stats.report(LOGGER, "Indexer finished", "indexer.total");
415         }
416     }
417 
checkConfiguration()418     private static void checkConfiguration() {
419         if (bareConfig && (env.getConfigURI() == null || env.getConfigURI().isEmpty())) {
420             die("Missing webappURI setting");
421         }
422 
423         if (!repositories.isEmpty() && !cfg.isHistoryEnabled()) {
424             die("Repositories were specified; history is off however");
425         }
426 
427         try {
428             cfg.checkConfiguration();
429         } catch (Configuration.ConfigurationException e) {
430             die(e.getMessage());
431         }
432     }
433 
434     /**
435      * Parse OpenGrok Indexer options
436      * This method was created so that it would be easier to write unit
437      * tests against the Indexer option parsing mechanism.
438      *
439      * Limit usage lines to {@link org.opengrok.indexer.util.OptionParser.Option#MAX_DESCRIPTION_LINE_LENGTH}
440      * characters for concise formatting.
441      *
442      * @param argv the command line arguments
443      * @return array of remaining non option arguments
444      * @throws ParseException if parsing failed
445      */
parseOptions(String[] argv)446     public static String[] parseOptions(String[] argv) throws ParseException {
447         final String[] usage = {HELP_OPT_1};
448 
449         if (argv.length == 0) {
450             argv = usage;  // will force usage output
451             status = 1; // with non-zero EXIT STATUS
452         }
453 
454         /*
455          * Pre-match any of the --help options so that some possible exception-generating args handlers (e.g. -R)
456          * can be short-circuited.
457          */
458         boolean preHelp = Arrays.stream(argv).anyMatch(s -> HELP_OPT_1.equals(s) ||
459                 HELP_OPT_2.equals(s) || HELP_OPT_3.equals(s));
460 
461         OptionParser configure = OptionParser.scan(parser ->
462                 parser.on("-R configPath").execute(cfgFile -> {
463             try {
464                 cfg = Configuration.read(new File((String) cfgFile));
465             } catch (IOException e) {
466                 if (!preHelp) {
467                     die(e.getMessage());
468                 } else {
469                     System.err.printf("Warning: failed to read -R %s%n", cfgFile);
470                 }
471             }
472         }));
473 
474         searchPaths.clear();
475 
476         optParser = OptionParser.execute(parser -> {
477             parser.setPrologue(String.format("%nUsage: java -jar %s [options] [subDir1 [...]]%n", OPENGROK_JAR));
478 
479             parser.on(HELP_OPT_3, HELP_OPT_2, HELP_OPT_1, "=[mode]",
480                     "With no mode specified, display this usage summary. Or specify a mode:",
481                     "  config - display configuration.xml examples.",
482                     "   ctags - display ctags command-line.",
483                     "    guru - display AnalyzerGuru details.",
484                     "   repos - display enabled repositories.").execute(v -> {
485                         help = true;
486                         helpUsage = parser.getUsage();
487                         String mode = (String) v;
488                         if (mode != null && !mode.isEmpty()) {
489                             try {
490                                 helpMode = HelpMode.valueOf(((String) v).toUpperCase(Locale.ROOT));
491                             } catch (IllegalArgumentException ex) {
492                                 die("mode '" + v + "' is not valid.");
493                             }
494                         }
495             });
496 
497             parser.on("--apiTimeout", "=number", Integer.class,
498                     "Set timeout for asynchronous API requests.").execute(v -> cfg.setApiTimeout((Integer) v));
499 
500             parser.on("--connectTimeout", "=number", Integer.class,
501                     "Set connect timeout. Used for API requests.").execute(v -> cfg.setConnectTimeout((Integer) v));
502 
503             parser.on(
504                 "-A (.ext|prefix.):(-|analyzer)", "--analyzer",
505                     "/(\\.\\w+|\\w+\\.):(-|[a-zA-Z_0-9.]+)/",
506                     "Associates files with the specified prefix or extension (case-",
507                     "insensitive) to be analyzed with the given analyzer, where 'analyzer'",
508                     "may be specified using a class name (case-sensitive e.g. RubyAnalyzer)",
509                     "or analyzer language name (case-sensitive e.g. C). Option may be",
510                     "repeated.",
511                     "  Ex: -A .foo:CAnalyzer",
512                     "      will use the C analyzer for all files ending with .FOO",
513                     "  Ex: -A bar.:Perl",
514                     "      will use the Perl analyzer for all files starting with",
515                     "      \"BAR\" (no full-stop)",
516                     "  Ex: -A .c:-",
517                     "      will disable specialized analyzers for all files ending with .c").
518                 execute(analyzerSpec -> {
519                     String[] arg = ((String) analyzerSpec).split(":");
520                     String fileSpec = arg[0];
521                     String analyzer = arg[1];
522                     configureFileAnalyzer(fileSpec, analyzer);
523                 }
524             );
525 
526             parser.on("-c", "--ctags", "=/path/to/ctags",
527                     "Path to Universal Ctags. Default is ctags in environment PATH.").execute(
528                             v -> cfg.setCtags((String) v));
529 
530             parser.on("--canonicalRoot", "=/path/",
531                     "Allow symlinks to canonical targets starting with the specified root",
532                     "without otherwise needing to specify -N,--symlink for such symlinks. A",
533                     "canonical root must end with a file separator. For security, a canonical",
534                     "root cannot be the root directory. Option may be repeated.").execute(v -> {
535                 String root = (String) v;
536                 String problem = CanonicalRootValidator.validate(root, "--canonicalRoot");
537                 if (problem != null) {
538                     die(problem);
539                 }
540                 canonicalRoots.add(root);
541             });
542 
543             parser.on("--checkIndex", "Check index, exit with 0 on success,",
544                     "with 1 on failure.").execute(v -> checkIndex = true);
545 
546             parser.on("-d", "--dataRoot", "=/path/to/data/root",
547                 "The directory where OpenGrok stores the generated data.").
548                 execute(drPath -> {
549                     File dataRoot = new File((String) drPath);
550                     if (!dataRoot.exists() && !dataRoot.mkdirs()) {
551                         die("Cannot create data root: " + dataRoot);
552                     }
553                     if (!dataRoot.isDirectory()) {
554                         die("Data root must be a directory");
555                     }
556                     try {
557                         cfg.setDataRoot(dataRoot.getCanonicalPath());
558                     } catch (IOException e) {
559                         die(e.getMessage());
560                     }
561                 }
562             );
563 
564             parser.on("--depth", "=number", Integer.class,
565                 "Scanning depth for repositories in directory structure relative to",
566                 "source root. Default is " + Configuration.defaultScanningDepth + ".").execute(depth ->
567                     cfg.setScanningDepth((Integer) depth));
568 
569             parser.on("--disableRepository", "=type_name",
570                     "Disables operation of an OpenGrok-supported repository. See also",
571                     "-h,--help repos. Option may be repeated.",
572                     "  Ex: --disableRepository git",
573                     "      will disable the GitRepository",
574                     "  Ex: --disableRepository MercurialRepository").execute(v -> {
575                 String repoType = (String) v;
576                 String repoSimpleType = RepositoryFactory.matchRepositoryByName(repoType);
577                 if (repoSimpleType == null) {
578                     System.err.printf("'--disableRepository %s' does not match a type and is ignored%n", v);
579                 } else {
580                     disabledRepositories.add(repoSimpleType);
581                 }
582             });
583 
584             parser.on("-e", "--economical",
585                     "To consume less disk space, OpenGrok will not generate and save",
586                     "hypertext cross-reference files but will generate on demand, which could",
587                     "be slightly slow.").execute(v -> cfg.setGenerateHtml(false));
588 
589             parser.on("-G", "--assignTags",
590                 "Assign commit tags to all entries in history for all repositories.").execute(v ->
591                     cfg.setTagsEnabled(true));
592 
593             // for backward compatibility
594             parser.on("-H", "Enable history.").execute(v -> cfg.setHistoryEnabled(true));
595 
596             parser.on("--historyBased", "=on|off", ON_OFF, Boolean.class,
597                             "If history based reindex is in effect, the set of files ",
598                             "changed/deleted since the last reindex is determined from history ",
599                             "of the repositories. This needs history, history cache and ",
600                             "projects to be enabled. This should be much faster than the ",
601                             "classic way of traversing the directory structure. ",
602                             "The default is on. If you need to e.g. index files untracked by ",
603                             "SCM, set this to off. Currently works only for Git.",
604                             "All repositories in a project need to support this in order ",
605                             "to be indexed using history.").
606                     execute(v -> cfg.setHistoryBasedReindex((Boolean) v));
607 
608             parser.on("--historyThreads", "=number", Integer.class,
609                     "The number of threads to use for history cache generation on repository level. ",
610                     "By default the number of threads will be set to the number of available CPUs.",
611                     "Assumes -H/--history.").execute(threadCount ->
612                     cfg.setHistoryParallelism((Integer) threadCount));
613 
614             parser.on("--historyFileThreads", "=number", Integer.class,
615                     "The number of threads to use for history cache generation ",
616                     "when dealing with individual files.",
617                     "By default the number of threads will be set to the number of available CPUs.",
618                     "Assumes -H/--history.").execute(threadCount ->
619                     cfg.setHistoryFileParallelism((Integer) threadCount));
620 
621             parser.on("-I", "--include", "=pattern",
622                     "Only files matching this pattern will be examined. Pattern supports",
623                     "wildcards (example: -I '*.java' -I '*.c'). Option may be repeated.").execute(
624                             pattern -> cfg.getIncludedNames().add((String) pattern));
625 
626             parser.on("-i", "--ignore", "=pattern",
627                     "Ignore matching files (prefixed with 'f:' or no prefix) or directories",
628                     "(prefixed with 'd:'). Pattern supports wildcards (example: -i '*.so'",
629                     "-i d:'test*'). Option may be repeated.").execute(pattern ->
630                     cfg.getIgnoredNames().add((String) pattern));
631 
632             parser.on("-l", "--lock", "=on|off|simple|native", LUCENE_LOCKS,
633                     "Set OpenGrok/Lucene locking mode of the Lucene database during index",
634                     "generation. \"on\" is an alias for \"simple\". Default is off.").execute(v -> {
635                 try {
636                     if (v != null) {
637                         String vuc = v.toString().toUpperCase(Locale.ROOT);
638                         cfg.setLuceneLocking(LuceneLockName.valueOf(vuc));
639                     }
640                 } catch (IllegalArgumentException e) {
641                     System.err.printf("`--lock %s' is invalid and ignored%n", v);
642                 }
643             });
644 
645             parser.on("--leadingWildCards", "=on|off", ON_OFF, Boolean.class,
646                 "Allow or disallow leading wildcards in a search. Default is on.").execute(v ->
647                     cfg.setAllowLeadingWildcard((Boolean) v));
648 
649             parser.on("-m", "--memory", "=number", Double.class,
650                     "Amount of memory (MB) that may be used for buffering added documents and",
651                     "deletions before they are flushed to the directory (default " +
652                             Configuration.defaultRamBufferSize + ").",
653                     "Please increase JVM heap accordingly too.").execute(memSize ->
654                     cfg.setRamBufferSize((Double) memSize));
655 
656             parser.on("--mandoc", "=/path/to/mandoc", "Path to mandoc(1) binary.")
657                     .execute(mandocPath -> cfg.setMandoc((String) mandocPath));
658 
659             parser.on("-N", "--symlink", "=/path/to/symlink",
660                     "Allow the symlink to be followed. Other symlinks targeting the same",
661                     "canonical target or canonical children will be allowed too. Option may",
662                     "be repeated. (By default only symlinks directly under the source root",
663                     "directory are allowed. See also --canonicalRoot)").execute(v ->
664                     allowedSymlinks.add((String) v));
665 
666             parser.on("-n", "--noIndex",
667                     "Do not generate indexes and other data (such as history cache and xref",
668                     "files), but process all other command line options.").execute(v ->
669                     runIndex = false);
670 
671             parser.on("--nestingMaximum", "=number", Integer.class,
672                     "Maximum depth of nested repositories. Default is 1.").execute(v ->
673                     cfg.setNestingMaximum((Integer) v));
674 
675             parser.on("-O", "--optimize", "=on|off", ON_OFF, Boolean.class,
676                     "Turn on/off the optimization of the index database as part of the",
677                     "indexing step. Default is on.").
678                 execute(v -> {
679                     boolean oldval = cfg.isOptimizeDatabase();
680                     cfg.setOptimizeDatabase((Boolean) v);
681                     if (oldval != cfg.isOptimizeDatabase()) {
682                         optimizedChanged = true;
683                     }
684                 }
685             );
686 
687             parser.on("-o", "--ctagOpts", "=path",
688                 "File with extra command line options for ctags.").
689                 execute(path -> {
690                     String CTagsExtraOptionsFile = (String) path;
691                     File CTagsFile = new File(CTagsExtraOptionsFile);
692                     if (!(CTagsFile.isFile() && CTagsFile.canRead())) {
693                         die("File '" + CTagsExtraOptionsFile + "' not found for the -o option");
694                     }
695                     System.err.println("INFO: file with extra "
696                         + "options for ctags: " + CTagsExtraOptionsFile);
697                     cfg.setCTagsExtraOptionsFile(CTagsExtraOptionsFile);
698                 }
699             );
700 
701             parser.on("-P", "--projects",
702                 "Generate a project for each top-level directory in source root.").execute(v -> {
703                 addProjects = true;
704                 cfg.setProjectsEnabled(true);
705             });
706 
707             parser.on("-p", "--defaultProject", "=path/to/default/project",
708                     "Path (relative to the source root) to a project that should be selected",
709                     "by default in the web application (when no other project is set either",
710                     "in a cookie or in parameter). Option may be repeated to specify several",
711                     "projects. Use the special value __all__ to indicate all projects.").execute(v ->
712                     defaultProjects.add((String) v));
713 
714             parser.on("--profiler", "Pause to await profiler or debugger.").
715                 execute(v -> awaitProfiler = true);
716 
717             parser.on("--progress",
718                     "Print per-project percentage progress information.").execute(v ->
719                     cfg.setPrintProgress(true));
720 
721             parser.on("-Q", "--quickScan",  "=on|off", ON_OFF, Boolean.class,
722                     "Turn on/off quick context scan. By default, only the first 1024KB of a",
723                     "file is scanned, and a link ('[..all..]') is inserted when the file is",
724                     "bigger. Activating this may slow the server down. (Note: this setting",
725                     "only affects the web application.) Default is on.").execute(v ->
726                     cfg.setQuickContextScan((Boolean) v));
727 
728             parser.on("-q", "--quiet",
729                     "Run as quietly as possible. Sets logging level to WARNING.").execute(v ->
730                     LoggerUtil.setBaseConsoleLogLevel(Level.WARNING));
731 
732             parser.on("-R /path/to/configuration",
733                 "Read configuration from the specified file.").execute(v -> {
734                 // Already handled above. This populates usage.
735             });
736 
737             parser.on("-r", "--remote", "=on|off|uionly|dirbased",
738                 REMOTE_REPO_CHOICES,
739                 "Specify support for remote SCM systems.",
740                 "      on - allow retrieval for remote SCM systems.",
741                 "     off - ignore SCM for remote systems.",
742                 "  uionly - support remote SCM for user interface only.",
743                 "dirbased - allow retrieval during history index only for repositories",
744                 "           which allow getting history for directories.").
745                 execute(v -> {
746                     String option = (String) v;
747                     if (option.equalsIgnoreCase(ON)) {
748                         cfg.setRemoteScmSupported(Configuration.RemoteSCM.ON);
749                     } else if (option.equalsIgnoreCase(OFF)) {
750                         cfg.setRemoteScmSupported(Configuration.RemoteSCM.OFF);
751                     } else if (option.equalsIgnoreCase(DIRBASED)) {
752                         cfg.setRemoteScmSupported(Configuration.RemoteSCM.DIRBASED);
753                     } else if (option.equalsIgnoreCase(UIONLY)) {
754                         cfg.setRemoteScmSupported(Configuration.RemoteSCM.UIONLY);
755                     }
756                 }
757             );
758 
759             parser.on("--renamedHistory", "=on|off", ON_OFF, Boolean.class,
760                 "Enable or disable generating history for renamed files.",
761                 "If set to on, makes history indexing slower for repositories",
762                 "with lots of renamed files. Default is off.").execute(v ->
763                     cfg.setHandleHistoryOfRenamedFiles((Boolean) v));
764 
765             parser.on("--repository", "=[path/to/repository|@file_with_paths]",
766                     "Path (relative to the source root) to a repository for generating",
767                     "history (if -H,--history is on). By default all discovered repositories",
768                     "are history-eligible; using --repository limits to only those specified.",
769                     "File containing paths can be specified via @path syntax.",
770                     "Option may be repeated.")
771                 .execute(v -> handlePathParameter(repositories, ((String) v).trim()));
772 
773             parser.on("-S", "--search", "=[path/to/repository|@file_with_paths]",
774                     "Search for source repositories under -s,--source, and add them. Path",
775                     "(relative to the source root) is optional. ",
776                     "File containing paths can be specified via @path syntax.",
777                     "Option may be repeated.")
778                 .execute(v -> {
779                         searchRepositories = true;
780                         String value = ((String) v).trim();
781                         if (!value.isEmpty()) {
782                             handlePathParameter(searchPaths, value);
783                         }
784                     });
785 
786             parser.on("-s", "--source", "=/path/to/source/root",
787                 "The root directory of the source tree.").
788                 execute(source -> {
789                     File sourceRoot = new File((String) source);
790                     if (!sourceRoot.isDirectory()) {
791                         die("Source root " + sourceRoot + " must be a directory");
792                     }
793                     try {
794                         cfg.setSourceRoot(sourceRoot.getCanonicalPath());
795                     } catch (IOException e) {
796                         die(e.getMessage());
797                     }
798                 }
799             );
800 
801             parser.on("--style", "=path",
802                     "Path to the subdirectory in the web application containing the requested",
803                     "stylesheet. The factory-setting is: \"default\".").execute(stylePath ->
804                     cfg.setWebappLAF((String) stylePath));
805 
806             parser.on("-T", "--threads", "=number", Integer.class,
807                     "The number of threads to use for index generation, repository scan",
808                     "and repository invalidation.",
809                     "By default the number of threads will be set to the number of available",
810                     "CPUs. This influences the number of spawned ctags processes as well.").
811                     execute(threadCount -> cfg.setIndexingParallelism((Integer) threadCount));
812 
813             parser.on("-t", "--tabSize", "=number", Integer.class,
814                 "Default tab size to use (number of spaces per tab character).")
815                     .execute(tabSize -> cfg.setTabSize((Integer) tabSize));
816 
817             parser.on("--token", "=string|@file_with_string",
818                     "Authorization bearer API token to use when making API calls",
819                     "to the web application").
820                     execute(optarg -> {
821                         String value = ((String) optarg).trim();
822                         if (value.startsWith("@")) {
823                             try (BufferedReader in = new BufferedReader(new InputStreamReader(
824                                     new FileInputStream(Path.of(value).toString().substring(1))))) {
825                                 String token = in.readLine().trim();
826                                 cfg.setIndexerAuthenticationToken(token);
827                             } catch (IOException e) {
828                                 die("Failed to read from " + value);
829                             }
830                         } else {
831                             cfg.setIndexerAuthenticationToken(value);
832                         }
833                     });
834 
835             parser.on("-U", "--uri", "=SCHEME://webappURI:port/contextPath",
836                 "Send the current configuration to the specified web application.").execute(webAddr -> {
837                     webappURI = (String) webAddr;
838                     try {
839                         URI uri = new URI(webappURI);
840                         String scheme = uri.getScheme();
841                         if (!scheme.equals("http") && !scheme.equals("https")) {
842                             die("webappURI '" + webappURI + "' does not have HTTP/HTTPS scheme");
843                         }
844                     } catch (URISyntaxException e) {
845                         die("URL '" + webappURI + "' is not valid.");
846                     }
847 
848                     env = RuntimeEnvironment.getInstance();
849                     env.setConfigURI(webappURI);
850                 }
851             );
852 
853             parser.on("---unitTest");  // For unit test only, will not appear in help
854 
855             parser.on("--updateConfig",
856                     "Populate the web application with a bare configuration, and exit.").execute(v ->
857                     bareConfig = true);
858 
859             parser.on("--userPage", "=URL",
860                 "Base URL of the user Information provider.",
861                 "Example: \"https://www.example.org/viewProfile.jspa?username=\".",
862                 "Use \"none\" to disable link.").execute(v -> cfg.setUserPage((String) v));
863 
864             parser.on("--userPageSuffix", "=URL-suffix",
865                 "URL Suffix for the user Information provider. Default: \"\".")
866                     .execute(suffix -> cfg.setUserPageSuffix((String) suffix));
867 
868             parser.on("-V", "--version", "Print version, and quit.").execute(v -> {
869                 System.out.println(Info.getFullVersion());
870                 System.exit(0);
871             });
872 
873             parser.on("-v", "--verbose", "Set logging level to INFO.").execute(v -> {
874                 verbose = true;
875                 LoggerUtil.setBaseConsoleLogLevel(Level.INFO);
876             });
877 
878             parser.on("-W", "--writeConfig", "=/path/to/configuration",
879                     "Write the current configuration to the specified file (so that the web",
880                     "application can use the same configuration).").execute(configFile ->
881                     configFilename = (String) configFile);
882 
883             parser.on("--webappCtags", "=on|off", ON_OFF, Boolean.class,
884                     "Web application should run ctags when necessary. Default is off.").
885                     execute(v -> cfg.setWebappCtags((Boolean) v));
886         });
887 
888         // Need to read the configuration file first, so that options may be overwritten later.
889         configure.parse(argv);
890 
891         LOGGER.log(Level.INFO, "Indexer options: {0}", Arrays.toString(argv));
892 
893         if (cfg == null) {
894             cfg = new Configuration();
895         }
896 
897         cfg.setHistoryEnabled(false);  // force user to turn on history capture
898 
899         argv = optParser.parse(argv);
900 
901         return argv;
902     }
903 
die(String message)904     private static void die(String message) {
905         System.err.println("ERROR: " + message);
906         System.exit(1);
907     }
908 
configureFileAnalyzer(String fileSpec, String analyzer)909     private static void configureFileAnalyzer(String fileSpec, String analyzer) {
910 
911         boolean prefix = false;
912 
913         // removing '.' from file specification
914         // expecting either ".extensionName" or "prefixName."
915         if (fileSpec.endsWith(".")) {
916             fileSpec = fileSpec.substring(0, fileSpec.lastIndexOf('.'));
917             prefix = true;
918         } else {
919             fileSpec = fileSpec.substring(1);
920         }
921         fileSpec = fileSpec.toUpperCase(Locale.ROOT);
922 
923         // Disable analyzer?
924         if (analyzer.equals("-")) {
925             if (prefix) {
926                 AnalyzerGuru.addPrefix(fileSpec, null);
927             } else {
928                 AnalyzerGuru.addExtension(fileSpec, null);
929             }
930         } else {
931             try {
932                 if (prefix) {
933                     AnalyzerGuru.addPrefix(
934                         fileSpec,
935                         AnalyzerGuru.findFactory(analyzer));
936                 } else {
937                     AnalyzerGuru.addExtension(
938                         fileSpec,
939                         AnalyzerGuru.findFactory(analyzer));
940                 }
941 
942             } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException
943                     | InvocationTargetException e) {
944                 LOGGER.log(Level.SEVERE, "Unable to locate FileAnalyzerFactory for {0}", analyzer);
945                 LOGGER.log(Level.SEVERE, "Stack: ", e.fillInStackTrace());
946                 System.exit(1);
947             }
948         }
949     }
950 
951     /**
952      * Write configuration to a file.
953      * @param env runtime environment
954      * @param filename file name to write the configuration to
955      * @throws IOException if I/O exception occurred
956      */
writeConfigToFile(RuntimeEnvironment env, String filename)957     public static void writeConfigToFile(RuntimeEnvironment env, String filename) throws IOException {
958         if (filename != null) {
959             LOGGER.log(Level.INFO, "Writing configuration to {0}", filename);
960             env.writeConfiguration(new File(filename));
961             LOGGER.log(Level.INFO, "Done writing configuration to {0}", filename);
962         }
963     }
964 
965     // Wrapper for prepareIndexer() that always generates history cache.
prepareIndexer(RuntimeEnvironment env, boolean searchRepositories, boolean addProjects, boolean createDict, List<String> subFiles, List<String> repositories)966     public void prepareIndexer(RuntimeEnvironment env,
967                                boolean searchRepositories,
968                                boolean addProjects,
969                                boolean createDict,
970                                List<String> subFiles,
971                                List<String> repositories) throws IndexerException, IOException {
972 
973         prepareIndexer(env,
974                 searchRepositories ? Collections.singleton(env.getSourceRootPath()) : Collections.emptySet(),
975                 addProjects, createDict, true, subFiles, repositories);
976     }
977 
978     /**
979      * Generate history cache and/or scan the repositories.
980      *
981      * This is the first phase of the indexing where history cache is being
982      * generated for repositories (at least for those which support getting
983      * history per directory).
984      *
985      * @param env runtime environment
986      * @param searchPaths list of paths in which to search for repositories
987      * @param addProjects if true, add projects
988      * @param createDict if true, create dictionary
989      * @param createHistoryCache create history cache flag
990      * @param subFiles list of directories
991      * @param repositories list of repositories
992      * @throws IndexerException indexer exception
993      * @throws IOException I/O exception
994      */
prepareIndexer(RuntimeEnvironment env, Set<String> searchPaths, boolean addProjects, boolean createDict, boolean createHistoryCache, List<String> subFiles, List<String> repositories)995     public void prepareIndexer(RuntimeEnvironment env,
996             Set<String> searchPaths,
997             boolean addProjects,
998             boolean createDict,
999             boolean createHistoryCache,
1000             List<String> subFiles,
1001             List<String> repositories) throws IndexerException, IOException {
1002 
1003         if (!env.validateUniversalCtags()) {
1004             throw new IndexerException("Didn't find Universal Ctags");
1005         }
1006 
1007         // Projects need to be created first since when adding repositories below,
1008         // some project properties might be needed for that.
1009         if (addProjects) {
1010             File[] files = env.getSourceRootFile().listFiles();
1011             Map<String, Project> projects = env.getProjects();
1012 
1013             addProjects(files, projects);
1014         }
1015 
1016         if (!searchPaths.isEmpty()) {
1017             LOGGER.log(Level.INFO, "Scanning for repositories in {0} (down to {1} levels below source root)",
1018                     new Object[]{searchPaths, env.getScanningDepth()});
1019             Statistics stats = new Statistics();
1020             env.setRepositories(searchPaths.toArray(new String[0]));
1021             stats.report(LOGGER, String.format("Done scanning for repositories, found %d repositories",
1022                     env.getRepositories().size()), "indexer.repository.scan");
1023         }
1024 
1025         if (createHistoryCache) {
1026             // Even if history is disabled globally, it can be enabled for some repositories.
1027             if (repositories != null && !repositories.isEmpty()) {
1028                 LOGGER.log(Level.INFO, "Generating history cache for repositories: {0}",
1029                         String.join(",", repositories));
1030                 HistoryGuru.getInstance().
1031                         createCache(repositories);
1032             } else {
1033                 LOGGER.log(Level.INFO, "Generating history cache for all repositories ...");
1034                 HistoryGuru.getInstance().createCache();
1035             }
1036             LOGGER.info("Done generating history cache");
1037         }
1038 
1039         if (createDict) {
1040             IndexDatabase.listFrequentTokens(subFiles);
1041         }
1042     }
1043 
addProjects(File[] files, Map<String, Project> projects)1044     private void addProjects(File[] files, Map<String, Project> projects) {
1045         // Keep a copy of the old project list so that we can preserve
1046         // the customization of existing projects.
1047         Map<String, Project> oldProjects = new HashMap<>();
1048         for (Project p : projects.values()) {
1049             oldProjects.put(p.getName(), p);
1050         }
1051 
1052         projects.clear();
1053 
1054         // Add a project for each top-level directory in source root.
1055         for (File file : files) {
1056             String name = file.getName();
1057             String path = '/' + name;
1058             if (oldProjects.containsKey(name)) {
1059                 // This is an existing object. Reuse the old project,
1060                 // possibly with customizations, instead of creating a
1061                 // new with default values.
1062                 Project p = oldProjects.get(name);
1063                 p.setPath(path);
1064                 p.setName(name);
1065                 p.completeWithDefaults();
1066                 projects.put(name, p);
1067             } else if (!name.startsWith(".") && file.isDirectory()) {
1068                 // Found a new directory with no matching project, so
1069                 // create a new project with default properties.
1070                 projects.put(name, new Project(name, path));
1071             }
1072         }
1073     }
1074 
1075     /**
1076      * This is the second phase of the indexer which generates Lucene index
1077      * by passing source code files through ctags, generating xrefs
1078      * and storing data from the source files in the index (along with history,
1079      * if any).
1080      *
1081      * @param update if set to true, index database is updated, otherwise optimized
1082      * @param subFiles index just some subdirectories
1083      * @param progress object to receive notifications as indexer progress is made
1084      * @throws IOException if I/O exception occurred
1085      */
doIndexerExecution(final boolean update, List<String> subFiles, IndexChangedListener progress)1086     public void doIndexerExecution(final boolean update, List<String> subFiles,
1087         IndexChangedListener progress)
1088             throws IOException {
1089         Statistics elapsed = new Statistics();
1090         LOGGER.info("Starting indexing");
1091 
1092         RuntimeEnvironment env = RuntimeEnvironment.getInstance();
1093         IndexerParallelizer parallelizer = env.getIndexerParallelizer();
1094         final CountDownLatch latch;
1095         if (subFiles == null || subFiles.isEmpty()) {
1096             if (update) {
1097                 latch = IndexDatabase.updateAll(progress);
1098             } else if (env.isOptimizeDatabase()) {
1099                 latch = IndexDatabase.optimizeAll();
1100             } else {
1101                 latch = new CountDownLatch(0);
1102             }
1103         } else {
1104             List<IndexDatabase> dbs = new ArrayList<>();
1105 
1106             for (String path : subFiles) {
1107                 Project project = Project.getProject(path);
1108                 if (project == null && env.hasProjects()) {
1109                     LOGGER.log(Level.WARNING, "Could not find a project for \"{0}\"", path);
1110                 } else {
1111                     IndexDatabase db;
1112                     if (project == null) {
1113                         db = new IndexDatabase();
1114                     } else {
1115                         db = new IndexDatabase(project);
1116                     }
1117                     int idx = dbs.indexOf(db);
1118                     if (idx != -1) {
1119                         db = dbs.get(idx);
1120                     }
1121 
1122                     if (db.addDirectory(path)) {
1123                         if (idx == -1) {
1124                             dbs.add(db);
1125                         }
1126                     } else {
1127                         LOGGER.log(Level.WARNING, "Directory does not exist \"{0}\"", path);
1128                     }
1129                 }
1130             }
1131 
1132             latch = new CountDownLatch(dbs.size());
1133             for (final IndexDatabase db : dbs) {
1134                 final boolean optimize = env.isOptimizeDatabase();
1135                 db.addIndexChangedListener(progress);
1136                 parallelizer.getFixedExecutor().submit(() -> {
1137                     try {
1138                         if (update) {
1139                             db.update();
1140                         } else if (optimize) {
1141                             db.optimize();
1142                         }
1143                     } catch (Throwable e) {
1144                         LOGGER.log(Level.SEVERE, "An error occurred while "
1145                                 + (update ? "updating" : "optimizing")
1146                                 + " index", e);
1147                     } finally {
1148                         latch.countDown();
1149                     }
1150                 });
1151             }
1152         }
1153 
1154         // Wait forever for the executors to finish.
1155         try {
1156             LOGGER.info("Waiting for the executors to finish");
1157             latch.await();
1158         } catch (InterruptedException exp) {
1159             LOGGER.log(Level.WARNING, "Received interrupt while waiting" +
1160                     " for executor to finish", exp);
1161         }
1162         elapsed.report(LOGGER, "Done indexing data of all repositories", "indexer.repository.indexing");
1163 
1164         CtagsUtil.deleteTempFiles();
1165     }
1166 
sendToConfigHost(RuntimeEnvironment env, String host)1167     public void sendToConfigHost(RuntimeEnvironment env, String host) {
1168         LOGGER.log(Level.INFO, "Sending configuration to: {0}", host);
1169         try {
1170             env.writeConfiguration(host);
1171         } catch (IOException | IllegalArgumentException ex) {
1172             LOGGER.log(Level.SEVERE, String.format(
1173                     "Failed to send configuration to %s "
1174                     + "(is web application server running with opengrok deployed?)", host), ex);
1175         } catch (InterruptedException e) {
1176             LOGGER.log(Level.WARNING, "interrupted while sending configuration");
1177         }
1178         LOGGER.info("Configuration update routine done, check log output for errors.");
1179     }
1180 
pauseToAwaitProfiler()1181     private static void pauseToAwaitProfiler() {
1182         Scanner scan = new Scanner(System.in);
1183         String in;
1184         do {
1185             System.out.print("Start profiler. Continue (Y/N)? ");
1186             in = scan.nextLine().toLowerCase(Locale.ROOT);
1187         } while (!in.equals("y") && !in.equals("n"));
1188 
1189         if (in.equals("n")) {
1190             System.exit(1);
1191         }
1192     }
1193 
1194     // Visible for testing
handlePathParameter(Collection<String> paramValueStore, String pathValue)1195     static void handlePathParameter(Collection<String> paramValueStore, String pathValue) {
1196         if (pathValue.startsWith("@")) {
1197             paramValueStore.addAll(loadPathsFromFile(pathValue.substring(1)));
1198         } else {
1199             paramValueStore.add(pathValue);
1200         }
1201     }
1202 
loadPathsFromFile(String filename)1203     private static List<String> loadPathsFromFile(String filename) {
1204         try {
1205             return Files.readAllLines(Path.of(filename));
1206         } catch (IOException e) {
1207             LOGGER.log(Level.SEVERE, String.format("Could not load paths from %s", filename), e);
1208             throw new UncheckedIOException(e);
1209         }
1210     }
1211 
exitWithHelp()1212     private static void exitWithHelp() {
1213         PrintStream helpStream = status != 0 ? System.err : System.out;
1214         switch (helpMode) {
1215             case CONFIG:
1216                 helpStream.print(ConfigurationHelp.getSamples());
1217                 break;
1218             case CTAGS:
1219                 /*
1220                  * Force the environment's ctags, because this method is called
1221                  * before main() does the heavyweight setConfiguration().
1222                  */
1223                 env.setCtags(cfg.getCtags());
1224                 helpStream.println("Ctags command-line:");
1225                 helpStream.println();
1226                 helpStream.println(getCtagsCommand());
1227                 helpStream.println();
1228                 break;
1229             case GURU:
1230                 helpStream.println(AnalyzerGuruHelp.getUsage());
1231                 break;
1232             case REPOS:
1233                 /*
1234                  * Force the environment's disabledRepositories (as above).
1235                  */
1236                 env.setDisabledRepositories(cfg.getDisabledRepositories());
1237                 helpStream.println(RepositoriesHelp.getText());
1238                 break;
1239             default:
1240                 helpStream.println(helpUsage);
1241                 break;
1242         }
1243         System.exit(status);
1244     }
1245 
getCtagsCommand()1246     private static String getCtagsCommand() {
1247         Ctags ctags = CtagsUtil.newInstance(env);
1248         return Executor.escapeForShell(ctags.getArgv(), true, SystemUtils.IS_OS_WINDOWS);
1249     }
1250 
1251     private enum HelpMode {
1252         CONFIG, CTAGS, DEFAULT, GURU, REPOS
1253     }
1254 
Indexer()1255     private Indexer() {
1256     }
1257 }
1258