package com.sixsq.slipstream.configuration;
/*
* +=================================================================+
* SlipStream Server (WAR)
* =====
* Copyright (C) 2013 SixSq Sarl (sixsq.com)
* =====
* 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.
* -=================================================================-
*/
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import javax.persistence.NoResultException;
import org.restlet.data.Protocol;
import org.restlet.data.Reference;
import slipstream.ui.views.Representation;
import com.sixsq.slipstream.connector.ConnectorFactory;
import com.sixsq.slipstream.exceptions.ConfigurationException;
import com.sixsq.slipstream.exceptions.ValidationException;
import com.sixsq.slipstream.factory.ParametersFactory;
import com.sixsq.slipstream.persistence.Parameter;
import com.sixsq.slipstream.persistence.ParameterType;
import com.sixsq.slipstream.persistence.ServiceConfiguration;
import com.sixsq.slipstream.persistence.ServiceConfiguration.RequiredParameters;
import com.sixsq.slipstream.persistence.ServiceConfigurationParameter;
/**
* This singleton class is the interface to the service configuration. It
* handles reading some services configuration files and makes the parameters
* available via the getProperty() method. The configuration can be reloaded
* from the UI, which purges the persisted configuration in the db. When the
* service is started for the first time, some configuration files are loaded and
* persisted.
*
* The system first reads a set of default values. These may be overridden by a
* user specified configuration files.
*
* After setting the defaults, this class check if the "slipstream.config.dir"
* system property is set. If it is, it will be used as a name to check the
* configuration directory exists. Users should probably use an absolute path
* when setting the system property. A relative name will be resolved via the
* JVM and may not have the desired effect.
*
* If the system property is not set, then the configuration directory is searched
* in the user's home area.
*
* If no configuration directory is found, then just the default configuration is
* used. The default configuration is likely to be incomplete (some critical
* properties will not have reasonable default values) and will likely to cause
* the service to fail during initialization elsewhere.
*
* The singleton instance of this class is immutable, so clients are encouraged
* to cache a copy of instance returned by getInstance() rather than reinvoking
* the method.
*
*/
public class Configuration {
/**
* Singleton instance. Since this will not be used heavily after startup use
* lazy, synchronized instantiation.
*/
private static Configuration instance = null;
/**
* Name of the system property used to set the location/name of the
* configuration file. It is suggested that users specify an absolute path
* name when using this system property.
*/
private static final String CONFIG_SYSTEM_PROPERTY = "slipstream.config.dir";
/**
* Home config directory.
*/
private static final String HOME_CONFIG_DIRECTORY = ".slipstream";
private ServiceConfiguration serviceConfiguration = new ServiceConfiguration();
private Reference baseRef;
private int defaultPort = 80;
private int defaultSecurePort = 443;
/**
* SlipStream version number derived from the slipstream.version property in
* the property default file.
*/
public String version;
/**
* The base URL of the SlipStream service (without a trailing slash) as a
* String. This value will never be null and will never have a trailing
* slash.
*/
public String baseUrl;
public static boolean isEnabled(String key) throws ValidationException {
return Boolean.parseBoolean(Configuration.getInstance().getProperty(key));
}
public static boolean isQuotaEnabled() throws ValidationException {
return isEnabled(ServiceConfiguration.RequiredParameters.SLIPSTREAM_QUOTA_ENABLE.getName());
}
public static boolean isMetricsLoggerEnabled() throws ValidationException {
return isEnabled(ServiceConfiguration.RequiredParameters.SLIPSTREAM_METRICS_LOGGER_ENABLE.getName());
}
public static boolean isMetricsGraphiteEnabled() throws ValidationException {
return isEnabled(ServiceConfiguration.RequiredParameters.SLIPSTREAM_METRICS_GRAPHITE_ENABLE.getName());
}
public static boolean getMeteringEnabled() throws ConfigurationException, ValidationException {
Configuration config = Configuration.getInstance();
Boolean enabled = Boolean.parseBoolean(config.getProperty(
ServiceConfiguration.RequiredParameters.SLIPSTREAM_METERING_ENABLE.getName(), "true"));
return enabled;
}
/**
* Return the singleton instance of a Configuration object. This method must
* be synchronized to ensure that only one instance of this class is
* constructed.
*
* @return singleton Configuration instance
*
* @throws ConfigurationException
* if there is an error when reading the configuration
* @throws ValidationException
*/
public static synchronized Configuration getInstance() throws ConfigurationException, ValidationException {
if (instance == null) {
instance = new Configuration();
}
return instance;
}
/**
* Private constructor, called only from getInstance(), ensures that this is
* a singleton class. The constructor will first load the default properties
* and then search for the user-specified configuration file.
*
* @throws ConfigurationException
* if an error occurs during the default initialization or when
* searching for user-specified configuration file
* @throws ValidationException
*/
private Configuration() throws ConfigurationException, ValidationException {
try {
ServiceConfiguration serviceConfiguration = ServiceConfiguration.load();
update(serviceConfiguration.getParameters());
} catch (NoResultException ex) {
reset();
store();
}
}
private void postProcessParameters() throws ConfigurationException, ValidationException {
// Extract the SlipStream version number from the tag. Add this as a
// property in the configuration. Do this at the end so that a user
// cannot override the value.
extractAndSetVersion();
// Validate the base URL (and associated Reference) and cache the
// results.
baseRef = initializeBaseRef(RequiredParameters.SLIPSTREAM_BASE_URL.getName());
// Calculate the base path. This value must both begin and end with a
// slash and cannot be null or empty.
String pathSlash = baseRef.getPath();
if (pathSlash == null) {
pathSlash = "/";
}
if (!pathSlash.startsWith("/")) {
pathSlash = "/" + pathSlash;
}
if (!pathSlash.endsWith("/")) {
pathSlash += "/";
}
// Setup commonly used strings related to the base URL.
baseUrl = initializeBaseUrl(baseRef);
setMandatoryToAllParameters();
}
private void extractAndSetVersion() throws ValidationException {
String versionRequiredParameter = RequiredParameters.SLIPSTREAM_VERSION.getName();
ServiceConfigurationParameter versionParameter = getParameters().getParameter(
versionRequiredParameter);
if (versionParameter == null) {
throw (new ConfigurationException("Missing mandatory configuration parameter "
+ versionRequiredParameter));
}
version = versionParameter.getValue();
versionParameter.setReadonly(true);
Representation.setReleaseVersion(version);
}
private void setMandatoryToAllParameters() {
for (ServiceConfigurationParameter parameter : serviceConfiguration.getParameterList()) {
parameter.setMandatory(true);
}
}
/**
* Merge a list of parameters with the parameters provided by the
* connectors. loaded from file with the parameters known by the
* connector(s) specified in the config file. Build the properties using the
* first element of the property name '.' separated. This allows the config
* file to provide properties that are not known to a connector defined in
* the ServiceConfiguration.AllowedParameter.CLOUD_CONNECTOR_CLASS property
* of the config file. Since connector class names can be added at anytime
* during the lifetime of a SlipStream server instance.
*
*/
private void mergeWithParametersFromConnectors() {
// We might have loaded a new list of connector classes, so
// reset the connectors
ConnectorFactory.resetConnectors();
String[] connectorClassNames = getConnectorClassNames();
Map<String, ServiceConfigurationParameter> connectorsParameters;
try {
connectorsParameters = ParametersFactory.getServiceConfigurationParametersTemplate(connectorClassNames);
} catch (ValidationException e) {
throw new ConfigurationException(e.getMessage());
}
// Loop around the connector parameters and add them to the list if
// they are not there, otherwise reset their fields, with the exception
// of the value
for (ServiceConfigurationParameter p : connectorsParameters.values()) {
ServiceConfigurationParameter parameter = p;
if (serviceConfiguration.parametersContainKey(p.getName())) {
parameter = serviceConfiguration.getParameters().get(p.getName());
parameter.setDescription(p.getDescription());
parameter.setMandatory(p.isMandatory());
parameter.setCategory(p.getCategory());
parameter.setType(p.getType());
if (parameter.getType() == ParameterType.Enum) {
parameter.setEnumValues(p.getEnumValues());
}
}
serviceConfiguration.setParameter(parameter);
}
}
protected ServiceConfigurationParameter createParameter(String value, String parameterFormattedKeyName,
String description, String category) throws ValidationException {
ServiceConfigurationParameter parameter = new ServiceConfigurationParameter(parameterFormattedKeyName, value);
parameter.setDescription(description);
parameter.setMandatory(true);
parameter.setCategory(category);
return parameter;
}
private Map<String, ServiceConfigurationParameter> convertPropertiesToParameters(Properties properties)
throws ValidationException {
Map<String, ServiceConfigurationParameter> parameters = new HashMap<String, ServiceConfigurationParameter>();
for (Entry<Object, Object> entry : properties.entrySet()) {
String key = (String) entry.getKey();
String value = (String) entry.getValue();
ServiceConfigurationParameter parameter = createParameter(value, key, null, extractCategory(key));
parameters.put(key, parameter);
}
return parameters;
}
private String extractCategory(String key) {
return key.split("\\.")[0];
}
protected String[] getConnectorClassNames() {
String cloudConnectorClassNameParameterKey = ServiceConfiguration.RequiredParameters.CLOUD_CONNECTOR_CLASS
.getName();
if (!serviceConfiguration.parametersContainKey(cloudConnectorClassNameParameterKey)) {
throw (new ConfigurationException(
"Missing from the configuration file mandatory system configuration parameter: "
+ cloudConnectorClassNameParameterKey));
}
String cloudConnectorClassNameParameterValue = serviceConfiguration.getParameters()
.get(cloudConnectorClassNameParameterKey).getValue();
return ConnectorFactory.splitConnectorClassNames(cloudConnectorClassNameParameterValue);
}
/**
* Utility method to search through possible locations of the configuration
* file and return the most appropriate one.
*
* @return File object containing configuration or null if none found
*
* @throws ConfigurationException
* if any error occurs while reading configuration files
*/
public static File findConfigurationDirectory() throws ConfigurationException {
// Check first if a system property is set that defines the location of
// the configuration information.
String name = System.getProperty(CONFIG_SYSTEM_PROPERTY);
if (name != null) {
return new File(name);
}
// Try the home area of the user.
String home = System.getProperty("user.home");
if (home != null) {
File ssHomeDir = new File(home + File.separator + HOME_CONFIG_DIRECTORY);
if (ssHomeDir.canRead()) {
return ssHomeDir;
}
}
// Nothing found
return null;
}
public static Properties loadPropertiesFile(URI uri, Properties properties) {
InputStream inputStream = null;
try {
URL url = uri.toURL();
inputStream = url.openStream();
properties.load(inputStream);
} catch (MalformedURLException e) {
throw new ConfigurationException("Invalid configuration URL.");
} catch (IOException e) {
throw new ConfigurationException("Error loading configuration file.");
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException consumed) {
// Ignore errors on close.
}
}
return properties;
}
public ServiceConfiguration getParameters() {
return serviceConfiguration;
}
/**
* Retrieve the configuration value associated with the given key. Will
* return null if the key does not exist.
*
* @param key
* parameter name
*
* @return value associated with key or null if the key does not exist
*/
public String getProperty(String key) {
ServiceConfigurationParameter parameter = serviceConfiguration.getParameter(key);
return (parameter == null ? null : parameter.getValue());
}
/**
* Retrieve the configuration value associated with the given key. Will
* return defaultValue if the key does not exist or is set to null.
*
* @param key
* parameter name
* @param defaultValue
*
* @return value associated with key or null if the key does not exist
*/
public String getProperty(String key, String defaultValue) {
String value = getProperty(key);
return (value == null ? defaultValue : value);
}
/**
* Retrieve the configuration value associated with the given key or throw
* an exception if it does not exist.
*
* @param key
*
* @return value associated with the key
*
* @throws ConfigurationException
* if there is no value associated with the given key
*/
public String getRequiredProperty(String key) throws ConfigurationException {
String value = getProperty(key);
if (value == null) {
throw new ConfigurationException("missing configuration property: " + key);
}
return value;
}
/**
* Returns a Reference containing a copy of the base Reference. Because
* Reference objects are mutable, copies of the internal object must be
* returned to guarantee consistency of the Configuration object.
*
* @return copy of the base Reference for the service
*/
public Reference getBaseRef() {
return new Reference(baseRef);
}
/**
* Retrieve the property containing the base URL, validate it, and return a
* Reference containing the value.
*
* The path in the returned reference will always have a trailing slash.
*
* @param propertyName
* name of the property holding the base URL
*
* @return Reference containing the validated base URL
*
* @throws ConfigurationException
* if the property does not exist or the contained value is
* invalid
*/
private Reference initializeBaseRef(String propertyName) throws ConfigurationException {
String uri = getRequiredProperty(propertyName);
// Create a Reference so that it can do all the work of parsing the
// input URI.
Reference reference = null;
try {
reference = new Reference(uri);
} catch (IllegalArgumentException e) {
throw new ConfigurationException("invalid uri: " + uri);
}
// Check that the scheme is either http or https. Set the default port
// number accordingly.
String scheme = reference.getScheme(true);
int port = -1;
if ("http".equals(scheme)) {
port = 80;
} else if ("https".equals(scheme)) {
port = 443;
} else {
scheme = (scheme == null) ? "" : scheme;
throw new ConfigurationException("invalid scheme: '" + scheme + "'");
}
// Pull out the port number, if specified in the URL.
int uriPort = reference.getHostPort();
// Use port values in the following order: explicit port in URI, default
// port specified as argument, or default scheme port.
if (uriPort > 0) {
port = uriPort;
} else {
if (reference.getSchemeProtocol() == Protocol.HTTP) {
port = defaultPort;
} else if (reference.getSchemeProtocol() == Protocol.HTTPS) {
port = defaultSecurePort;
}
}
// Pull out the host name.
String host = reference.getHostDomain();
if (host == null) {
throw new ConfigurationException("invalid host in root URL");
}
// Pull out the path for the URL. Ensure that this is always an absolute
// path and that the path has a trailing slash.
String rootPath = reference.getPath();
if (rootPath == null) {
rootPath = "/";
}
if (!rootPath.endsWith("/")) {
rootPath = rootPath + "/";
}
if (!rootPath.startsWith("/")) {
rootPath = "/" + rootPath;
}
// Reconstitute the root URL, thereby ignoring any extraneous parts of
// the specified configuration parameter.
return new Reference(scheme, host, port, rootPath, null, null);
}
/**
* Create a string representation of the base URL. This value will have all
* trailing slashes removed.
*
* @param baseRef
* @return
*/
private String initializeBaseUrl(Reference baseRef) {
String url = baseRef.toString();
// TODO: This assumption should be fixed in the rest of the code.
// Strip any trailing slashes.
while (url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
}
return url;
}
/**
* Constructs a URL to a service from configuration information.
*
* @param configServiceName
* name of service section in configuration file
* @return complete URL
*
* @throws ConfigurationException
*/
public String getServiceUrl(String configServiceName) throws ConfigurationException {
String url;
try {
url = getRequiredProperty(configServiceName + ".url") + getRequiredProperty(configServiceName + ".service");
} catch (ConfigurationException e) {
url = getDefaultServiceUrl(configServiceName);
}
return url;
}
/**
* Constructs a url to the default service root based on configuration
* information.
*
* @param configServiceName
* name of service section in configuration file
* @return complete url
* @throws IOException
* @throws FileNotFoundException
* @throws ConfigurationException
*/
public String getDefaultServiceUrl(String configServiceName) throws ConfigurationException {
return baseUrl + getRequiredProperty(configServiceName + ".service");
}
/**
* Forces to re-read the configuration from file, removing all persisted
* state. Re-load the default parameters from the configured connectors and
* validate that the required parameters are present.
*
* @throws ConfigurationException
* @throws ValidationException
*/
public void reset() throws ConfigurationException, ValidationException {
serviceConfiguration = new ServiceConfiguration();
mergeWithParametersFromConnectors();
postProcessParameters();
validateRequiredParameters();
resetRequiredParameterDefinition();
}
/**
* First load config file (just in case there are new required parameters)
* The overwrite them with content from the db (if previously persisted)
* Then process and validate
*
* @throws ValidationException
* @throws ConfigurationException
*/
public void update(Map<String, ServiceConfigurationParameter> parameters) throws ConfigurationException,
ValidationException {
serviceConfiguration.setParameters(parameters);
mergeWithParametersFromConnectors();
postProcessParameters();
validateRequiredParameters();
resetRequiredParameterDefinition();
}
private void validateRequiredParameters() {
getParameters().validate();
}
private void resetRequiredParameterDefinition() {
for (RequiredParameters required : ServiceConfiguration.RequiredParameters.values()) {
ServiceConfigurationParameter target = serviceConfiguration.getParameter(required.getName());
target.setCategory(required.getCategory().name());
target.setType(required.getType());
target.setDescription(required.getDescription());
target.setInstructions(required.getInstruction());
target.setMandatory(true);
}
}
public void store() {
serviceConfiguration = (ServiceConfiguration) serviceConfiguration.store();
}
}