/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* See LICENSE.txt included in this distribution for the specific
* language governing permissions and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at LICENSE.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*/
/*
* Copyright (c) 2007, 2017, Oracle and/or its affiliates. All rights reserved.
*/
package org.opensolaris.opengrok.configuration;
import java.beans.ExceptionListener;
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.opensolaris.opengrok.authorization.AuthControlFlag;
import org.opensolaris.opengrok.authorization.AuthorizationStack;
import org.opensolaris.opengrok.history.RepositoryInfo;
import org.opensolaris.opengrok.index.Filter;
import org.opensolaris.opengrok.index.IgnoredNames;
import org.opensolaris.opengrok.logger.LoggerFactory;
/**
* Placeholder class for all configuration variables. Due to the multithreaded
* nature of the web application, each thread will use the same instance of the
* configuration object for each page request. Class and methods should have
* package scope, but that didn't work with the XMLDecoder/XMLEncoder.
*/
public final class Configuration {
private static final Logger LOGGER = LoggerFactory.getLogger(Configuration.class);
public static final String PLUGIN_DIRECTORY_DEFAULT = "plugins";
public static final String STATISTICS_FILE_DEFAULT = "statistics.json";
/**
* A check if a pattern contains at least one pair of parentheses meaning
* that there is at least one capture group. This group must not be empty.
*/
private static final String PATTERN_SINGLE_GROUP = ".*\\([^\\)]+\\).*";
/**
* Error string for invalid patterns without a single group. This is passed
* as a first argument to the constructor of PatternSyntaxException and in
* the output it is followed by the invalid pattern.
*
* @see PatternSyntaxException
* @see #PATTERN_SINGLE_GROUP
*/
private static final String PATTERN_MUST_CONTAIN_GROUP = "The pattern must contain at least one non-empty group -";
/**
* Error string for negative numbers (could be int, double, long, ...).
* First argument is the name of the property, second argument is the actual
* value.
*/
private static final String NEGATIVE_NUMBER_ERROR = "Invalid value for \"%s\" - \"%s\". Expected value greater or equal than 0";
private String ctags;
/**
* Should the history log be cached?
*/
private boolean historyCache;
/**
* The maximum time in milliseconds {@code HistoryCache.get()} can take
* before its result is cached.
*/
private int historyCacheTime;
private int messageLimit;
/**
* Directory with authorization plugins. Default value is
* dataRoot/../plugins (can be /var/opengrok/plugins if dataRoot is
* /var/opengrok/data).
*/
private String pluginDirectory;
/**
* Enable watching the plugin directory for changes in real time. Suitable
* for development.
*/
private boolean authorizationWatchdogEnabled;
private AuthorizationStack pluginStack;
private Map<String,Project> projects; // project name -> Project
private Set<Group> groups;
private String sourceRoot;
private String dataRoot;
private List<RepositoryInfo> repositories;
private String urlPrefix;
private boolean generateHtml;
/**
* Default projects will be used, when no project is selected and no project
* is in cookie, so basically only the first time a page is opened,
* or when web cookies are cleared.
*/
private Set<Project> defaultProjects;
/**
* Default size of memory to be used for flushing of Lucene docs per thread.
* Lucene 4.x uses 16MB and 8 threads, so below is a nice tunable.
*/
private double ramBufferSize;
private boolean verbose;
/**
* If below is set, then we count how many files per project we need to
* process and print percentage of completion per project.
*/
private boolean printProgress;
private boolean allowLeadingWildcard;
private IgnoredNames ignoredNames;
private Filter includedNames;
private String userPage;
private String userPageSuffix;
private String bugPage;
private String bugPattern;
private String reviewPage;
private String reviewPattern;
private String webappLAF;
private RemoteSCM remoteScmSupported;
private boolean optimizeDatabase;
private boolean useLuceneLocking;
private boolean compressXref;
private boolean indexVersionedFilesOnly;
private boolean tagsEnabled;
private int hitsPerPage;
private int cachePages;
private boolean lastEditedDisplayMode;
private String databaseDriver;
private String databaseUrl;
private String CTagsExtraOptionsFile;
private int scanningDepth;
private Set<String> allowedSymlinks;
private boolean obfuscatingEMailAddresses;
private boolean chattyStatusPage;
private final Map<String, String> cmds; // repository type -> command
private int tabSize;
private int command_timeout; // in seconds
private int indexRefreshPeriod; // in seconds
private boolean scopesEnabled;
private boolean foldingEnabled;
private String statisticsFilePath;
/*
* Set to false if we want to disable fetching history of individual files
* (by running appropriate SCM command) when the history is not found
* in history cache for repositories capable of fetching history for
* directories. This option affects file based history cache only.
*/
private boolean fetchHistoryWhenNotInCache;
/*
* Set to false to disable extended handling of history of files across
* renames, i.e. support getting diffs of revisions across renames
* for capable repositories.
*/
private boolean handleHistoryOfRenamedFiles;
public static final double defaultRamBufferSize = 16;
public static final int defaultScanningDepth = 3;
/**
* The name of the eftar file relative to the <var>DATA_ROOT</var>, which
* contains definition tags.
*/
public static final String EFTAR_DTAGS_FILE = "index/dtags.eftar";
private transient File dtagsEftar = null;
/**
* Revision messages will be collapsible if they exceed this many number of
* characters. Front end enforces an appropriate minimum.
*/
private int revisionMessageCollapseThreshold;
/**
* Groups are collapsed if number of repositories is greater than this
* threshold. This applies only for non-favorite groups - groups which don't
* contain a project which is considered as a favorite project for the user.
* Favorite projects are the projects which the user browses and searches
* and are stored in a cookie. Favorite groups are always expanded.
*/
private int groupsCollapseThreshold;
/**
* Current indexed message will be collapsible if they exceed this many
* number of characters. Front end enforces an appropriate minimum.
*/
private int currentIndexedCollapseThreshold;
/**
* Upper bound for number of threads used for performing multi-project
* searches. This is total for the whole webapp/CLI utility.
*/
private int MaxSearchThreadCount;
/*
* types of handling history for remote SCM repositories:
* ON - index history and display it in webapp
* OFF - do not index or display history in webapp
* DIRBASED - index history only for repositories capable
* of getting history for directories
* UIONLY - display history only in webapp (do not index it)
*/
public enum RemoteSCM {
ON, OFF, DIRBASED, UIONLY
}
/**
* Get the default tab size (number of space characters per tab character)
* to use for each project. If {@code <= 0} tabs are read/write as is.
*
* @return current tab size set.
* @see Project#getTabSize()
* @see org.opensolaris.opengrok.analysis.ExpandTabsReader
*/
public int getTabSize() {
return tabSize;
}
/**
* Set the default tab size (number of space characters per tab character)
* to use for each project. If {@code <= 0} tabs are read/write as is.
*
* @param tabSize tabsize to set.
* @see Project#setTabSize(int)
* @see org.opensolaris.opengrok.analysis.ExpandTabsReader
*/
public void setTabSize(int tabSize) {
this.tabSize = tabSize;
}
public int getScanningDepth() {
return scanningDepth;
}
/**
* Set the scanning depth to a new value
*
* @param scanningDepth the new value
* @throws IllegalArgumentException when the scanningDepth is negative
*/
public void setScanningDepth(int scanningDepth) throws IllegalArgumentException {
if (scanningDepth < 0) {
throw new IllegalArgumentException(
String.format(NEGATIVE_NUMBER_ERROR, "scanningDepth", scanningDepth));
}
this.scanningDepth = scanningDepth;
}
public int getCommandTimeout() {
return command_timeout;
}
/**
* Set the command timeout to a new value
*
* @param command_timeout the new value
* @throws IllegalArgumentException when the timeout is negative
*/
public void setCommandTimeout(int command_timeout) throws IllegalArgumentException {
if (command_timeout < 0) {
throw new IllegalArgumentException(
String.format(NEGATIVE_NUMBER_ERROR, "command_timeout", command_timeout));
}
this.command_timeout = command_timeout;
}
public int getIndexRefreshPeriod() {
return indexRefreshPeriod;
}
public void setIndexRefreshPeriod(int seconds) {
this.indexRefreshPeriod = seconds;
}
public String getStatisticsFilePath() {
return statisticsFilePath;
}
public void setStatisticsFilePath(String statisticsFilePath) {
this.statisticsFilePath = statisticsFilePath;
}
public boolean isLastEditedDisplayMode() {
return lastEditedDisplayMode;
}
public void setLastEditedDisplayMode(boolean lastEditedDisplayMode) {
this.lastEditedDisplayMode = lastEditedDisplayMode;
}
public int getGroupsCollapseThreshold() {
return groupsCollapseThreshold;
}
/**
* Set the groups collapse threshold to a new value
*
* @param groupsCollapseThreshold the new value
* @throws IllegalArgumentException when the timeout is negative
*/
public void setGroupsCollapseThreshold(int groupsCollapseThreshold) throws IllegalArgumentException {
if (groupsCollapseThreshold < 0) {
throw new IllegalArgumentException(
String.format(NEGATIVE_NUMBER_ERROR, "groupsCollapseThreshold", groupsCollapseThreshold));
}
this.groupsCollapseThreshold = groupsCollapseThreshold;
}
/**
* Creates a new instance of Configuration
*/
public Configuration() {
//defaults for an opengrok instance configuration
cmds = new HashMap<>();
setAllowedSymlinks(new HashSet<>());
setAuthorizationWatchdogEnabled(false);
setBugPage("http://bugs.myserver.org/bugdatabase/view_bug.do?bug_id=");
setBugPattern("\\b([12456789][0-9]{6})\\b");
setCachePages(5);
setCommandTimeout(600); // 10 minutes
setCompressXref(true);
setCtags(System.getProperty("org.opensolaris.opengrok.analysis.Ctags", "ctags"));
setCurrentIndexedCollapseThreshold(27);
setDataRoot(null);
setFetchHistoryWhenNotInCache(true);
setFoldingEnabled(true);
setGenerateHtml(true);
setGroups(new TreeSet<>());
setGroupsCollapseThreshold(4);
setHandleHistoryOfRenamedFiles(true);
setHistoryCache(true);
setHistoryCacheTime(30);
setHitsPerPage(25);
setIgnoredNames(new IgnoredNames());
setIncludedNames(new Filter());
setIndexRefreshPeriod(3600);
setIndexVersionedFilesOnly(false);
setLastEditedDisplayMode(true);
setMaxSearchThreadCount(2 * Runtime.getRuntime().availableProcessors());
setMessageLimit(500);
setOptimizeDatabase(true);
setPluginDirectory(null);
setPluginStack(new AuthorizationStack(AuthControlFlag.REQUIRED, "default stack"));
setPrintProgress(false);
setProjects(new HashMap<>());
setQuickContextScan(true);
//below can cause an outofmemory error, since it is defaulting to NO LIMIT
setRamBufferSize(defaultRamBufferSize); //MB
setRemoteScmSupported(RemoteSCM.OFF);
setRepositories(new ArrayList<>());
setReviewPage("http://arc.myserver.org/caselog/PSARC/");
setReviewPattern("\\b(\\d{4}/\\d{3})\\b"); // in form e.g. PSARC 2008/305
setRevisionMessageCollapseThreshold(200);
setScanningDepth(defaultScanningDepth); // default depth of scanning for repositories
setScopesEnabled(true);
setSourceRoot(null);
setStatisticsFilePath(null);
//setTabSize(4);
setTagsEnabled(false);
setUrlPrefix("/source/s?");
//setUrlPrefix("../s?"); // TODO generate relative search paths, get rid of -w <webapp> option to indexer !
setUserPage("http://www.myserver.org/viewProfile.jspa?username=");
// Set to empty string so we can append it to the URL
// unconditionally later.
setUserPageSuffix("");
setUsingLuceneLocking(false);
setVerbose(false);
setWebappLAF("default");
}
public String getRepoCmd(String clazzName) {
return cmds.get(clazzName);
}
public String setRepoCmd(String clazzName, String cmd) {
if (clazzName == null) {
return null;
}
if (cmd == null || cmd.length() == 0) {
return cmds.remove(clazzName);
}
return cmds.put(clazzName, cmd);
}
// just to satisfy bean/de|encoder stuff
public Map<String, String> getCmds() {
return Collections.unmodifiableMap(cmds);
}
/**
* @see RuntimeEnvironment#getMessagesInTheSystem()
*
* @return int the current message limit
*/
public int getMessageLimit() {
return messageLimit;
}
/**
* @see RuntimeEnvironment#getMessagesInTheSystem()
*
* @param messageLimit the limit
* @throws IllegalArgumentException when the limit is negative
*/
public void setMessageLimit(int messageLimit) throws IllegalArgumentException {
if (messageLimit < 0) {
throw new IllegalArgumentException(
String.format(NEGATIVE_NUMBER_ERROR, "messageLimit", messageLimit));
}
this.messageLimit = messageLimit;
}
public String getPluginDirectory() {
return pluginDirectory;
}
public void setPluginDirectory(String pluginDirectory) {
this.pluginDirectory = pluginDirectory;
}
public boolean isAuthorizationWatchdogEnabled() {
return authorizationWatchdogEnabled;
}
public void setAuthorizationWatchdogEnabled(boolean authorizationWatchdogEnabled) {
this.authorizationWatchdogEnabled = authorizationWatchdogEnabled;
}
public AuthorizationStack getPluginStack() {
return pluginStack;
}
public void setPluginStack(AuthorizationStack pluginStack) {
this.pluginStack = pluginStack;
}
public void setCmds(Map<String, String> cmds) {
this.cmds.clear();
this.cmds.putAll(cmds);
}
public String getCtags() {
return ctags;
}
public void setCtags(String ctags) {
this.ctags = ctags;
}
public int getCachePages() {
return cachePages;
}
/**
* Set the cache pages to a new value
*
* @param cachePages the new value
* @throws IllegalArgumentException when the cachePages is negative
*/
public void setCachePages(int cachePages) throws IllegalArgumentException {
if (cachePages < 0) {
throw new IllegalArgumentException(
String.format(NEGATIVE_NUMBER_ERROR, "cachePages", cachePages));
}
this.cachePages = cachePages;
}
public int getHitsPerPage() {
return hitsPerPage;
}
/**
* Set the hits per page to a new value
*
* @param hitsPerPage the new value
* @throws IllegalArgumentException when the hitsPerPage is negative
*/
public void setHitsPerPage(int hitsPerPage) throws IllegalArgumentException {
if (hitsPerPage < 0) {
throw new IllegalArgumentException(
String.format(NEGATIVE_NUMBER_ERROR, "hitsPerPage", hitsPerPage));
}
this.hitsPerPage = hitsPerPage;
}
/**
* Should the history log be cached?
*
* @return {@code true} if a {@code HistoryCache} implementation should be
* used, {@code false} otherwise
*/
public boolean isHistoryCache() {
return historyCache;
}
/**
* Set whether history should be cached.
*
* @param historyCache if {@code true} enable history cache
*/
public void setHistoryCache(boolean historyCache) {
this.historyCache = historyCache;
}
/**
* How long can a history request take before it's cached? If the time is
* exceeded, the result is cached. This setting only affects
* {@code FileHistoryCache}.
*
* @return the maximum time in milliseconds a history request can take
* before it's cached
*/
public int getHistoryCacheTime() {
return historyCacheTime;
}
/**
* Set the maximum time a history request can take before it's cached. This
* setting is only respected if {@code FileHistoryCache} is used.
*
* @param historyCacheTime maximum time in milliseconds
*/
public void setHistoryCacheTime(int historyCacheTime) {
this.historyCacheTime = historyCacheTime;
}
public boolean isFetchHistoryWhenNotInCache() {
return fetchHistoryWhenNotInCache;
}
public void setFetchHistoryWhenNotInCache(boolean nofetch) {
this.fetchHistoryWhenNotInCache = nofetch;
}
public boolean isHandleHistoryOfRenamedFiles() {
return handleHistoryOfRenamedFiles;
}
public void setHandleHistoryOfRenamedFiles(boolean enable) {
this.handleHistoryOfRenamedFiles = enable;
}
public Map<String,Project> getProjects() {
return projects;
}
public void setProjects(Map<String,Project> projects) {
this.projects = projects;
}
/**
* Adds a group to the set. This is performed upon configuration parsing
*
* @param group group
* @throws IOException when group is not unique across the set
*/
public void addGroup(Group group) throws IOException {
if (!groups.add(group)) {
throw new IOException(
String.format("Duplicate group name '%s' in configuration.",
group.getName()));
}
}
public Set<Group> getGroups() {
return groups;
}
public void setGroups(Set<Group> groups) {
this.groups = groups;
}
public String getSourceRoot() {
return sourceRoot;
}
public void setSourceRoot(String sourceRoot) {
this.sourceRoot = sourceRoot;
}
public String getDataRoot() {
return dataRoot;
}
/**
* Sets data root.
*
* This method also sets the pluginDirectory if it is not already set and
* also this method sets the statisticsFilePath if it is not already set
*
* @see #setPluginDirectory(java.lang.String)
* @see #setStatisticsFilePath(java.lang.String)
*
* @param dataRoot
*/
public void setDataRoot(String dataRoot) {
if (dataRoot != null && getPluginDirectory() == null) {
setPluginDirectory(dataRoot + "/../" + PLUGIN_DIRECTORY_DEFAULT);
}
if (dataRoot != null && getStatisticsFilePath() == null) {
setStatisticsFilePath(dataRoot + "/" + STATISTICS_FILE_DEFAULT);
}
this.dataRoot = dataRoot;
}
public List<RepositoryInfo> getRepositories() {
return repositories;
}
public void setRepositories(List<RepositoryInfo> repositories) {
this.repositories = repositories;
}
public String getUrlPrefix() {
return urlPrefix;
}
/**
* Set the URL prefix to be used by the {@link
* org.opensolaris.opengrok.analysis.executables.JavaClassAnalyzer} as well
* as lexers (see {@link org.opensolaris.opengrok.analysis.JFlexXref}) when
* they create output with html links.
*
* @param urlPrefix prefix to use.
*/
public void setUrlPrefix(String urlPrefix) {
this.urlPrefix = urlPrefix;
}
public void setGenerateHtml(boolean generateHtml) {
this.generateHtml = generateHtml;
}
public boolean isGenerateHtml() {
return generateHtml;
}
public void setDefaultProjects(Set<Project> defaultProjects) {
this.defaultProjects = defaultProjects;
}
public Set<Project> getDefaultProjects() {
return defaultProjects;
}
public double getRamBufferSize() {
return ramBufferSize;
}
/**
* set size of memory to be used for flushing docs (default 16 MB) (this can
* improve index speed a LOT) note that this is per thread (lucene uses 8
* threads by default in 4.x)
*
* @param ramBufferSize new size in MB
*/
public void setRamBufferSize(double ramBufferSize) {
this.ramBufferSize = ramBufferSize;
}
public boolean isVerbose() {
return verbose;
}
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
public boolean isPrintProgress() {
return printProgress;
}
public void setPrintProgress(boolean printProgress) {
this.printProgress = printProgress;
}
public void setAllowLeadingWildcard(boolean allowLeadingWildcard) {
this.allowLeadingWildcard = allowLeadingWildcard;
}
public boolean isAllowLeadingWildcard() {
return allowLeadingWildcard;
}
private boolean quickContextScan;
public boolean isQuickContextScan() {
return quickContextScan;
}
public void setQuickContextScan(boolean quickContextScan) {
this.quickContextScan = quickContextScan;
}
public void setIgnoredNames(IgnoredNames ignoredNames) {
this.ignoredNames = ignoredNames;
}
public IgnoredNames getIgnoredNames() {
return ignoredNames;
}
public void setIncludedNames(Filter includedNames) {
this.includedNames = includedNames;
}
public Filter getIncludedNames() {
return includedNames;
}
public void setUserPage(String userPage) {
this.userPage = userPage;
}
public String getUserPage() {
return userPage;
}
public void setUserPageSuffix(String userPageSuffix) {
this.userPageSuffix = userPageSuffix;
}
public String getUserPageSuffix() {
return userPageSuffix;
}
public void setBugPage(String bugPage) {
this.bugPage = bugPage;
}
public String getBugPage() {
return bugPage;
}
/**
* Set the bug pattern to a new value
*
* @param bugPattern the new pattern
* @throws PatternSyntaxException when the pattern is not a valid regexp or
* does not contain at least one capture group and the group does not
* contain a single character
*/
public void setBugPattern(String bugPattern) throws PatternSyntaxException {
if (!bugPattern.matches(PATTERN_SINGLE_GROUP)) {
throw new PatternSyntaxException(PATTERN_MUST_CONTAIN_GROUP, bugPattern, 0);
}
this.bugPattern = Pattern.compile(bugPattern).toString();
}
public String getBugPattern() {
return bugPattern;
}
public String getReviewPage() {
return reviewPage;
}
public void setReviewPage(String reviewPage) {
this.reviewPage = reviewPage;
}
public String getReviewPattern() {
return reviewPattern;
}
/**
* Set the review pattern to a new value
*
* @param reviewPattern the new pattern
* @throws PatternSyntaxException when the pattern is not a valid regexp or
* does not contain at least one capture group and the group does not
* contain a single character
*/
public void setReviewPattern(String reviewPattern) throws PatternSyntaxException {
if (!reviewPattern.matches(PATTERN_SINGLE_GROUP)) {
throw new PatternSyntaxException(PATTERN_MUST_CONTAIN_GROUP, reviewPattern, 0);
}
this.reviewPattern = Pattern.compile(reviewPattern).toString();
}
public String getWebappLAF() {
return webappLAF;
}
public void setWebappLAF(String webappLAF) {
this.webappLAF = webappLAF;
}
public RemoteSCM getRemoteScmSupported() {
return remoteScmSupported;
}
public void setRemoteScmSupported(RemoteSCM remoteScmSupported) {
this.remoteScmSupported = remoteScmSupported;
}
public boolean isOptimizeDatabase() {
return optimizeDatabase;
}
public void setOptimizeDatabase(boolean optimizeDatabase) {
this.optimizeDatabase = optimizeDatabase;
}
public boolean isUsingLuceneLocking() {
return useLuceneLocking;
}
public void setUsingLuceneLocking(boolean useLuceneLocking) {
this.useLuceneLocking = useLuceneLocking;
}
public void setCompressXref(boolean compressXref) {
this.compressXref = compressXref;
}
public boolean isCompressXref() {
return compressXref;
}
public boolean isIndexVersionedFilesOnly() {
return indexVersionedFilesOnly;
}
public void setIndexVersionedFilesOnly(boolean indexVersionedFilesOnly) {
this.indexVersionedFilesOnly = indexVersionedFilesOnly;
}
public boolean isTagsEnabled() {
return this.tagsEnabled;
}
public void setTagsEnabled(boolean tagsEnabled) {
this.tagsEnabled = tagsEnabled;
}
public void setRevisionMessageCollapseThreshold(int threshold) {
this.revisionMessageCollapseThreshold = threshold;
}
public int getRevisionMessageCollapseThreshold() {
return this.revisionMessageCollapseThreshold;
}
public int getCurrentIndexedCollapseThreshold() {
return currentIndexedCollapseThreshold;
}
public void setCurrentIndexedCollapseThreshold(int currentIndexedCollapseThreshold) {
this.currentIndexedCollapseThreshold = currentIndexedCollapseThreshold;
}
private transient Date lastModified;
/**
* Get the date of the last index update.
*
* @return the time of the last index update.
*/
public Date getDateForLastIndexRun() {
if (lastModified == null) {
File timestamp = new File(getDataRoot(), "timestamp");
if (timestamp.exists()) {
lastModified = new Date(timestamp.lastModified());
}
}
return lastModified;
}
public void refreshDateForLastIndexRun() {
lastModified = null;
}
/**
* Get the contents of a file or empty string if the file cannot be read.
*/
private static String getFileContent(File file) {
if (file == null || !file.canRead()) {
return "";
}
FileReader fin = null;
BufferedReader input = null;
try {
fin = new FileReader(file);
input = new BufferedReader(fin);
String line;
StringBuilder contents = new StringBuilder();
String EOL = System.getProperty("line.separator");
while ((line = input.readLine()) != null) {
contents.append(line).append(EOL);
}
return contents.toString();
} catch (java.io.FileNotFoundException e) {
/*
* should usually not happen
*/
} catch (java.io.IOException e) {
LOGGER.log(Level.WARNING, "failed to read header include file: {0}", e.getMessage());
} finally {
if (input != null) {
try {
input.close();
} catch (Exception e) {
/*
* nothing we can do about it
*/ }
} else if (fin != null) {
try {
fin.close();
} catch (Exception e) {
/*
* nothing we can do about it
*/ }
}
}
return "";
}
/**
* The name of the file relative to the <var>DATA_ROOT</var>, which should
* be included into the footer of generated web pages.
*/
public static final String FOOTER_INCLUDE_FILE = "footer_include";
private transient String footer = null;
/**
* Get the contents of the footer include file.
*
* @return an empty string if it could not be read successfully, the
* contents of the file otherwise.
* @see #FOOTER_INCLUDE_FILE
*/
public String getFooterIncludeFileContent() {
if (footer == null) {
footer = getFileContent(new File(getDataRoot(), FOOTER_INCLUDE_FILE));
}
return footer;
}
/**
* The name of the file relative to the <var>DATA_ROOT</var>, which should
* be included into the header of generated web pages.
*/
public static final String HEADER_INCLUDE_FILE = "header_include";
private transient String header = null;
/**
* Get the contents of the header include file.
*
* @return an empty string if it could not be read successfully, the
* contents of the file otherwise.
* @see #HEADER_INCLUDE_FILE
*/
public String getHeaderIncludeFileContent() {
if (header == null) {
header = getFileContent(new File(getDataRoot(), HEADER_INCLUDE_FILE));
}
return header;
}
/**
* The name of the file relative to the <var>DATA_ROOT</var>, which should
* be included into the body of web app's "Home" page.
*/
public static final String BODY_INCLUDE_FILE = "body_include";
private transient String body = null;
/**
* Get the contents of the body include file.
*
* @return an empty string if it could not be read successfully, the
* contents of the file otherwise.
* @see Configuration#BODY_INCLUDE_FILE
*/
public String getBodyIncludeFileContent() {
if (body == null) {
body = getFileContent(new File(getDataRoot(), BODY_INCLUDE_FILE));
}
return body;
}
/**
* The name of the file relative to the <var>DATA_ROOT</var>, which should
* be included into the error page handling access forbidden errors - HTTP
* code 403 Forbidden.
*/
public static final String E_FORBIDDEN_INCLUDE_FILE = "error_forbidden_include";
private transient String eforbidden_content = null;
/**
* Get the contents of the page for forbidden error page (403 Forbidden)
* include file.
*
* @return an empty string if it could not be read successfully, the
* contents of the file otherwise.
* @see Configuration#E_FORBIDDEN_INCLUDE_FILE
*/
public String getForbiddenIncludeFileContent() {
if (eforbidden_content == null) {
eforbidden_content = getFileContent(new File(getDataRoot(), E_FORBIDDEN_INCLUDE_FILE));
}
return eforbidden_content;
}
/**
* Get the eftar file, which contains definition tags.
*
* @return {@code null} if there is no such file, the file otherwise.
*/
public File getDtagsEftar() {
if (dtagsEftar == null) {
File tmp = new File(getDataRoot() + "/" + EFTAR_DTAGS_FILE);
if (tmp.canRead()) {
dtagsEftar = tmp;
}
}
return dtagsEftar;
}
public String getCTagsExtraOptionsFile() {
return CTagsExtraOptionsFile;
}
public void setCTagsExtraOptionsFile(String filename) {
this.CTagsExtraOptionsFile = filename;
}
public Set<String> getAllowedSymlinks() {
return allowedSymlinks;
}
public void setAllowedSymlinks(Set<String> allowedSymlinks) {
this.allowedSymlinks = allowedSymlinks;
}
public boolean isObfuscatingEMailAddresses() {
return obfuscatingEMailAddresses;
}
public void setObfuscatingEMailAddresses(boolean obfuscate) {
this.obfuscatingEMailAddresses = obfuscate;
}
public boolean isChattyStatusPage() {
return chattyStatusPage;
}
public void setChattyStatusPage(boolean chattyStatusPage) {
this.chattyStatusPage = chattyStatusPage;
}
public boolean isScopesEnabled() {
return scopesEnabled;
}
public void setScopesEnabled(boolean scopesEnabled) {
this.scopesEnabled = scopesEnabled;
}
public boolean isFoldingEnabled() {
return foldingEnabled;
}
public void setFoldingEnabled(boolean foldingEnabled) {
this.foldingEnabled = foldingEnabled;
}
public int getMaxSearchThreadCount() {
return MaxSearchThreadCount;
}
public void setMaxSearchThreadCount(int count) {
this.MaxSearchThreadCount = count;
}
/**
* Write the current configuration to a file
*
* @param file the file to write the configuration into
* @throws IOException if an error occurs
*/
public void write(File file) throws IOException {
try (FileOutputStream out = new FileOutputStream(file)) {
this.encodeObject(out);
}
}
public String getXMLRepresentationAsString() {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
this.encodeObject(bos);
return bos.toString();
}
private void encodeObject(OutputStream out) {
try (XMLEncoder e = new XMLEncoder(new BufferedOutputStream(out))) {
e.writeObject(this);
}
}
public static Configuration read(File file) throws IOException {
try (FileInputStream in = new FileInputStream(file)) {
return decodeObject(in);
}
}
public static Configuration makeXMLStringAsConfiguration(String xmlconfig) throws IOException {
final Configuration ret;
final ByteArrayInputStream in = new ByteArrayInputStream(xmlconfig.getBytes());
ret = decodeObject(in);
return ret;
}
private static Configuration decodeObject(InputStream in) throws IOException {
final Object ret;
final LinkedList<Exception> exceptions = new LinkedList<>();
ExceptionListener listener = new ExceptionListener() {
@Override
public void exceptionThrown(Exception e) {
exceptions.addLast(e);
}
};
try (XMLDecoder d = new XMLDecoder(new BufferedInputStream(in), null, listener)) {
ret = d.readObject();
}
if (!(ret instanceof Configuration)) {
throw new IOException("Not a valid config file");
}
if (!exceptions.isEmpty()) {
// There was an exception during parsing.
// see {@code addGroup}
if (exceptions.getFirst() instanceof IOException) {
throw (IOException) exceptions.getFirst();
}
throw new IOException(exceptions.getFirst());
}
Configuration conf = ((Configuration) ret);
// Removes all non root groups.
// This ensures that when the configuration is reloaded then the set
// contains only root groups. Subgroups are discovered again
// as follows below
conf.groups.removeIf(new Predicate<Group>() {
@Override
public boolean test(Group g) {
return g.getParent() != null;
}
});
// Traversing subgroups and checking for duplicates,
// effectively transforms the group tree to a structure (Set)
// supporting an iterator.
TreeSet<Group> copy = new TreeSet<>();
LinkedList<Group> stack = new LinkedList<>(conf.groups);
while (!stack.isEmpty()) {
Group group = stack.pollFirst();
stack.addAll(group.getSubgroups());
if (!copy.add(group)) {
throw new IOException(
String.format("Duplicate group name '%s' in configuration.",
group.getName()));
}
// populate groups where the current group in in their subtree
Group tmp = group.getParent();
while (tmp != null) {
tmp.addDescendant(group);
tmp = tmp.getParent();
}
}
conf.setGroups(copy);
return conf;
}
}