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) 2007, 2018, Oracle and/or its affiliates. All rights reserved.
22  * Portions Copyright (c) 2017-2019, Chris Fraire <cfraire@me.com>.
23  * Portions Copyright (c) 2020, Aleksandr Kirillov <alexkirillovsamara@gmail.com>.
24  */
25 package org.opengrok.indexer.configuration;
26 
27 import java.beans.ExceptionListener;
28 import java.beans.XMLDecoder;
29 import java.beans.XMLEncoder;
30 import java.io.BufferedInputStream;
31 import java.io.BufferedOutputStream;
32 import java.io.ByteArrayInputStream;
33 import java.io.ByteArrayOutputStream;
34 import java.io.File;
35 import java.io.FileInputStream;
36 import java.io.FileOutputStream;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.io.OutputStream;
40 import java.nio.file.Path;
41 import java.nio.file.Paths;
42 import java.util.ArrayList;
43 import java.util.Collections;
44 import java.util.HashMap;
45 import java.util.HashSet;
46 import java.util.LinkedList;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Set;
50 import java.util.TreeSet;
51 import java.util.concurrent.ConcurrentHashMap;
52 import java.util.function.Predicate;
53 import java.util.logging.Level;
54 import java.util.logging.Logger;
55 import java.util.regex.Pattern;
56 import java.util.regex.PatternSyntaxException;
57 import java.util.stream.Collectors;
58 
59 import org.opengrok.indexer.authorization.AuthControlFlag;
60 import org.opengrok.indexer.authorization.AuthorizationStack;
61 import org.opengrok.indexer.history.RepositoryInfo;
62 import org.opengrok.indexer.index.Filter;
63 import org.opengrok.indexer.index.IgnoredNames;
64 import org.opengrok.indexer.logger.LoggerFactory;
65 
66 
67 /**
68  * Placeholder class for all configuration variables. Due to the multi-threaded
69  * nature of the web application, each thread will use the same instance of the
70  * configuration object for each page request. Class and methods should have
71  * package scope, but that didn't work with the XMLDecoder/XMLEncoder.
72  * <p>
73  * This should be as close to a
74  * <a href="https://en.wikipedia.org/wiki/Plain_old_Java_object">POJO</a> as
75  * possible.
76  */
77 public final class Configuration {
78 
79     private static final Logger LOGGER = LoggerFactory.getLogger(Configuration.class);
80     public static final String PLUGIN_DIRECTORY_DEFAULT = "plugins";
81     public static final String STATISTICS_FILE_DEFAULT = "statistics.json";
82 
83     /**
84      * A check if a pattern contains at least one pair of parentheses meaning
85      * that there is at least one capture group. This group must not be empty.
86      */
87     private static final String PATTERN_SINGLE_GROUP = ".*\\([^\\)]+\\).*";
88     /**
89      * Error string for invalid patterns without a single group. This is passed
90      * as a first argument to the constructor of PatternSyntaxException and in
91      * the output it is followed by the invalid pattern.
92      *
93      * @see PatternSyntaxException
94      * @see #PATTERN_SINGLE_GROUP
95      */
96     private static final String PATTERN_MUST_CONTAIN_GROUP = "The pattern must contain at least one non-empty group -";
97     /**
98      * Error string for negative numbers (could be int, double, long, ...).
99      * First argument is the name of the property, second argument is the actual
100      * value.
101      */
102     private static final String NEGATIVE_NUMBER_ERROR = "Invalid value for \"%s\" - \"%s\". Expected value greater or equal than 0";
103     /**
104      * Error string for non-positive numbers (could be int, double, long, ...).
105      * First argument is the name of the property, second argument is the actual
106      * value.
107      */
108     private static final String NONPOSITIVE_NUMBER_ERROR =
109         "Invalid value for \"%s\" - \"%s\". Expected value greater than 0";
110 
111     /**
112      * Path to {@code ctags} binary.
113      */
114     private String ctags;
115     private boolean webappCtags;
116 
117     /**
118      * A defined value to specify the mandoc binary or else null so that mandoc
119      * will be cross-referenced using {@code PlainXref}.
120      */
121     private String mandoc;
122 
123     /**
124      * Should the history log be cached?
125      */
126     private boolean historyCache;
127     /**
128      * The maximum time in milliseconds {@code HistoryCache.get()} can take
129      * before its result is cached.
130      */
131     private int historyCacheTime;
132     /**
133      * flag to generate history. This is bigger hammer than @{code historyCache}
134      * above. If set to false, no history query will be ever made and the webapp
135      * will not display any history related links/allow any history queries.
136      */
137     private boolean historyEnabled;
138     /**
139      * maximum number of messages in webapp.
140      */
141     private int messageLimit;
142     /**
143      * Directory with authorization plug-ins. Default value is
144      * {@code dataRoot/../plugins} (can be {@code /var/opengrok/plugins} if dataRoot is
145      * {@code /var/opengrok/data}).
146      */
147     private String pluginDirectory;
148     /**
149      * Enable watching the plug-in directory for changes in real time. Suitable
150      * for development.
151      */
152     private boolean authorizationWatchdogEnabled;
153     private AuthorizationStack pluginStack;
154     private Map<String, Project> projects; // project name -> Project
155     private Set<Group> groups;
156     private String sourceRoot;
157     private String dataRoot;
158     /**
159      * Directory with include files for web application (header, footer, etc.).
160      */
161     private String includeRoot;
162     private List<RepositoryInfo> repositories;
163 
164     private boolean generateHtml;
165     /**
166      * Default projects will be used, when no project is selected and no project
167      * is in cookie, so basically only the first time a page is opened,
168      * or when web cookies are cleared.
169      */
170     private Set<Project> defaultProjects;
171     /**
172      * Default size of memory to be used for flushing of Lucene docs per thread.
173      * Lucene 4.x uses 16MB and 8 threads, so below is a nice tunable.
174      */
175     private double ramBufferSize;
176     /**
177      * If below is set, then we count how many files per project we need to
178      * process and print percentage of completion per project.
179      */
180     private boolean printProgress;
181     private boolean allowLeadingWildcard;
182     private IgnoredNames ignoredNames;
183     private Filter includedNames;
184     private String userPage;
185     private String userPageSuffix;
186     private String bugPage;
187     private String bugPattern;
188     private String reviewPage;
189     private String reviewPattern;
190     private String webappLAF;
191     private RemoteSCM remoteScmSupported;
192     private boolean optimizeDatabase;
193     private boolean quickContextScan;
194 
195     private LuceneLockName luceneLocking = LuceneLockName.OFF;
196     private boolean compressXref;
197     private boolean indexVersionedFilesOnly;
198     private int indexingParallelism;
199     private int historyParallelism;
200     private int historyRenamedParallelism;
201     private boolean tagsEnabled;
202     private int hitsPerPage;
203     private int cachePages;
204     private short contextLimit; // initialized non-zero in ctor
205     private short contextSurround;
206     private boolean lastEditedDisplayMode;
207     private String CTagsExtraOptionsFile;
208     private int scanningDepth;
209     private Set<String> allowedSymlinks;
210     private Set<String> canonicalRoots;
211     private boolean obfuscatingEMailAddresses;
212     private boolean chattyStatusPage;
213     private final Map<String, String> cmds;  // repository type -> command
214     private int tabSize;
215     private int commandTimeout; // in seconds
216     private int interactiveCommandTimeout; // in seconds
217     private long ctagsTimeout; // in seconds
218     private boolean scopesEnabled;
219     private boolean projectsEnabled;
220     private boolean foldingEnabled;
221     private String statisticsFilePath;
222     /*
223      * Set to false if we want to disable fetching history of individual files
224      * (by running appropriate SCM command) when the history is not found
225      * in history cache for repositories capable of fetching history for
226      * directories. This option affects file based history cache only.
227      */
228     private boolean fetchHistoryWhenNotInCache;
229     /*
230      * Set to false to disable extended handling of history of files across
231      * renames, i.e. support getting diffs of revisions across renames
232      * for capable repositories.
233      */
234     private boolean handleHistoryOfRenamedFiles;
235 
236     public static final double defaultRamBufferSize = 16;
237 
238     /**
239      * The directory hierarchy depth to limit the scanning for repositories.
240      * E.g. if the /mercurial/ directory (relative to source root) is a repository
241      * and /mercurial/usr/closed/ is sub-repository, the latter will be discovered
242      * only if the depth is set to 3 or greater.
243      */
244     public static final int defaultScanningDepth = 3;
245 
246     /**
247      * The name of the eftar file relative to the <var>DATA_ROOT</var>, which
248      * contains definition tags.
249      */
250     public static final String EFTAR_DTAGS_NAME = "dtags.eftar";
251 
252     /**
253      * Revision messages will be collapsible if they exceed this many number of
254      * characters. Front end enforces an appropriate minimum.
255      */
256     private int revisionMessageCollapseThreshold;
257 
258     /**
259      * Groups are collapsed if number of repositories is greater than this
260      * threshold. This applies only for non-favorite groups - groups which don't
261      * contain a project which is considered as a favorite project for the user.
262      * Favorite projects are the projects which the user browses and searches
263      * and are stored in a cookie. Favorite groups are always expanded.
264      */
265     private int groupsCollapseThreshold;
266 
267     /**
268      * Current indexed message will be collapsible if they exceed this many
269      * number of characters. Front end enforces an appropriate minimum.
270      */
271     private int currentIndexedCollapseThreshold;
272 
273     /**
274      * Upper bound for number of threads used for performing multi-project
275      * searches. This is total for the whole webapp.
276      */
277     private int MaxSearchThreadCount;
278 
279     /**
280      * Upper bound for number of threads used for getting revision contents.
281      * This is total for the whole webapp.
282      */
283     private int MaxRevisionThreadCount;
284 
285     /**
286      * If false, do not display listing or projects/repositories on the index page.
287      */
288     private boolean displayRepositories;
289 
290     /**
291      * If true, list directories first in xref directory listing.
292      */
293     private boolean listDirsFirst = true;
294 
295     /**
296      * A flag if the navigate window should be opened by default when browsing
297      * the source code of projects.
298      */
299     private boolean navigateWindowEnabled;
300 
301     private SuggesterConfig suggesterConfig = new SuggesterConfig();
302 
303     private Set<String> disabledRepositories;
304 
305     /*
306      * types of handling history for remote SCM repositories:
307      *  ON - index history and display it in webapp
308      *  OFF - do not index or display history in webapp
309      *  DIRBASED - index history only for repositories capable
310      *             of getting history for directories
311      *  UIONLY - display history only in webapp (do not index it)
312      */
313     public enum RemoteSCM {
314         ON, OFF, DIRBASED, UIONLY
315     }
316 
317     /**
318      * Get the default tab size (number of space characters per tab character)
319      * to use for each project. If {@code <= 0} tabs are read/write as is.
320      *
321      * @return current tab size set.
322      * @see Project#getTabSize()
323      * @see org.opengrok.indexer.analysis.ExpandTabsReader
324      */
getTabSize()325     public int getTabSize() {
326         return tabSize;
327     }
328 
329     /**
330      * Set the default tab size (number of space characters per tab character)
331      * to use for each project. If {@code <= 0} tabs are read/write as is.
332      *
333      * @param tabSize tabsize to set.
334      * @see Project#setTabSize(int)
335      * @see org.opengrok.indexer.analysis.ExpandTabsReader
336      */
setTabSize(int tabSize)337     public void setTabSize(int tabSize) {
338         this.tabSize = tabSize;
339     }
340 
getScanningDepth()341     public int getScanningDepth() {
342         return scanningDepth;
343     }
344 
345     /**
346      * Set the scanning depth to a new value.
347      *
348      * @param scanningDepth the new value
349      * @throws IllegalArgumentException when the scanningDepth is negative
350      */
setScanningDepth(int scanningDepth)351     public void setScanningDepth(int scanningDepth) throws IllegalArgumentException {
352         if (scanningDepth < 0) {
353             throw new IllegalArgumentException(
354                     String.format(NEGATIVE_NUMBER_ERROR, "scanningDepth", scanningDepth));
355         }
356         this.scanningDepth = scanningDepth;
357     }
358 
getCommandTimeout()359     public int getCommandTimeout() {
360         return commandTimeout;
361     }
362 
363     /**
364      * Set the command timeout to a new value.
365      *
366      * @param commandTimeout the new value
367      * @throws IllegalArgumentException when the timeout is negative
368      */
setCommandTimeout(int commandTimeout)369     public void setCommandTimeout(int commandTimeout) throws IllegalArgumentException {
370         if (commandTimeout < 0) {
371             throw new IllegalArgumentException(
372                     String.format(NEGATIVE_NUMBER_ERROR, "commandTimeout", commandTimeout));
373         }
374         this.commandTimeout = commandTimeout;
375     }
376 
getInteractiveCommandTimeout()377     public int getInteractiveCommandTimeout() {
378         return interactiveCommandTimeout;
379     }
380 
381     /**
382      * Set the interactive command timeout to a new value.
383      *
384      * @param commandTimeout the new value
385      * @throws IllegalArgumentException when the timeout is negative
386      */
setInteractiveCommandTimeout(int commandTimeout)387     public void setInteractiveCommandTimeout(int commandTimeout) throws IllegalArgumentException {
388         if (commandTimeout < 0) {
389             throw new IllegalArgumentException(
390                     String.format(NEGATIVE_NUMBER_ERROR, "interactiveCommandTimeout", commandTimeout));
391         }
392         this.interactiveCommandTimeout = commandTimeout;
393     }
394 
getCtagsTimeout()395     public long getCtagsTimeout() {
396         return ctagsTimeout;
397     }
398 
399     /**
400      * Set the ctags timeout to a new value.
401      *
402      * @param timeout the new value
403      * @throws IllegalArgumentException when the timeout is negative
404      */
setCtagsTimeout(long timeout)405     public void setCtagsTimeout(long timeout) throws IllegalArgumentException {
406         if (commandTimeout < 0) {
407             throw new IllegalArgumentException(
408                     String.format(NEGATIVE_NUMBER_ERROR, "ctagsTimeout", timeout));
409         }
410         this.ctagsTimeout = timeout;
411     }
412 
getStatisticsFilePath()413     public String getStatisticsFilePath() {
414         return statisticsFilePath;
415     }
416 
setStatisticsFilePath(String statisticsFilePath)417     public void setStatisticsFilePath(String statisticsFilePath) {
418         this.statisticsFilePath = statisticsFilePath;
419     }
420 
isLastEditedDisplayMode()421     public boolean isLastEditedDisplayMode() {
422         return lastEditedDisplayMode;
423     }
424 
setLastEditedDisplayMode(boolean lastEditedDisplayMode)425     public void setLastEditedDisplayMode(boolean lastEditedDisplayMode) {
426         this.lastEditedDisplayMode = lastEditedDisplayMode;
427     }
428 
getGroupsCollapseThreshold()429     public int getGroupsCollapseThreshold() {
430         return groupsCollapseThreshold;
431     }
432 
433     /**
434      * Set the groups collapse threshold to a new value.
435      *
436      * @param groupsCollapseThreshold the new value
437      * @throws IllegalArgumentException when the timeout is negative
438      */
setGroupsCollapseThreshold(int groupsCollapseThreshold)439     public void setGroupsCollapseThreshold(int groupsCollapseThreshold) throws IllegalArgumentException {
440         if (groupsCollapseThreshold < 0) {
441             throw new IllegalArgumentException(
442                     String.format(NEGATIVE_NUMBER_ERROR, "groupsCollapseThreshold", groupsCollapseThreshold));
443         }
444         this.groupsCollapseThreshold = groupsCollapseThreshold;
445     }
446 
447     /**
448      * Creates a new instance of Configuration with default values.
449      */
Configuration()450     public Configuration() {
451         // This list of calls is sorted alphabetically so please keep it.
452         cmds = new HashMap<>();
453         setAllowLeadingWildcard(true);
454         setAllowedSymlinks(new HashSet<>());
455         setAuthorizationWatchdogEnabled(false);
456         //setBugPage("http://bugs.myserver.org/bugdatabase/view_bug.do?bug_id=");
457         setBugPattern("\\b([12456789][0-9]{6})\\b");
458         setCachePages(5);
459         setCanonicalRoots(new HashSet<>());
460         setCommandTimeout(600); // 10 minutes
461         setInteractiveCommandTimeout(30);
462         setCompressXref(true);
463         setContextLimit((short) 10);
464         //contextSurround is default(short)
465         //ctags is default(String)
466         setCtagsTimeout(10);
467         setCurrentIndexedCollapseThreshold(27);
468         setDataRoot(null);
469         setDisplayRepositories(true);
470         setFetchHistoryWhenNotInCache(true);
471         setFoldingEnabled(true);
472         setGenerateHtml(true);
473         setGroups(new TreeSet<>());
474         setGroupsCollapseThreshold(4);
475         setHandleHistoryOfRenamedFiles(false);
476         setHistoryCache(true);
477         setHistoryCacheTime(30);
478         setHistoryEnabled(true);
479         setHitsPerPage(25);
480         setIgnoredNames(new IgnoredNames());
481         setIncludedNames(new Filter());
482         setIndexVersionedFilesOnly(false);
483         setLastEditedDisplayMode(true);
484         //luceneLocking default is OFF
485         //mandoc is default(String)
486         setMaxSearchThreadCount(2 * Runtime.getRuntime().availableProcessors());
487         setMaxRevisionThreadCount(Runtime.getRuntime().availableProcessors());
488         setMessageLimit(500);
489         setNavigateWindowEnabled(false);
490         setOptimizeDatabase(true);
491         setPluginDirectory(null);
492         setPluginStack(new AuthorizationStack(AuthControlFlag.REQUIRED, "default stack"));
493         setPrintProgress(false);
494         setDisabledRepositories(new HashSet<>());
495         setProjects(new ConcurrentHashMap<>());
496         setQuickContextScan(true);
497         //below can cause an outofmemory error, since it is defaulting to NO LIMIT
498         setRamBufferSize(defaultRamBufferSize); //MB
499         setRemoteScmSupported(RemoteSCM.OFF);
500         setRepositories(new ArrayList<>());
501         //setReviewPage("http://arc.myserver.org/caselog/PSARC/");
502         setReviewPattern("\\b(\\d{4}/\\d{3})\\b"); // in form e.g. PSARC 2008/305
503         setRevisionMessageCollapseThreshold(200);
504         setScanningDepth(defaultScanningDepth); // default depth of scanning for repositories
505         setScopesEnabled(true);
506         setSourceRoot(null);
507         setStatisticsFilePath(null);
508         //setTabSize(4);
509         setTagsEnabled(false);
510         //setUserPage("http://www.myserver.org/viewProfile.jspa?username=");
511         // Set to empty string so we can append it to the URL
512         // unconditionally later.
513         setUserPageSuffix("");
514         setWebappLAF("default");
515         // webappCtags is default(boolean)
516     }
517 
getRepoCmd(String clazzName)518     public String getRepoCmd(String clazzName) {
519         return cmds.get(clazzName);
520     }
521 
setRepoCmd(String clazzName, String cmd)522     public String setRepoCmd(String clazzName, String cmd) {
523         if (clazzName == null) {
524             return null;
525         }
526         if (cmd == null || cmd.length() == 0) {
527             return cmds.remove(clazzName);
528         }
529         return cmds.put(clazzName, cmd);
530     }
531 
532     // just to satisfy bean/de|encoder stuff
getCmds()533     public Map<String, String> getCmds() {
534         return Collections.unmodifiableMap(cmds);
535     }
536 
537     /**
538      * @see org.opengrok.indexer.web.messages.MessagesContainer
539      *
540      * @return int the current message limit
541      */
getMessageLimit()542     public int getMessageLimit() {
543         return messageLimit;
544     }
545 
546     /**
547      * @see org.opengrok.indexer.web.messages.MessagesContainer
548      *
549      * @param messageLimit the limit
550      * @throws IllegalArgumentException when the limit is negative
551      */
setMessageLimit(int messageLimit)552     public void setMessageLimit(int messageLimit) throws IllegalArgumentException {
553         if (messageLimit < 0) {
554             throw new IllegalArgumentException(
555                     String.format(NEGATIVE_NUMBER_ERROR, "messageLimit", messageLimit));
556         }
557         this.messageLimit = messageLimit;
558     }
559 
getPluginDirectory()560     public String getPluginDirectory() {
561         return pluginDirectory;
562     }
563 
setPluginDirectory(String pluginDirectory)564     public void setPluginDirectory(String pluginDirectory) {
565         this.pluginDirectory = pluginDirectory;
566     }
567 
isAuthorizationWatchdogEnabled()568     public boolean isAuthorizationWatchdogEnabled() {
569         return authorizationWatchdogEnabled;
570     }
571 
setAuthorizationWatchdogEnabled(boolean authorizationWatchdogEnabled)572     public void setAuthorizationWatchdogEnabled(boolean authorizationWatchdogEnabled) {
573         this.authorizationWatchdogEnabled = authorizationWatchdogEnabled;
574     }
575 
getPluginStack()576     public AuthorizationStack getPluginStack() {
577         return pluginStack;
578     }
579 
setPluginStack(AuthorizationStack pluginStack)580     public void setPluginStack(AuthorizationStack pluginStack) {
581         this.pluginStack = pluginStack;
582     }
583 
setCmds(Map<String, String> cmds)584     public void setCmds(Map<String, String> cmds) {
585         this.cmds.clear();
586         this.cmds.putAll(cmds);
587     }
588 
589     /**
590      * Gets the configuration's ctags command. Default is null.
591      * @return the configured value
592      */
getCtags()593     public String getCtags() {
594         return ctags;
595     }
596 
597     /**
598      * Sets the configuration's ctags command.
599      * @param ctags A program name (full-path if necessary) or {@code null}
600      */
setCtags(String ctags)601     public void setCtags(String ctags) {
602         this.ctags = ctags;
603     }
604 
605     /**
606      * Gets the configuration's mandoc command. Default is {@code null}.
607      * @return the configured value
608      */
getMandoc()609     public String getMandoc() {
610         return mandoc;
611     }
612 
613     /**
614      * Sets the configuration's mandoc command.
615      * @param value A program name (full-path if necessary) or {@code null}
616      */
setMandoc(String value)617     public void setMandoc(String value) {
618         this.mandoc = value;
619     }
620 
621     /**
622      * Gets the total number of context lines per file to show in cases where it
623      * is limited. Default is 10.
624      * @return a value greater than zero
625      */
getContextLimit()626     public short getContextLimit() {
627         return contextLimit;
628     }
629 
630     /**
631      * Sets the total number of context lines per file to show in cases where it
632      * is limited.
633      * @param value a value greater than zero
634      * @throws IllegalArgumentException if {@code value} is not positive
635      */
setContextLimit(short value)636     public void setContextLimit(short value) throws IllegalArgumentException {
637         if (value < 1) {
638             throw new IllegalArgumentException(
639                 String.format(NONPOSITIVE_NUMBER_ERROR, "contextLimit", value));
640         }
641         this.contextLimit = value;
642     }
643 
644     /**
645      * Gets the number of context lines to show before or after any match.
646      * Default is zero.
647      * @return a value greater than or equal to zero
648      */
getContextSurround()649     public short getContextSurround() {
650         return contextSurround;
651     }
652 
653     /**
654      * Sets the number of context lines to show before or after any match.
655      * @param value a value greater than or equal to zero
656      * @throws IllegalArgumentException if {@code value} is negative
657      */
setContextSurround(short value)658     public void setContextSurround(short value)
659             throws IllegalArgumentException {
660         if (value < 0) {
661             throw new IllegalArgumentException(
662                 String.format(NEGATIVE_NUMBER_ERROR, "contextSurround", value));
663         }
664         this.contextSurround = value;
665     }
666 
getCachePages()667     public int getCachePages() {
668         return cachePages;
669     }
670 
671     /**
672      * Set the cache pages to a new value.
673      *
674      * @param cachePages the new value
675      * @throws IllegalArgumentException when the cachePages is negative
676      */
setCachePages(int cachePages)677     public void setCachePages(int cachePages) throws IllegalArgumentException {
678         if (cachePages < 0) {
679             throw new IllegalArgumentException(
680                     String.format(NEGATIVE_NUMBER_ERROR, "cachePages", cachePages));
681         }
682         this.cachePages = cachePages;
683     }
684 
getHitsPerPage()685     public int getHitsPerPage() {
686         return hitsPerPage;
687     }
688 
689     /**
690      * Set the hits per page to a new value.
691      *
692      * @param hitsPerPage the new value
693      * @throws IllegalArgumentException when the hitsPerPage is negative
694      */
setHitsPerPage(int hitsPerPage)695     public void setHitsPerPage(int hitsPerPage) throws IllegalArgumentException {
696         if (hitsPerPage < 0) {
697             throw new IllegalArgumentException(
698                     String.format(NEGATIVE_NUMBER_ERROR, "hitsPerPage", hitsPerPage));
699         }
700         this.hitsPerPage = hitsPerPage;
701     }
702 
703     /**
704      * Should the history be enabled ?
705      *
706      * @return {@code true} if history is enabled, {@code false} otherwise
707      */
isHistoryEnabled()708     public boolean isHistoryEnabled() {
709         return historyEnabled;
710     }
711 
712     /**
713      * Set whether history should be enabled.
714      *
715      * @param flag if {@code true} enable history
716      */
setHistoryEnabled(boolean flag)717     public void setHistoryEnabled(boolean flag) {
718         this.historyEnabled = flag;
719     }
720 
721     /**
722      * Should the history log be cached?
723      *
724      * @return {@code true} if a {@code HistoryCache} implementation should be
725      * used, {@code false} otherwise
726      */
isHistoryCache()727     public boolean isHistoryCache() {
728         return historyCache;
729     }
730 
731     /**
732      * Set whether history should be cached.
733      *
734      * @param historyCache if {@code true} enable history cache
735      */
setHistoryCache(boolean historyCache)736     public void setHistoryCache(boolean historyCache) {
737         this.historyCache = historyCache;
738     }
739 
740     /**
741      * How long can a history request take before it's cached? If the time is
742      * exceeded, the result is cached. This setting only affects
743      * {@code FileHistoryCache}.
744      *
745      * @return the maximum time in milliseconds a history request can take
746      * before it's cached
747      */
getHistoryCacheTime()748     public int getHistoryCacheTime() {
749         return historyCacheTime;
750     }
751 
752     /**
753      * Set the maximum time a history request can take before it's cached. This
754      * setting is only respected if {@code FileHistoryCache} is used.
755      *
756      * @param historyCacheTime maximum time in milliseconds
757      */
setHistoryCacheTime(int historyCacheTime)758     public void setHistoryCacheTime(int historyCacheTime) {
759         this.historyCacheTime = historyCacheTime;
760     }
761 
isFetchHistoryWhenNotInCache()762     public boolean isFetchHistoryWhenNotInCache() {
763         return fetchHistoryWhenNotInCache;
764     }
765 
setFetchHistoryWhenNotInCache(boolean nofetch)766     public void setFetchHistoryWhenNotInCache(boolean nofetch) {
767         this.fetchHistoryWhenNotInCache = nofetch;
768     }
769 
isHandleHistoryOfRenamedFiles()770     public boolean isHandleHistoryOfRenamedFiles() {
771         return handleHistoryOfRenamedFiles;
772     }
773 
setHandleHistoryOfRenamedFiles(boolean enable)774     public void setHandleHistoryOfRenamedFiles(boolean enable) {
775         this.handleHistoryOfRenamedFiles = enable;
776     }
777 
isNavigateWindowEnabled()778     public boolean isNavigateWindowEnabled() {
779         return navigateWindowEnabled;
780     }
781 
setNavigateWindowEnabled(boolean navigateWindowEnabled)782     public void setNavigateWindowEnabled(boolean navigateWindowEnabled) {
783         this.navigateWindowEnabled = navigateWindowEnabled;
784     }
785 
getProjects()786     public Map<String, Project> getProjects() {
787         return projects;
788     }
789 
setProjects(Map<String, Project> projects)790     public void setProjects(Map<String, Project> projects) {
791         this.projects = projects;
792     }
793 
794     /**
795      * Adds a group to the set. This is performed upon configuration parsing
796      *
797      * @param group group
798      * @throws IOException when group is not unique across the set
799      */
addGroup(Group group)800     public void addGroup(Group group) throws IOException {
801         if (!groups.add(group)) {
802             throw new IOException(
803                     String.format("Duplicate group name '%s' in configuration.",
804                             group.getName()));
805         }
806     }
807 
getGroups()808     public Set<Group> getGroups() {
809         return groups;
810     }
811 
setGroups(Set<Group> groups)812     public void setGroups(Set<Group> groups) {
813         this.groups = groups;
814     }
815 
getSourceRoot()816     public String getSourceRoot() {
817         return sourceRoot;
818     }
819 
setSourceRoot(String sourceRoot)820     public void setSourceRoot(String sourceRoot) {
821         this.sourceRoot = sourceRoot;
822     }
823 
getDataRoot()824     public String getDataRoot() {
825         return dataRoot;
826     }
827 
828     /**
829      * Sets data root.
830      *
831      * This method also sets the pluginDirectory if it is not already set and
832      * also this method sets the statisticsFilePath if it is not already set
833      *
834      * @see #setPluginDirectory(java.lang.String)
835      * @see #setStatisticsFilePath(java.lang.String)
836      *
837      * @param dataRoot data root path
838      */
setDataRoot(String dataRoot)839     public void setDataRoot(String dataRoot) {
840         if (dataRoot != null && getPluginDirectory() == null) {
841             setPluginDirectory(dataRoot + "/../" + PLUGIN_DIRECTORY_DEFAULT);
842         }
843         if (dataRoot != null && getStatisticsFilePath() == null) {
844             setStatisticsFilePath(dataRoot + "/" + STATISTICS_FILE_DEFAULT);
845         }
846         this.dataRoot = dataRoot;
847     }
848 
849     /**
850      * If {@link #includeRoot} is not set, {@link #dataRoot} will be returned.
851      * @return web include root directory
852      */
getIncludeRoot()853     public String getIncludeRoot() {
854         return includeRoot != null ? includeRoot : dataRoot;
855     }
856 
setIncludeRoot(String newRoot)857     public void setIncludeRoot(String newRoot) {
858         this.includeRoot = newRoot;
859     }
860 
getRepositories()861     public List<RepositoryInfo> getRepositories() {
862         return repositories;
863     }
864 
setRepositories(List<RepositoryInfo> repositories)865     public void setRepositories(List<RepositoryInfo> repositories) {
866         this.repositories = repositories;
867     }
868 
addRepositories(List<RepositoryInfo> repositories)869     public void addRepositories(List<RepositoryInfo> repositories) {
870         this.repositories.addAll(repositories);
871     }
872 
setGenerateHtml(boolean generateHtml)873     public void setGenerateHtml(boolean generateHtml) {
874         this.generateHtml = generateHtml;
875     }
876 
isGenerateHtml()877     public boolean isGenerateHtml() {
878         return generateHtml;
879     }
880 
setDefaultProjects(Set<Project> defaultProjects)881     public void setDefaultProjects(Set<Project> defaultProjects) {
882         this.defaultProjects = defaultProjects;
883     }
884 
getDefaultProjects()885     public Set<Project> getDefaultProjects() {
886         return defaultProjects;
887     }
888 
getRamBufferSize()889     public double getRamBufferSize() {
890         return ramBufferSize;
891     }
892 
893     /**
894      * Set size of memory to be used for flushing docs (default 16 MB) (this can
895      * improve index speed a LOT) note that this is per thread (lucene uses 8
896      * threads by default in 4.x).
897      *
898      * @param ramBufferSize new size in MB
899      */
setRamBufferSize(double ramBufferSize)900     public void setRamBufferSize(double ramBufferSize) {
901         this.ramBufferSize = ramBufferSize;
902     }
903 
isPrintProgress()904     public boolean isPrintProgress() {
905         return printProgress;
906     }
907 
setPrintProgress(boolean printProgress)908     public void setPrintProgress(boolean printProgress) {
909         this.printProgress = printProgress;
910     }
911 
setAllowLeadingWildcard(boolean allowLeadingWildcard)912     public void setAllowLeadingWildcard(boolean allowLeadingWildcard) {
913         this.allowLeadingWildcard = allowLeadingWildcard;
914     }
915 
isAllowLeadingWildcard()916     public boolean isAllowLeadingWildcard() {
917         return allowLeadingWildcard;
918     }
919 
isQuickContextScan()920     public boolean isQuickContextScan() {
921         return quickContextScan;
922     }
923 
setQuickContextScan(boolean quickContextScan)924     public void setQuickContextScan(boolean quickContextScan) {
925         this.quickContextScan = quickContextScan;
926     }
927 
setIgnoredNames(IgnoredNames ignoredNames)928     public void setIgnoredNames(IgnoredNames ignoredNames) {
929         this.ignoredNames = ignoredNames;
930     }
931 
getIgnoredNames()932     public IgnoredNames getIgnoredNames() {
933         return ignoredNames;
934     }
935 
setIncludedNames(Filter includedNames)936     public void setIncludedNames(Filter includedNames) {
937         this.includedNames = includedNames;
938     }
939 
getIncludedNames()940     public Filter getIncludedNames() {
941         return includedNames;
942     }
943 
setUserPage(String userPage)944     public void setUserPage(String userPage) {
945         this.userPage = userPage;
946     }
947 
getUserPage()948     public String getUserPage() {
949         return userPage;
950     }
951 
setUserPageSuffix(String userPageSuffix)952     public void setUserPageSuffix(String userPageSuffix) {
953         this.userPageSuffix = userPageSuffix;
954     }
955 
getUserPageSuffix()956     public String getUserPageSuffix() {
957         return userPageSuffix;
958     }
959 
setBugPage(String bugPage)960     public void setBugPage(String bugPage) {
961         this.bugPage = bugPage;
962     }
963 
getBugPage()964     public String getBugPage() {
965         return bugPage;
966     }
967 
968     /**
969      * Set the bug pattern to a new value.
970      *
971      * @param bugPattern the new pattern
972      * @throws PatternSyntaxException when the pattern is not a valid regexp or
973      * does not contain at least one capture group and the group does not
974      * contain a single character
975      */
setBugPattern(String bugPattern)976     public void setBugPattern(String bugPattern) throws PatternSyntaxException {
977         if (!bugPattern.matches(PATTERN_SINGLE_GROUP)) {
978             throw new PatternSyntaxException(PATTERN_MUST_CONTAIN_GROUP, bugPattern, 0);
979         }
980         this.bugPattern = Pattern.compile(bugPattern).toString();
981     }
982 
getBugPattern()983     public String getBugPattern() {
984         return bugPattern;
985     }
986 
getReviewPage()987     public String getReviewPage() {
988         return reviewPage;
989     }
990 
setReviewPage(String reviewPage)991     public void setReviewPage(String reviewPage) {
992         this.reviewPage = reviewPage;
993     }
994 
getReviewPattern()995     public String getReviewPattern() {
996         return reviewPattern;
997     }
998 
999     /**
1000      * Set the review pattern to a new value.
1001      *
1002      * @param reviewPattern the new pattern
1003      * @throws PatternSyntaxException when the pattern is not a valid regexp or
1004      * does not contain at least one capture group and the group does not
1005      * contain a single character
1006      */
setReviewPattern(String reviewPattern)1007     public void setReviewPattern(String reviewPattern) throws PatternSyntaxException {
1008         if (!reviewPattern.matches(PATTERN_SINGLE_GROUP)) {
1009             throw new PatternSyntaxException(PATTERN_MUST_CONTAIN_GROUP, reviewPattern, 0);
1010         }
1011         this.reviewPattern = Pattern.compile(reviewPattern).toString();
1012     }
1013 
getWebappLAF()1014     public String getWebappLAF() {
1015         return webappLAF;
1016     }
1017 
setWebappLAF(String webappLAF)1018     public void setWebappLAF(String webappLAF) {
1019         this.webappLAF = webappLAF;
1020     }
1021 
1022     /**
1023      * Gets a value indicating if the web app should run ctags as necessary.
1024      */
isWebappCtags()1025     public boolean isWebappCtags() {
1026         return webappCtags;
1027     }
1028 
1029     /**
1030      * Sets a value indicating if the web app should run ctags as necessary.
1031      */
setWebappCtags(boolean value)1032     public void setWebappCtags(boolean value) {
1033         this.webappCtags = value;
1034     }
1035 
getRemoteScmSupported()1036     public RemoteSCM getRemoteScmSupported() {
1037         return remoteScmSupported;
1038     }
1039 
setRemoteScmSupported(RemoteSCM remoteScmSupported)1040     public void setRemoteScmSupported(RemoteSCM remoteScmSupported) {
1041         this.remoteScmSupported = remoteScmSupported;
1042     }
1043 
isOptimizeDatabase()1044     public boolean isOptimizeDatabase() {
1045         return optimizeDatabase;
1046     }
1047 
setOptimizeDatabase(boolean optimizeDatabase)1048     public void setOptimizeDatabase(boolean optimizeDatabase) {
1049         this.optimizeDatabase = optimizeDatabase;
1050     }
1051 
getLuceneLocking()1052     public LuceneLockName getLuceneLocking() {
1053         return luceneLocking;
1054     }
1055 
1056     /**
1057      * @param value off|on|simple|native where "on" is an alias for "simple".
1058      * Any other value is a fallback alias for "off" (with a logged warning).
1059      */
setLuceneLocking(LuceneLockName value)1060     public void setLuceneLocking(LuceneLockName value) {
1061         this.luceneLocking = value;
1062     }
1063 
setCompressXref(boolean compressXref)1064     public void setCompressXref(boolean compressXref) {
1065         this.compressXref = compressXref;
1066     }
1067 
isCompressXref()1068     public boolean isCompressXref() {
1069         return compressXref;
1070     }
1071 
isIndexVersionedFilesOnly()1072     public boolean isIndexVersionedFilesOnly() {
1073         return indexVersionedFilesOnly;
1074     }
1075 
setIndexVersionedFilesOnly(boolean indexVersionedFilesOnly)1076     public void setIndexVersionedFilesOnly(boolean indexVersionedFilesOnly) {
1077         this.indexVersionedFilesOnly = indexVersionedFilesOnly;
1078     }
1079 
getIndexingParallelism()1080     public int getIndexingParallelism() {
1081         return indexingParallelism;
1082     }
1083 
setIndexingParallelism(int value)1084     public void setIndexingParallelism(int value) {
1085         this.indexingParallelism = value > 0 ? value : 0;
1086     }
1087 
getHistoryParallelism()1088     public int getHistoryParallelism() {
1089         return historyParallelism;
1090     }
1091 
setHistoryParallelism(int value)1092     public void setHistoryParallelism(int value) {
1093         this.historyParallelism = value > 0 ? value : 0;
1094     }
1095 
getHistoryRenamedParallelism()1096     public int getHistoryRenamedParallelism() {
1097         return historyRenamedParallelism;
1098     }
1099 
setHistoryRenamedParallelism(int value)1100     public void setHistoryRenamedParallelism(int value) {
1101         this.historyRenamedParallelism = value > 0 ? value : 0;
1102     }
1103 
isTagsEnabled()1104     public boolean isTagsEnabled() {
1105         return this.tagsEnabled;
1106     }
1107 
setTagsEnabled(boolean tagsEnabled)1108     public void setTagsEnabled(boolean tagsEnabled) {
1109         this.tagsEnabled = tagsEnabled;
1110     }
1111 
setRevisionMessageCollapseThreshold(int threshold)1112     public void setRevisionMessageCollapseThreshold(int threshold) {
1113         this.revisionMessageCollapseThreshold = threshold;
1114     }
1115 
getRevisionMessageCollapseThreshold()1116     public int getRevisionMessageCollapseThreshold() {
1117         return this.revisionMessageCollapseThreshold;
1118     }
1119 
getCurrentIndexedCollapseThreshold()1120     public int getCurrentIndexedCollapseThreshold() {
1121         return currentIndexedCollapseThreshold;
1122     }
1123 
setCurrentIndexedCollapseThreshold(int currentIndexedCollapseThreshold)1124     public void setCurrentIndexedCollapseThreshold(int currentIndexedCollapseThreshold) {
1125         this.currentIndexedCollapseThreshold = currentIndexedCollapseThreshold;
1126     }
1127 
getDisplayRepositories()1128     public boolean getDisplayRepositories() {
1129         return this.displayRepositories;
1130     }
1131 
setDisplayRepositories(boolean flag)1132     public void setDisplayRepositories(boolean flag) {
1133         this.displayRepositories = flag;
1134     }
1135 
getListDirsFirst()1136     public boolean getListDirsFirst() {
1137         return listDirsFirst;
1138     }
1139 
setListDirsFirst(boolean flag)1140     public void setListDirsFirst(boolean flag) {
1141         listDirsFirst = flag;
1142     }
1143 
1144     /**
1145      * The name of the file relative to the <var>DATA_ROOT</var>, which should
1146      * be included into the footer of generated web pages.
1147      */
1148     public static final String FOOTER_INCLUDE_FILE = "footer_include";
1149 
1150     /**
1151      * The name of the file relative to the <var>DATA_ROOT</var>, which should
1152      * be included into the header of generated web pages.
1153      */
1154     public static final String HEADER_INCLUDE_FILE = "header_include";
1155 
1156     /**
1157      * The name of the file relative to the <var>DATA_ROOT</var>, which should
1158      * be included into the body of web app's "Home" page.
1159      */
1160     public static final String BODY_INCLUDE_FILE = "body_include";
1161 
1162     /**
1163      * The name of the file relative to the <var>DATA_ROOT</var>, which should
1164      * be included into the error page handling access forbidden errors - HTTP
1165      * code 403 Forbidden.
1166      */
1167     public static final String E_FORBIDDEN_INCLUDE_FILE = "error_forbidden_include";
1168 
1169 
1170     /**
1171      * The name of the file relative to the <var>DATA_ROOT</var>, which should
1172      * be included into the HTTP header of generated web pages.
1173      */
1174     public static final String HTTP_HEADER_INCLUDE_FILE = "http_header_include";
1175 
1176     /**
1177      * @return path to the file holding compiled path descriptions for the web application
1178      */
getDtagsEftarPath()1179     public Path getDtagsEftarPath() {
1180         return Paths.get(getDataRoot(), "index", EFTAR_DTAGS_NAME);
1181     }
1182 
getCTagsExtraOptionsFile()1183     public String getCTagsExtraOptionsFile() {
1184         return CTagsExtraOptionsFile;
1185     }
1186 
setCTagsExtraOptionsFile(String filename)1187     public void setCTagsExtraOptionsFile(String filename) {
1188         this.CTagsExtraOptionsFile = filename;
1189     }
1190 
getAllowedSymlinks()1191     public Set<String> getAllowedSymlinks() {
1192         return allowedSymlinks;
1193     }
1194 
setAllowedSymlinks(Set<String> allowedSymlinks)1195     public void setAllowedSymlinks(Set<String> allowedSymlinks) {
1196         this.allowedSymlinks = allowedSymlinks;
1197     }
1198 
getCanonicalRoots()1199     public Set<String> getCanonicalRoots() {
1200         return canonicalRoots;
1201     }
1202 
setCanonicalRoots(Set<String> canonicalRoots)1203     public void setCanonicalRoots(Set<String> canonicalRoots) {
1204         this.canonicalRoots = canonicalRoots;
1205     }
1206 
isObfuscatingEMailAddresses()1207     public boolean isObfuscatingEMailAddresses() {
1208         return obfuscatingEMailAddresses;
1209     }
1210 
setObfuscatingEMailAddresses(boolean obfuscate)1211     public void setObfuscatingEMailAddresses(boolean obfuscate) {
1212         this.obfuscatingEMailAddresses = obfuscate;
1213     }
1214 
isChattyStatusPage()1215     public boolean isChattyStatusPage() {
1216         return chattyStatusPage;
1217     }
1218 
setChattyStatusPage(boolean chattyStatusPage)1219     public void setChattyStatusPage(boolean chattyStatusPage) {
1220         this.chattyStatusPage = chattyStatusPage;
1221     }
1222 
isScopesEnabled()1223     public boolean isScopesEnabled() {
1224         return scopesEnabled;
1225     }
1226 
setScopesEnabled(boolean scopesEnabled)1227     public void setScopesEnabled(boolean scopesEnabled) {
1228         this.scopesEnabled = scopesEnabled;
1229     }
1230 
isFoldingEnabled()1231     public boolean isFoldingEnabled() {
1232         return foldingEnabled;
1233     }
1234 
setFoldingEnabled(boolean foldingEnabled)1235     public void setFoldingEnabled(boolean foldingEnabled) {
1236         this.foldingEnabled = foldingEnabled;
1237     }
1238 
getMaxSearchThreadCount()1239     public int getMaxSearchThreadCount() {
1240         return MaxSearchThreadCount;
1241     }
1242 
setMaxSearchThreadCount(int count)1243     public void setMaxSearchThreadCount(int count) {
1244         this.MaxSearchThreadCount = count;
1245     }
1246 
getMaxRevisionThreadCount()1247     public int getMaxRevisionThreadCount() {
1248         return MaxRevisionThreadCount;
1249     }
1250 
setMaxRevisionThreadCount(int count)1251     public void setMaxRevisionThreadCount(int count) {
1252         this.MaxRevisionThreadCount = count;
1253     }
1254 
isProjectsEnabled()1255     public boolean isProjectsEnabled() {
1256         return projectsEnabled;
1257     }
1258 
setProjectsEnabled(boolean flag)1259     public void setProjectsEnabled(boolean flag) {
1260         this.projectsEnabled = flag;
1261     }
1262 
getSuggesterConfig()1263     public SuggesterConfig getSuggesterConfig() {
1264         return suggesterConfig;
1265     }
1266 
setSuggesterConfig(final SuggesterConfig config)1267     public void setSuggesterConfig(final SuggesterConfig config) {
1268         if (config == null) {
1269             throw new IllegalArgumentException("Cannot set Suggester configuration to null");
1270         }
1271         this.suggesterConfig = config;
1272     }
1273 
getDisabledRepositories()1274     public Set<String> getDisabledRepositories() {
1275         return disabledRepositories;
1276     }
1277 
setDisabledRepositories(Set<String> disabledRepositories)1278     public void setDisabledRepositories(Set<String> disabledRepositories) {
1279         this.disabledRepositories = disabledRepositories;
1280     }
1281 
1282     /**
1283      * Write the current configuration to a file.
1284      *
1285      * @param file the file to write the configuration into
1286      * @throws IOException if an error occurs
1287      */
write(File file)1288     public void write(File file) throws IOException {
1289         try (FileOutputStream out = new FileOutputStream(file)) {
1290             this.encodeObject(out);
1291         }
1292     }
1293 
getXMLRepresentationAsString()1294     public String getXMLRepresentationAsString() {
1295         ByteArrayOutputStream bos = new ByteArrayOutputStream();
1296         this.encodeObject(bos);
1297         return bos.toString();
1298     }
1299 
encodeObject(OutputStream out)1300     public void encodeObject(OutputStream out) {
1301         try (XMLEncoder e = new XMLEncoder(new BufferedOutputStream(out))) {
1302             e.writeObject(this);
1303         }
1304     }
1305 
read(File file)1306     public static Configuration read(File file) throws IOException {
1307         LOGGER.log(Level.INFO, "Reading configuration from {0}", file.getCanonicalPath());
1308         try (FileInputStream in = new FileInputStream(file)) {
1309             return decodeObject(in);
1310         }
1311     }
1312 
makeXMLStringAsConfiguration(String xmlconfig)1313     public static Configuration makeXMLStringAsConfiguration(String xmlconfig) throws IOException {
1314         final Configuration ret;
1315         final ByteArrayInputStream in = new ByteArrayInputStream(xmlconfig.getBytes());
1316         ret = decodeObject(in);
1317         return ret;
1318     }
1319 
decodeObject(InputStream in)1320     private static Configuration decodeObject(InputStream in) throws IOException {
1321         final Object ret;
1322         final LinkedList<Exception> exceptions = new LinkedList<>();
1323         ExceptionListener listener = new ExceptionListener() {
1324             @Override
1325             public void exceptionThrown(Exception e) {
1326                 exceptions.addLast(e);
1327             }
1328         };
1329 
1330         try (XMLDecoder d = new XMLDecoder(new BufferedInputStream(in), null, listener)) {
1331             ret = d.readObject();
1332         }
1333 
1334         if (!(ret instanceof Configuration)) {
1335             throw new IOException("Not a valid config file");
1336         }
1337 
1338         if (!exceptions.isEmpty()) {
1339             // There was an exception during parsing.
1340             // see {@code addGroup}
1341             if (exceptions.getFirst() instanceof IOException) {
1342                 throw (IOException) exceptions.getFirst();
1343             }
1344             throw new IOException(exceptions.getFirst());
1345         }
1346 
1347         Configuration conf = ((Configuration) ret);
1348 
1349         // Removes all non root groups.
1350         // This ensures that when the configuration is reloaded then the set
1351         // contains only root groups. Subgroups are discovered again
1352         // as follows below
1353         conf.groups.removeIf(new Predicate<Group>() {
1354             @Override
1355             public boolean test(Group g) {
1356                 return g.getParent() != null;
1357             }
1358         });
1359 
1360         // Traversing subgroups and checking for duplicates,
1361         // effectively transforms the group tree to a structure (Set)
1362         // supporting an iterator.
1363         TreeSet<Group> copy = new TreeSet<>();
1364         LinkedList<Group> stack = new LinkedList<>(conf.groups);
1365         while (!stack.isEmpty()) {
1366             Group group = stack.pollFirst();
1367             stack.addAll(group.getSubgroups());
1368 
1369             if (!copy.add(group)) {
1370                 throw new IOException(
1371                         String.format("Duplicate group name '%s' in configuration.",
1372                                 group.getName()));
1373             }
1374 
1375             // populate groups where the current group in in their subtree
1376             Group tmp = group.getParent();
1377             while (tmp != null) {
1378                 tmp.addDescendant(group);
1379                 tmp = tmp.getParent();
1380             }
1381         }
1382         conf.setGroups(copy);
1383 
1384         /*
1385          * Validate any defined canonicalRoot entries, and only include where
1386          * validation succeeds.
1387          */
1388         if (conf.canonicalRoots != null) {
1389             conf.canonicalRoots = conf.canonicalRoots.stream().filter(s -> {
1390                 String problem = CanonicalRootValidator.validate(s, "canonicalRoot element");
1391                 if (problem == null) {
1392                     return true;
1393                 } else {
1394                     LOGGER.warning(problem);
1395                     return false;
1396                 }
1397             }).collect(Collectors.toCollection(HashSet::new));
1398         }
1399 
1400         return conf;
1401     }
1402 }
1403