package org.gbif.ipt.config;
import org.gbif.ipt.model.User;
import org.gbif.ipt.model.User.Role;
import org.gbif.ipt.service.BaseManager;
import org.gbif.ipt.service.InvalidConfigException;
import org.gbif.ipt.service.InvalidConfigException.TYPE;
import org.gbif.ipt.service.admin.ConfigManager;
import org.gbif.ipt.service.admin.ExtensionManager;
import org.gbif.ipt.service.admin.RegistrationManager;
import org.gbif.ipt.service.admin.UserAccountManager;
import org.gbif.ipt.service.admin.VocabulariesManager;
import org.gbif.ipt.service.admin.impl.RegistrationManagerImpl;
import org.gbif.ipt.service.admin.impl.VocabulariesManagerImpl;
import org.gbif.ipt.service.manage.ResourceManager;
import org.gbif.ipt.utils.InputStreamUtils;
import org.gbif.ipt.utils.LogFileAppender;
import org.gbif.ipt.utils.URLUtils;
import org.gbif.utils.HttpUtil;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import javax.servlet.http.HttpServletResponse;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.log4j.LogManager;
import org.apache.log4j.xml.DOMConfigurator;
/**
* A skeleton implementation for the time being.
*/
@Singleton
public class ConfigManagerImpl extends BaseManager implements ConfigManager {
private final InputStreamUtils streamUtils;
private final UserAccountManager userManager;
private final ResourceManager resourceManager;
private final ExtensionManager extensionManager;
private final VocabulariesManager vocabManager;
private final RegistrationManager registrationManager;
private final ConfigWarnings warnings;
private final DefaultHttpClient client;
private final HttpUtil http;
private final PublishingMonitor publishingMonitor;
private static final String PATH_TO_CSS = "/styles/main.css";
private static final int DEFAULT_TO = 4000; // Default time out
private static final String DEPRECATED_VOCAB_PERSISTENCE_FILE = "vocabularies.xml";
@Inject
public ConfigManagerImpl(DataDir dataDir, AppConfig cfg, InputStreamUtils streamUtils,
UserAccountManager userManager,
ResourceManager resourceManager, ExtensionManager extensionManager, VocabulariesManager vocabManager,
RegistrationManager registrationManager, ConfigWarnings warnings, DefaultHttpClient client, PublishingMonitor
publishingMonitor) {
super(cfg, dataDir);
this.streamUtils = streamUtils;
this.userManager = userManager;
this.resourceManager = resourceManager;
this.extensionManager = extensionManager;
this.vocabManager = vocabManager;
this.registrationManager = registrationManager;
this.warnings = warnings;
this.client = client;
this.http = new HttpUtil(client);
this.publishingMonitor = publishingMonitor;
if (dataDir.isConfigured()) {
log.info("IPT DataDir configured - loading its configuration");
try {
loadDataDirConfig();
} catch (InvalidConfigException e) {
log.error("Configuration problems existing. Watch your data dir! " + e.getMessage(), e);
}
} else {
log.debug("IPT DataDir not configured - no configuration loaded");
}
}
/**
* It Creates a HttpHost object with the string given by the user and verifies if there is a connection with this
* host. If there is a connection with this host, it changes the current proxy host with this host. If not it keeps
* the current proxy.
*
* @param hostTemp the actual proxy.
* @param proxy an URL with the format http://proxy.my-institution.com:8080.
* @throws InvalidConfigException If it can not connect to the proxy host or if the port number is no integer or if
* the proxy URL is not with the valid format http://proxy.my-institution.com:8080
*/
private boolean changeProxy(HttpHost hostTemp, String proxy) {
try {
HttpHost host = URLUtils.getHost(proxy);
if (!http.verifyHost(host)) {
if (hostTemp != null) {
log.info(
"Proxy could not be validated, reverting to previous proxy setting on http client: " + hostTemp.toString());
client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, hostTemp);
}
throw new InvalidConfigException(TYPE.INVALID_PROXY, "admin.config.error.connectionRefused");
}
log.info("Proxy validated, updating the proxy setting on http client to: " + proxy);
client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, host);
} catch (NumberFormatException e) {
if (hostTemp != null) {
log.info("NumberFormatException encountered, reverting to previous proxy setting on http client: " + hostTemp
.toString());
client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, hostTemp);
}
throw new InvalidConfigException(TYPE.INVALID_PROXY, "admin.config.error.invalidPort");
} catch (MalformedURLException e) {
if (hostTemp != null) {
log.info("MalformedURLException encountered, reverting to previous proxy setting on http client: " + hostTemp
.toString());
client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, hostTemp);
}
throw new InvalidConfigException(TYPE.INVALID_PROXY, "admin.config.error.invalidProxyURL");
}
return true;
}
/**
* Returns the local host name.
*/
public String getHostName() {
return URLUtils.getHostName();
}
public boolean isBaseURLValid() {
try {
URL baseURL = new URL(cfg.getProperty(AppConfig.BASEURL));
return validateBaseURL(baseURL);
} catch (MalformedURLException e) {
log.error("MalformedURLException encountered while validating baseURL");
}
return false;
}
/**
* Update configuration singleton from config file in data dir.
*/
public void loadDataDirConfig() throws InvalidConfigException {
log.info("Reading DATA DIRECTORY: " + dataDir.dataDir.getAbsolutePath());
log.info("Loading IPT config ...");
cfg.loadConfig();
log.info("Reloading log4j settings ...");
reloadLogger();
if (cfg.getProxy() != null) {
log.info("Configuring http proxy ...");
try {
setProxy(cfg.getProxy());
} catch (InvalidConfigException e) {
warnings.addStartupError(e);
}
}
log.info("Loading user accounts ...");
userManager.load();
log.info("Loading vocabularies ...");
vocabManager.load();
log.info("Ensure latest versions of default vocabularies are installed...");
vocabManager.installOrUpdateDefaults();
File vocabDir = dataDir.configFile(VocabulariesManagerImpl.CONFIG_FOLDER);
File deprecatedVocabFile = new File(vocabDir, DEPRECATED_VOCAB_PERSISTENCE_FILE);
if (deprecatedVocabFile.exists()) {
log.info("Perform 1-time event: delete deprecated vocabularies.xml file");
FileUtils.deleteQuietly(deprecatedVocabFile);
}
log.info("Loading extensions ...");
extensionManager.load();
if (!dataDir.configFile(RegistrationManagerImpl.PERSISTENCE_FILE_V2).exists()) {
log.info("Perform 1-time event: migrate registration.xml into registration2.xml with passwords encrypted");
registrationManager.encryptRegistration();
}
log.info("Loading registration configuration...");
registrationManager.load();
log.info("Loading resource configurations ...");
// default creator used to populate missing resource creator when loading resources
User defaultCreator = (userManager.list(Role.Admin).isEmpty()) ? null : userManager.list(Role.Admin).get(0);
resourceManager.load(dataDir.dataFile(DataDir.RESOURCES_DIR), defaultCreator);
// start publishing monitor
log.info("Starting Publishing Monitor...");
publishingMonitor.start();
}
private void reloadLogger() {
LogFileAppender.LOGDIR = dataDir.loggingDir().getAbsolutePath();
log.info("Setting logging dir to " + LogFileAppender.LOGDIR);
InputStream log4j;
// use different log4j settings files for production or debug mode
if (cfg.debug()) {
log4j = streamUtils.classpathStream("log4j.xml");
} else {
log4j = streamUtils.classpathStream("log4j-production.xml");
}
LogManager.resetConfiguration();
DOMConfigurator domConfig = new DOMConfigurator();
domConfig.doConfigure(log4j, LogManager.getLoggerRepository());
log.info("Reloaded log4j for " + (cfg.debug() ? "debugging" : "production"));
log.info("Logging to " + LogFileAppender.LOGDIR);
log.info("IPT Data Directory: " + dataDir.dataFile(".").getAbsolutePath());
}
public void saveConfig() throws InvalidConfigException {
try {
cfg.saveConfig();
} catch (IOException e) {
log.debug("Cant save IPT configuration: " + e.getMessage(), e);
throw new InvalidConfigException(TYPE.CONFIG_WRITE, "Cant save IPT configuration: " + e.getMessage());
}
}
public void setAnalyticsKey(String key) throws InvalidConfigException {
cfg.setProperty(AppConfig.ANALYTICS_KEY, StringUtils.trimToEmpty(key));
}
public void setBaseUrl(URL baseURL) throws InvalidConfigException {
boolean validate = true;
if (URLUtils.isLocalhost(baseURL)) {
log.info("Localhost used in base URL");
// validate if localhost URL is configured only in developer mode.
// use cfg registryType vs cfg devMode since it takes into account devMode from pom and production from setupPage
if (cfg.getRegistryType() == AppConfig.REGISTRY_TYPE.DEVELOPMENT) {
HttpHost hostTemp = (HttpHost) client.getParams().getParameter(ConnRoutePNames.DEFAULT_PROXY);
if (hostTemp != null) {
// if local URL is configured, the IPT should do the validation without a proxy.
setProxy(null);
validate = false;
if (!validateBaseURL(baseURL)) {
setProxy(hostTemp.toString());
throw new InvalidConfigException(TYPE.INACCESSIBLE_BASE_URL, "No IPT found at new base URL");
}
setProxy(hostTemp.toString());
}
} else {
// we want to allow baseURL equal the machine name in production mode, but not localhost
if (!baseURL.getHost().equalsIgnoreCase(this.getHostName())) {
// local URL is not permitted in production mode.
throw new InvalidConfigException(TYPE.INACCESSIBLE_BASE_URL,
"Localhost base URL not permitted in production mode, since the IPT will not be visible to the outside!");
}
}
}
if (validate && !validateBaseURL(baseURL)) {
throw new InvalidConfigException(TYPE.INACCESSIBLE_BASE_URL, "No IPT found at new base URL");
}
// store in properties file
log.info("Updating the baseURL to: " + baseURL);
cfg.setProperty(AppConfig.BASEURL, baseURL.toString());
}
public void setConfigProperty(String key, String value) {
cfg.setProperty(key, value);
}
public boolean setDataDir(File dataDir) throws InvalidConfigException {
boolean created = this.dataDir.setDataDir(dataDir);
loadDataDirConfig();
return created;
}
public void setDebugMode(boolean debug) throws InvalidConfigException {
cfg.setProperty(AppConfig.DEBUG, Boolean.toString(debug));
reloadLogger();
}
/**
* Turn archival mode on or off. If being turned off, there can be no associated organisations in the IPT that
* has its DOI registration agency account activated to start registering DOIs for datasets.
*
* @param archivalMode true to turn on, false to turn off
*/
public void setArchivalMode(boolean archivalMode) throws InvalidConfigException {
if (!archivalMode && registrationManager.findPrimaryDoiAgencyAccount() != null) {
throw new InvalidConfigException(TYPE.DOI_REGISTRATION_ALREADY_ACTIVATED,
"Cannot turn off archival mode since" + "DOI registration has been activated");
}
cfg.setProperty(AppConfig.ARCHIVAL_MODE, Boolean.toString(archivalMode));
}
public void setGbifAnalytics(boolean useGbifAnalytics) throws InvalidConfigException {
cfg.setProperty(AppConfig.ANALYTICS_GBIF, Boolean.toString(useGbifAnalytics));
}
public void setIptLocation(Double lat, Double lon) throws InvalidConfigException {
if (lat == null || lon == null) {
cfg.setProperty(AppConfig.IPT_LATITUDE, "");
cfg.setProperty(AppConfig.IPT_LONGITUDE, "");
} else {
if (lat > 90.0 || lat < -90.0 || (lon > 180.0 || lon < -180.0)) {
log.warn("IPT Lat/Lon is not a valid coordinate");
throw new InvalidConfigException(TYPE.FORMAT_ERROR, "IPT Lat/Lon is not a valid coordinate");
}
cfg.setProperty(AppConfig.IPT_LATITUDE, Double.toString(lat));
cfg.setProperty(AppConfig.IPT_LONGITUDE, Double.toString(lon));
}
}
/**
* It validates if is the first time that the user saves a proxy, if this is true, the proxy is saved normally (the
* first time that the proxy is saved is in the setup page), if not (the second time that the user saves a proxy is
* in
* the config page), it validates if this proxy is the same as current proxy, if this is true, nothing changes, if
* not, it removes the current proxy and save the new proxy.
*
* @param proxy an URL with the format http://proxy.my-institution.com:8080.
*/
public void setProxy(String proxy) throws InvalidConfigException {
proxy = StringUtils.trimToNull(proxy);
// save the current proxy
HttpHost hostTemp = null;
if (StringUtils.trimToNull(cfg.getProperty(AppConfig.PROXY)) != null) {
try {
URL urlTemp = new URL(cfg.getProperty(AppConfig.PROXY));
hostTemp = new HttpHost(urlTemp.getHost(), urlTemp.getPort());
} catch (MalformedURLException e) {
// This exception should not be shown, the urlTemp was validated before being saved.
log.info("the proxy URL is invalid", e);
}
}
if (proxy == null) {
// remove proxy from http client
// Suddenly the client didn't have proxy host.
log.info("No proxy entered, so removing proxy setting on http client");
client.getParams().removeParameter(ConnRoutePNames.DEFAULT_PROXY);
} else {
// Changing proxy host
if (hostTemp == null) {
// First time, before Setup
changeProxy(null, proxy);
} else {
// After Setup
// Validating if the current proxy is the same proxy given by the user
if (hostTemp.toString().equals(proxy)) {
changeProxy(hostTemp, proxy);
} else {
// remove proxy from http client
log.info("A change of proxy detected so starting by removing proxy setting on http client");
client.getParams().removeParameter(ConnRoutePNames.DEFAULT_PROXY);
changeProxy(hostTemp, proxy);
}
}
}
// store in properties file
cfg.setProperty(AppConfig.PROXY, proxy);
}
public boolean setupComplete() {
return dataDir.isConfigured() && cfg.getRegistryType() != null && !userManager.list(Role.Admin).isEmpty();
}
/**
* It validates if the there is a connection with the baseURL, it executes a request using the baseURL.
*
* @param baseURL a URL to validate.
*
* @return true if the response to the request has a status code equal to 200.
*/
public boolean validateBaseURL(URL baseURL) {
if (baseURL == null) {
return false;
}
try {
String testURL = baseURL.toString() + PATH_TO_CSS;
log.info("Validating BaseURL with get request (having 4 second timeout) to: " + testURL);
HttpResponse response = http.executeGetWithTimeout(new HttpGet(testURL), DEFAULT_TO);
return HttpServletResponse.SC_OK == response.getStatusLine().getStatusCode();
} catch (ClientProtocolException e) {
log.info("Protocol error connecting to new base URL [" + baseURL.toString() + "]", e);
} catch (IOException e) {
log.info("IO error connecting to new base URL [" + baseURL.toString() + "]", e);
} catch (Exception e) {
log.info("Unknown error connecting to new base URL [" + baseURL.toString() + "]", e);
}
return false;
}
}