blob: dabf0f7036a8f3b2a6063fa0c2642e0b8a13b026 [file] [log] [blame]
// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.enterprise.adaptor;
import java.io.*;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.*;
/**
* Configuration values for this program, like the GSA's hostname. Also several
* knobs, or controls, for changing the behavior of the program.
* <p>All available configuration:<br>
* <style type="text/css"> td { padding-right:2em; } </style>
* <table>
* <tr><td align=center><b>required?</b></td>
* <td><b>name</b></td><td><b>meaning</b></td>
* <tr><td> </td><td>gsa.acceptsDocControlsHeader </td><td>use
* X-Gsa-Doc-Controls HTTP header with namespaced ACLs.
* Otherwise ACLs are sent without namespace and as metadata.
* If not set, then an attempt to compute from gsa.version is made.
* Defaults to true for 7.2.0-0 and later, and false for earlier,
* as defined by gsa.version.
* <tr><td> </td><td>adaptor.fullListingSchedule </td><td> when to invoke
* {@link Adaptor#getDocIds Adaptor.getDocIds}, in cron format (minute,
* hour, day of month, month, day of week). Defaults to 0 3 * * *
* <tr><td> </td><td>adaptor.incrementalPollPeriodSecs </td><td> number
* of seconds between invocations of {@link
* PollingIncrementalLister#getModifiedDocIds
* PollingIncrementalLister.getModifiedDocIds}. Defaults to 900
* <tr><td> </td><td>adaptor.docContentTimeoutSecs </td><td> number of seconds
* adaptor has to complete sending content before it is interrupted. Timing
* starts when sending content starts. Defaults to 180
* <tr><td> </td><td>adaptor.docHeaderTimeoutSecs </td><td> number of seconds
* adaptor has to start sending content before it is interrupted.
* Defaults to 30
* <tr><td> </td><td>adaptor.pushDocIdsOnStartup </td><td> whether to invoke
* {@link Adaptor#getDocIds Adaptor.getDocIds} on process start
* (in addition to adaptor.fullListingSchedule). Defaults to true
* <tr><td> </td><td>docId.isUrl </td><td> say your adaptor's document ids
* are already URLs and avoid them being inserted into adaptor
generated URLs. Defaults to false
* <tr><td> </td><td>feed.crawlImmediatelyBitEnabled </td><td> send bit telling
* GSA to crawl immediately. Defaults to false
* <tr><td> </td><td>feed.maxUrls </td><td> set max number of URLs included
* per feed file. Defaults to 5000
* <tr><td> </td><td>feed.name </td><td> source name used in feeds. Generated
* if not provided
* <tr><td> </td><td>feed.noRecrawlBitEnabled </td><td> send bit telling
* GSA to crawl your documents only once. Defaults to false
* <tr><td> </td><td>gsa.version </td><td> version number used to configure
* expected GSA features. Defaults to acquiring from GSA.
* Uses 7.0.14-114 if acquiring fails.
* <tr><td> </td><td>gsa.614FeedWorkaroundEnabled </td><td> enable detour
* around particular feed parsing failure found in GSA version 6.14 .
* Defaults to false
* <tr><td> </td><td>gsa.70AuthMethodWorkaroundEnabled </td><td> send authmethod
* in feed files to workaround early GSA 7.0 bug. Defaults to false
* <tr><td> </td><td>gsa.characterEncoding </td><td> character set used
* in feed files. Defaults to UTF-8
* <tr><td align="center"> yes </td><td>gsa.hostname </td><td> machine to
* send feed files to. Process errors if not provided
* <tr><td> </td><td>gsa.samlEntityId </td><td> The SAML Entity ID that
* identifies the GSA. Defaults to
* http://google.com/enterprise/gsa/security-manager
* <tr><td> </td><td>journal.reducedMem </td><td> avoid tracking per URL
* information in RAM; suggested with over five hundred thousand documents.
* Defaults to true
* <tr><td> </td><td>gsa.scoringType</td><td> type of relevance algorithm
* GSA utilizes to rank documents. Either content or web. Is sent
* when adaptor.sendDocControlHeader is true. Defaults to content
* <tr><td> </td><td>server.dashboardPort </td><td> port on adaptor's
* machine for accessing adaptor's dashboard. Defaults to 5679
* <tr><td> </td><td>server.docIdPath </td><td> part of URL preceding
* encoded document ids. Defaults to /doc/
* <tr><td> </td><td>server.fullAccessHosts </td><td> hosts allowed access
* without authentication
* (certificates still needed when in secure mode). Defaults to
* empty but implicitly contains gsa.hostname
* <tr><td> </td><td>server.hostname </td><td>
* hostname of adaptor machine for URL generation.
* The GSA will use this hostname to crawl the adaptor.
* Defaults to lowercase of automatically detected hostname
* <tr><td> </td><td>server.keyAlias </td><td> keystore alias where
* encryption (public and private) keys are stored.
* Defaults to adaptor
* <tr><td> </td><td>server.maxWorkerThreads </td><td> number of maximum
* simultenous retrievals allowed. Defaults to 16
* <tr><td> </td><td>server.port </td><td> retriever port. Defaults to 5678
* <tr><td> </td><td>server.queueCapacity </td><td> max retriever queue size.
* Defaults to 160
* <tr><td> </td><td>server.reverseProxyPort </td><td> port used in
* retriever URLs (in case requests
* are routed through a reverse proxy). Defaults to server.port
* <tr><td> </td><td>server.reverseProxyProtocol </td><td> either http or https,
* depending on proxy traffic. Defaults to https in secure
* mode and http otherwise
* <tr><td> </td><td>server.samlEntityId </td><td> The SAML Entity ID that the
* Adaptor will use to identity itself. Defaults to
* http://google.com/enterprise/gsa/adaptor
* <tr><td> </td><td>server.secure </td><td> enables https and certificate
* checking. Defaults to false
* <tr><td> </td><td>server.useCompression </td><td> compress retrieval
* responses. Defaults to true
* <tr><td> </td><td>transform.acl.X </td><td> where X is an integer, match
* and modify principals as described. Defaults no modifications
* <tr><td> </td><td>transform.pipeline </td><td> sequence of
* transformation steps. Defaults to no-pipeline
* </table>
*/
public class Config {
private static final Logger log = Logger.getLogger(Config.class.getName());
/** Configuration keys whose default value is {@code null}. */
private final Set<String> noDefaultConfig = new HashSet<String>();
/** Default configuration values. */
private final Properties defaultConfig = new Properties();
/** Overriding configuration values loaded from command line. */
// Reads require no additional locks, but modifications require lock on 'this'
// to prevent lost updates.
private volatile Properties config = new Properties(defaultConfig);
/**
* The actual config file in use, or {@code null} if none have been loaded.
*/
private File configFile;
private long configFileLastModified;
private List<ConfigModificationListener> modificationListeners
= new CopyOnWriteArrayList<ConfigModificationListener>();
/**
* Map from config key to computer that generates the value for the key. These
* generated values are generally due to one value being formed from other
* values by default.
*/
private Map<String, ValueComputer> computeMap
= new HashMap<String, ValueComputer>();
public Config() {
String hostname = null;
try {
hostname = InetAddress.getLocalHost().getCanonicalHostName();
hostname = hostname.toLowerCase(Locale.ENGLISH); // work around GSA 7.0
} catch (UnknownHostException ex) {
// Ignore
}
addKey("server.hostname", hostname);
addKey("server.port", "5678");
addKey("server.reverseProxyPort", "GENERATE", new ValueComputer() {
public String compute(String rawValue) {
if ("GENERATE".equals(rawValue)) {
return getValue("server.port");
}
return rawValue;
}
});
addKey("server.reverseProxyProtocol", "GENERATE", new ValueComputer() {
public String compute(String rawValue) {
if ("GENERATE".equals(rawValue)) {
return isServerSecure() ? "https" : "http";
}
return rawValue;
}
});
addKey("server.dashboardPort", "5679");
addKey("server.docIdPath", "/doc/");
addKey("server.fullAccessHosts", "");
addKey("server.secure", "false");
addKey("server.keyAlias", "adaptor");
addKey("server.maxWorkerThreads", "16");
// A queue that takes one second to drain, assuming 16 threads and 100 ms
// for each request.
addKey("server.queueCapacity", "160");
addKey("server.useCompression", "true");
addKey("server.samlEntityId", "http://google.com/enterprise/gsa/adaptor");
addKey("gsa.hostname", null);
addKey("gsa.characterEncoding", "UTF-8");
addKey("gsa.version", "GENERATE");
addKey("gsa.614FeedWorkaroundEnabled", "false");
addKey("gsa.70AuthMethodWorkaroundEnabled", "false");
addKey("gsa.samlEntityId",
"http://google.com/enterprise/gsa/security-manager");
addKey("gsa.scoringType", "content");
addKey("docId.isUrl", "false");
addKey("feed.name", "GENERATE", new ValueComputer() {
public String compute(String rawValue) {
if ("GENERATE".equals(rawValue)) {
return "adaptor_" + getValue("server.hostname").replace('.', '-')
+ "_" + getValue("server.port");
}
return rawValue;
}
});
addKey("feed.noRecrawlBitEnabled", "false");
addKey("feed.crawlImmediatelyBitEnabled", "false");
//addKey("feed.noFollowBitEnabled", "false");
addKey("feed.maxUrls", "5000");
addKey("adaptor.pushDocIdsOnStartup", "true");
// 3:00 AM every day.
addKey("adaptor.fullListingSchedule", "0 3 * * *");
// 15 minutes.
addKey("adaptor.incrementalPollPeriodSecs", "900");
addKey("adaptor.docContentTimeoutSecs", "180");
addKey("adaptor.docHeaderTimeoutSecs", "30");
addKey("transform.pipeline", "");
addKey("journal.reducedMem", "true");
addKey("gsa.acceptsDocControlsHeader", "GENERATE", new ValueComputer() {
public String compute(String rawValue) {
if (!"GENERATE".equals(rawValue)) {
log.log(Level.FINE,
"returning raw gsa.acceptsDocControlsHeader: {0}", rawValue);
return rawValue;
}
String ver = getValue("gsa.version");
if ("GENERATE".equals(ver)) {
throw new IllegalStateException("gsa.version not yet available");
} else {
boolean computed = new GsaVersion(ver).isAtLeast("7.2.0-0");
log.log(Level.FINE,
"gsa.acceptsDocControlsHeader computed {0}", computed);
return "" + computed;
}
}
});
}
public Set<String> getAllKeys() {
return config.stringPropertyNames();
}
/* Preferences requiring you to set them: */
/**
* Required to be set: GSA machine to send document ids to. This is the
* hostname of your GSA on your network.
*/
String getGsaHostname() {
return getValue("gsa.hostname");
}
/* Preferences suggested you set them: */
String getFeedName() {
return getValue("feed.name");
}
/**
* Suggested to be set: Local port, on this computer, onto which requests from
* GSA come in on.
*/
int getServerPort() {
return Integer.parseInt(getValue("server.port"));
}
/**
* The port that should be used in feed file and other references to the
* adaptor. This does not affect the actual port the adaptor uses.
*/
int getServerReverseProxyPort() {
return Integer.parseInt(getValue("server.reverseProxyPort"));
}
/**
* The protocol that should be used in feed files and other references to the
* adaptor. This does not affect the actual protocol the adaptor uses.
*/
String getServerReverseProxyProtocol() {
return getValue("server.reverseProxyProtocol");
}
/**
* Local port, on this computer, from which the dashboard is served.
*/
int getServerDashboardPort() {
return Integer.parseInt(getValue("server.dashboardPort"));
}
/* More sophisticated preferences that can be left
unmodified for simple deployment and initial POC: */
/**
* Optional (default false): If your DocIds are already valid URLs you can
* have this method return true and they will be sent to GSA unmodified. If
* your DocId is like http://procurement.corp.company.com/internal/011212.html
* you can turn this true and that URL will be handed to the GSA.
*
* <p>By default DocIds are URL encoded and prefixed with http:// and this
* host's name and port.
*/
boolean isDocIdUrl() {
return Boolean.parseBoolean(getValue("docId.isUrl"));
}
/** Default is lowercase of InetAddress.getLocalHost().getHostName(). */
String getServerHostname() {
String hostname = getValue("server.hostname");
log.log(Level.FINER, "server hostname: {0}", hostname);
return hostname;
}
/**
* Comma-separated list of IPs or hostnames that can retrieve content without
* authentication checks. The GSA's hostname is implicitly in this list.
*
* <p>When in secure mode, clients are requested to provide a client
* certificate. If the provided client certificate is valid and the Common
* Name (CN) of the Subject is in this list (case-insensitively), then it is
* given access.
*
* <p>In non-secure mode, the hostnames in this list are resolved to IPs at
* startup and when a request is made from one of those IPs the client is
* given access.
*/
String[] getServerFullAccessHosts() {
return getValue("server.fullAccessHosts").split(",");
}
/**
* Optional: Returns this host's base URI which other paths will be resolved
* against. It is used to construct URIs to provide to the GSA for it to
* contact this server for various services.
*
* <p>It contains the protocol, hostname, and port.
*/
URI getServerBaseUri() {
return URI.create(getServerReverseProxyProtocol() + "://"
+ getServerHostname() + ":" + getServerReverseProxyPort());
}
/**
* Optional: Path below {@link #getServerBaseUri(DocId)} where documents are
* namespaced. Generally, should be at least {@code "/"} and end with a slash.
*/
String getServerDocIdPath() {
return getValue("server.docIdPath");
}
/**
* Whether full security should be enabled. When {@code true}, the adaptor is
* locked down using HTTPS, checks certificates, and generally behaves in a
* fully-secure manner. When {@code false} (default), the adaptor serves
* content over HTTP and is unable to authenticate users (all users are
* treated as anonymous).
*
* <p>The need for this setting is because when enabled, security requires a
* reasonable amount of configuration and know-how. To provide easy
* out-of-the-box execution, this is disabled by default.
*/
boolean isServerSecure() {
return Boolean.parseBoolean(getValue("server.secure"));
}
/**
* The alias in the keystore that has the key to use for encryption.
*/
String getServerKeyAlias() {
return getValue("server.keyAlias");
}
/**
* The maximum number of worker threads to use to respond to document
* requests.
*/
int getServerMaxWorkerThreads() {
return Integer.parseInt(getValue("server.maxWorkerThreads"));
}
/**
* The maximum request queue length.
*/
int getServerQueueCapacity() {
return Integer.parseInt(getValue("server.queueCapacity"));
}
String getServerSamlEntityId() {
return getValue("server.samlEntityId");
}
boolean isServerToUseCompression() {
return Boolean.parseBoolean(getValue("server.useCompression"));
}
boolean doesGsaAcceptDocControlsHeader() {
return Boolean.parseBoolean(getValue("gsa.acceptsDocControlsHeader"));
}
/**
* Optional (default false): Adds no-recrawl bit with sent records in feed
* file. If connector handles updates and deletes then GSA does not have to
* recrawl periodically to notice that a document is changed or deleted.
*/
boolean isFeedNoRecrawlBitEnabled() {
return Boolean.getBoolean(getValue("feed.noRecrawlBitEnabled"));
}
/**
* Optional (default false): Adds crawl-immediately bit with sent records in
* feed file. This bit makes the sent URL get crawl priority.
*/
boolean isCrawlImmediatelyBitEnabled() {
return Boolean.parseBoolean(getValue("feed.crawlImmediatelyBitEnabled"));
}
/**
* Whether the default {@code main()} should automatically start pushing all
* document ids on startup. Defaults to {@code true}.
*/
boolean isAdaptorPushDocIdsOnStartup() {
return Boolean.parseBoolean(getValue("adaptor.pushDocIdsOnStartup"));
}
/**
* Cron-style format for describing when the adaptor should perform full
* listings of {@code DocId}s. Multiple times can be specified by separating
* them with a '|' (vertical bar).
*/
String getAdaptorFullListingSchedule() {
return getValue("adaptor.fullListingSchedule");
}
long getAdaptorIncrementalPollPeriodMillis() {
return Long.parseLong(getValue("adaptor.incrementalPollPeriodSecs")) * 1000;
}
long getAdaptorDocHeaderTimeoutMillis() {
return Long.parseLong(getValue("adaptor.docHeaderTimeoutSecs")) * 1000;
}
long getAdaptorDocContentTimeoutMillis() {
return Long.parseLong(getValue("adaptor.docContentTimeoutSecs")) * 1000;
}
/**
* Returns a list of maps correspending to each transform in the pipeline.
* Each map is the configuration entries for that transform. The 'name'
* configuration entry is added in each map based on the name provided by the
* user.
*/
List<Map<String, String>> getTransformPipelineSpec() {
return getListOfConfigs("transform.pipeline");
}
/**
* Returns a list of maps corresponding to each item of the comma-separated
* value of {@code key}. Each map is the configuration entries for that item
* in the list. The 'name' configuration entry is added in each map based on
* the name provided by the user.
*
* <p>As an example, provided the following config:
* <pre><code>adaptor.servers=server1,fluttershy , rainbowDash
*adaptor.servers.fluttershy.hostname=fluttershy
*adaptor.servers.fluttershy.port=42
*adaptor.servers.server1.hostname=applejack
*adaptor.servers.server1.port=314
*adaptor.servers.rainbowDash.hostname=rainbowdash
*adaptor.servers.rainbowDash.port=159
*adaptor.servers.rainbowDash.name=20% cooler
*adaptor.servers.derpy.hostname=hooves</code></pre>
*
* <p>And calling:
* <pre><code>config.getConfigList("adaptor.servers");</code></pre>
*
* <p>Returns:
* <pre><code>[{
* "hostname": "applejack",
* "port": "42",
* "name": "server1",
*}, {
* "hostname": "fluttershy",
* "port": "314",
* "name": "fluttershy",
*}, {
* "hostname": "rainbowdash",
* "port": "159",
* "name": "raindowDash",
*}]</code></pre>
*/
public synchronized List<Map<String, String>>
getListOfConfigs(String key) {
String configValue = getValue(key).trim();
if ("".equals(configValue)) {
return Collections.emptyList();
}
String[] items = getValue(key).split(",");
List<Map<String, String>> listOfMaps
= new ArrayList<Map<String, String>>(items.length);
for (String item : items) {
item = item.trim();
if ("".equals(item)) {
throw new RuntimeException("Invalid format: " + configValue);
}
Map<String, String> params
= getValuesWithPrefix(key + "." + item + ".");
params.put("name", item);
listOfMaps.add(params);
}
return listOfMaps;
}
boolean isJournalReducedMem() {
return Boolean.parseBoolean(getValue("journal.reducedMem"));
}
// TODO(pjo): Implement on GSA
// /**
// * Optional (default false): Adds no-follow bit with sent records in feed
// * file. No-follow means that if document content has links they are not
// * followed.
// */
// boolean isNoFollowBitEnabled() {
// return Boolean.parseBoolean(getValue("feed.noFollowBitEnabled"));
// }
/* Preferences expected to never change: */
/** Provides the character encoding the GSA prefers. */
Charset getGsaCharacterEncoding() {
return Charset.forName(getValue("gsa.characterEncoding"));
}
String getGsaVersion() {
return getValue("gsa.version");
}
boolean isGsa614FeedWorkaroundEnabled() {
return Boolean.parseBoolean(getValue("gsa.614FeedWorkaroundEnabled"));
}
boolean isGsa70AuthMethodWorkaroundEnabled() {
return Boolean.parseBoolean(getValue("gsa.70AuthMethodWorkaroundEnabled"));
}
String getGsaSamlEntityId() {
return getValue("gsa.samlEntityId");
}
/**
* Provides max number of URLs (equal to number of document ids) that are sent
* to the GSA per feed file.
*/
int getFeedMaxUrls() {
return Integer.parseInt(getValue("feed.maxUrls"));
}
/**
* Provides the type of algorithm GSA is to use to rank documents sent by
* adaptor.
*/
String getScoringType() {
return getValue("gsa.scoringType");
}
/**
* Load user-provided configuration file.
*/
public synchronized void load(String configFile) throws IOException {
load(new File(configFile));
}
/**
* Load user-provided configuration file.
*/
public synchronized void load(File configFile) throws IOException {
this.configFile = configFile;
configFileLastModified = configFile.lastModified();
Reader reader = createReader(configFile);
try {
load(reader);
} finally {
reader.close();
}
}
/**
* Load user-provided configuration file, replacing any previously loaded file
* configuration.
*/
private void load(Reader configFile) throws IOException {
Properties newConfigFileProperties = new Properties(defaultConfig);
newConfigFileProperties.load(configFile);
Config fakeOldConfig;
Set<String> differentKeys;
synchronized (this) {
// Create replacement config.
Properties newConfig = new Properties(newConfigFileProperties);
for (Object o : config.keySet()) {
newConfig.put(o, config.get(o));
}
// Find differences.
differentKeys = findDifferences(config, newConfig);
if (differentKeys.isEmpty()) {
log.info("No configuration changes found");
return;
}
validate(newConfig);
fakeOldConfig = new Config();
fakeOldConfig.config = config;
this.config = newConfig;
}
log.info("New configuration file loaded");
fireConfigModificationEvent(fakeOldConfig, differentKeys);
}
Reader createReader(File configFile) throws IOException {
return new InputStreamReader(new BufferedInputStream(
new FileInputStream(configFile)), Charset.forName("UTF-8"));
}
/**
* @return {@code true} if configuration file was modified.
*/
public boolean ensureLatestConfigLoaded() throws IOException {
synchronized (this) {
if (configFile == null || !configFile.exists() || !configFile.isFile()) {
return false;
}
// Check for modifications.
long newLastModified = configFile.lastModified();
if (configFileLastModified == newLastModified || newLastModified == 0) {
return false;
}
log.info("Noticed modified configuration file");
load(configFile);
}
return true;
}
private Set<String> findDifferences(Properties config, Properties newConfig) {
Set<String> differentKeys = new HashSet<String>();
Set<String> names = new HashSet<String>();
names.addAll(config.stringPropertyNames());
names.addAll(newConfig.stringPropertyNames());
for (String name : names) {
String value = config.getProperty(name);
String newValue = newConfig.getProperty(name);
boolean equal = (value == null && newValue == null)
|| (value != null && value.equals(newValue));
if (!equal) {
differentKeys.add(name);
}
}
return differentKeys;
}
public void validate() {
validate(config);
}
private void validate(Properties config) {
Set<String> unset = new HashSet<String>();
for (String key : noDefaultConfig) {
if (config.getProperty(key) == null) {
unset.add(key);
}
}
if (unset.size() != 0) {
throw new IllegalStateException("Missing configuration values: " + unset);
}
}
/**
* Get a configuration value exactly as provided in configuration. Generally,
* {@link #getValue} should be used instead of this method.
*
* @return raw non-{@code null} value of {@code key}
* @throws IllegalStateException if {@code key} has no value
*/
public String getRawValue(String key) {
String value = config.getProperty(key);
if (value == null) {
throw new IllegalStateException(MessageFormat.format(
"You must set configuration key ''{0}''.", key));
}
return value;
}
/**
* Get a configuration value as computed based on the configuration. Some
* configuration values can be generated from other values. This method
* returns that computed configuration value instead of the raw value provided
* in configuration. This method should be preferred over {@link
* #getRawValue}.
*
* @return non-{@code null} value of {@code key}
* @throws IllegalStateException if {@code key} has no value
*/
public String getValue(String key) {
String value = getRawValue(key);
ValueComputer computer = computeMap.get(key);
if (computer != null) {
value = computer.compute(value);
}
return value;
}
/**
* Returns the current config file
*/
File getConfigFile() {
return configFile;
}
/**
* Gets all configuration values that begin with {@code prefix}, returning
* them as a map with the keys having {@code prefix} removed.
*/
public synchronized Map<String, String> getValuesWithPrefix(String prefix) {
Map<String, String> values = new HashMap<String, String>();
for (String key : config.stringPropertyNames()) {
if (!key.startsWith(prefix)) {
continue;
}
values.put(key.substring(prefix.length()), config.getProperty(key));
}
return values;
}
/**
* Add configuration key. If {@code defaultValue} is {@code null}, then no
* default value is used and the user must provide one.
*/
public synchronized void addKey(String key, String defaultValue) {
if (defaultConfig.containsKey(key) || noDefaultConfig.contains(key)) {
throw new IllegalStateException("Key already added: " + key);
}
if (defaultValue == null) {
noDefaultConfig.add(key);
} else {
defaultConfig.setProperty(key, defaultValue);
}
}
synchronized void addKey(String key, String defaultValue,
ValueComputer computer) {
addKey(key, defaultValue);
computeMap.put(key, computer);
}
/**
* Change the default value of a preexisting configuration key. If {@code
* defaultValue} is {@code null}, then no default is used and the user must
* provide one.
*/
public synchronized void overrideKey(String key, String defaultValue) {
if (!defaultConfig.containsKey(key) && !noDefaultConfig.contains(key)) {
log.log(Level.WARNING, "Overriding unknown configuration key: {0}", key);
}
defaultConfig.remove(key);
noDefaultConfig.remove(key);
if (defaultValue == null) {
noDefaultConfig.add(key);
} else {
defaultConfig.setProperty(key, defaultValue);
}
}
/**
* Manually set a configuration value. Depending on when called, it can
* override a user's configuration, which should be avoided.
*/
synchronized void setValue(String key, String value) {
config.setProperty(key, value);
}
void addConfigModificationListener(
ConfigModificationListener listener) {
modificationListeners.add(listener);
}
void removeConfigModificationListener(
ConfigModificationListener listener) {
modificationListeners.remove(listener);
}
private void fireConfigModificationEvent(Config oldConfig,
Set<String> modifiedKeys) {
ConfigModificationEvent ev
= new ConfigModificationEvent(this, oldConfig, modifiedKeys);
for (ConfigModificationListener listener : modificationListeners) {
try {
listener.configModified(ev);
} catch (Exception ex) {
log.log(Level.WARNING,
"Unexpected exception. Consider filing a bug.", ex);
}
}
}
interface ValueComputer {
/**
* Computes the effective value of a configuration value provided the
* literal value provided in configuration.
*/
public String compute(String rawValue);
}
}