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