/** * Copyright (C) 2009-2013 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.server.service.config; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.TreeMap; import java.util.UUID; import com.foundationdb.server.error.BadConfigDirectoryException; import com.foundationdb.server.error.ConfigurationPropertiesLoadException; import com.foundationdb.server.error.InvalidTimeZoneException; import com.foundationdb.server.error.MissingRequiredPropertiesException; import com.foundationdb.server.error.ServiceNotStartedException; import com.foundationdb.server.error.ServiceAlreadyStartedException; import com.foundationdb.server.service.Service; import com.foundationdb.util.tap.Tap; import com.google.inject.Inject; import org.joda.time.DateTimeZone; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ConfigurationServiceImpl implements ConfigurationService, Service { private static final Logger LOG = LoggerFactory.getLogger(ConfigurationServiceImpl.class); private final static String CONFIG_DEFAULTS_RESOURCE = "configuration-defaults.properties"; private static final String INITIALLY_ENABLED_TAPS = "taps.initiallyenabled"; private static final String INSTANCE_ID_PROP = "fdbsql.instance_id"; /** Server properties. Format specified by server. */ public static final String CONFIG_DIR_PROP = "fdbsql.config_dir"; // Note: Also in GuicedServiceManager public static final String SERVER_PROPERTIES_FILE_NAME = "server.properties"; private volatile Map<String,String> properties = null; private final Set<String> requiredKeys = new HashSet<>(); private volatile long queryTimeoutMilli = -1L; // No timeout @Inject public ConfigurationServiceImpl() { } @Override public long queryTimeoutMilli() { return queryTimeoutMilli; } @Override public void queryTimeoutMilli(long queryTimeoutMilli) { this.queryTimeoutMilli = queryTimeoutMilli; } @Override public String getInstanceID() { return getProperty(INSTANCE_ID_PROP); } @Override public boolean testing() { return false; } @Override public final String getProperty(String propertyName) throws PropertyNotDefinedException { String property = internalGetProperty(propertyName); if (property == null) { throw new PropertyNotDefinedException(propertyName); } return property; } private String internalGetProperty(String propertyName) { final Map<String, String> map = internalGetProperties(); return map.get(propertyName); } @Override public Map<String, String> getProperties() { Map<String, String> internalProperties = internalGetProperties(); Map<String, String> results = new TreeMap<>(); for (Map.Entry<String, String> entry : internalProperties.entrySet()) { results.put(entry.getKey(), entry.getValue()); } return Collections.unmodifiableMap(results); } @Override public Properties deriveProperties(String withPrefix) { Properties properties = new Properties(); for (Map.Entry<String,String> configProp : internalGetProperties().entrySet()) { String key = configProp.getKey(); if (key.startsWith(withPrefix)) { properties.setProperty( key.substring(withPrefix.length()), configProp.getValue() ); } } return properties; } @Override public final void start() throws ServiceAlreadyStartedException { if (properties == null) { properties = internalLoadProperties(); validateTimeZone(); String initiallyEnabledTaps = properties.get(INITIALLY_ENABLED_TAPS); if (initiallyEnabledTaps != null) { Tap.setInitiallyEnabled(initiallyEnabledTaps); } String id = properties.get(INSTANCE_ID_PROP); if((id == null) || id.isEmpty()) { UUID uuid = UUID.randomUUID(); properties.put(INSTANCE_ID_PROP, uuid.toString()); } } } private void validateTimeZone() { String timezone = System.getProperty("user.timezone"); // The goal here is to make sure that if the user sets user.timezone we will parse it correctly and not end up // in a situation where DateTimeZone is inconsistent with TimeZone, or where it just defaulted to UTC because // the user did "-Duser.timezone=America/Los_angeles" (note the lower case a). // There are other cases where it would work, such as "-Duser.timezone=PST", but PST isn't a valid timezone // according to DateTimeZone, so we will stop, even though it would technically work. That's ok though, because // PST is just there for java 1.1.x compatibility. // Note also that in the open jdk for 1.7u and 1.8 (at least) there's a bug on Mac OS X where if you specify // an invalid user.timezone, it will end up being GMT}05:00 where the 05:00 is your system time, and the } is // an unprintable character. I filed a bug though, so that might get fixed. // Also note, that, as of this writing, there are usages of both java.util date functionality and Joda if (timezone != null && timezone.length() != 0 && DateTimeZone.getProvider().getZone(timezone) == null) { // Originally a hard error but JRE on CentOS 6 found to consistently misuse /etc/sysconfig/clock // throw new InvalidTimeZoneException(); LOG.error("Reverting to timezone {} as Joda does not support user.timezone={}", DateTimeZone.getDefault(), timezone); } else { LOG.debug("Using timezone: {}", DateTimeZone.getDefault()); } } @Override public final void stop() { try { unloadProperties(); } finally { properties = null; } } @Override public void crash() { // Note: do not call unloadProperties(). properties = null; } private Map<String, String> internalLoadProperties() throws ServiceAlreadyStartedException { Map<String, String> ret = loadProperties(); Set<String> missingKeys = new HashSet<>(); for (String required : getRequiredKeys()) { if (!ret.containsKey(required)) { missingKeys.add(required); } } if (!missingKeys.isEmpty()) { throw new MissingRequiredPropertiesException(missingKeys); } return ret; } /** * Load and return a set of configuration properties. Override this method * for customization in unit tests. For example, some unit tests create data * files in a temporary directory. These should also override * {@link #unloadProperties()} to clean them up. * @return the configuration properties */ protected Map<String, String> loadProperties() { Properties props = null; props = loadResourceProperties(props); props = loadSystemProperties(props); props = loadConfigDirProperties(props); return propertiesToMap(props); } /** * Override this method in unit tests to clean up any temporary files, etc. * A class that overrides {@link #loadProperties()} should probably also * override this method. */ protected void unloadProperties() { } protected Set<String> getRequiredKeys() { return requiredKeys; } private static Map<String, String> propertiesToMap(Properties properties) { Map<String, String> ret = new HashMap<>(); for (String keyStr : properties.stringPropertyNames()) { String value = properties.getProperty(keyStr); ret.put(keyStr, value); } return ret; } private static Properties chainProperties(Properties defaults) { return defaults == null ? new Properties() : new Properties(defaults); } private Properties loadResourceProperties(Properties defaults) { Properties resourceProps = chainProperties(defaults); String resourceName = ConfigurationServiceImpl.class.getPackage().getName().replace(".", "/") + "/" + CONFIG_DEFAULTS_RESOURCE; Enumeration<URL> e; try { e = ConfigurationServiceImpl.class.getClassLoader().getResources(resourceName); } catch (IOException ex) { throw new ConfigurationPropertiesLoadException(CONFIG_DEFAULTS_RESOURCE, ex.getMessage()); } while (e.hasMoreElements()) { URL source = e.nextElement(); try (InputStream resourceIs = source.openStream()) { resourceProps.load(resourceIs); } catch (IOException ex) { throw new ConfigurationPropertiesLoadException(source.toString(), ex.getMessage()); } } stripRequiredProperties(resourceProps, requiredKeys); return resourceProps; } static void stripRequiredProperties(Properties properties, Set<String> toSet) { Set<String> requiredKeyStrings = new HashSet<>(); for (String key : properties.stringPropertyNames()) { if (key.startsWith("REQUIRED.")) { requiredKeyStrings.add(key); final String module = key.substring("REQUIRED.".length()); for (String name : properties.getProperty(key).split(",\\s*")) { toSet.add(module + '.' +name); } } } for (String key : requiredKeyStrings) { properties.remove(key); } } private static Properties loadSystemProperties(Properties defaults) { Properties loadedSystemProps = chainProperties(defaults); final Properties actualSystemProps = System.getProperties(); for (String key : actualSystemProps.stringPropertyNames()) { loadedSystemProps.setProperty(key, actualSystemProps.getProperty(key)); } return loadedSystemProps; } private static Properties loadConfigDirProperties(Properties defaults) { Properties combined = chainProperties(defaults); String configDirPath = combined.getProperty(CONFIG_DIR_PROP); if (configDirPath != null && !"NONE".equals(configDirPath)) { File configDir = new File(configDirPath); if (!configDir.exists() || !configDir.isDirectory()) { throw new BadConfigDirectoryException(configDir.getAbsolutePath()); } try { loadFromFile(combined, configDirPath, SERVER_PROPERTIES_FILE_NAME); } catch(IOException e) { throw new ConfigurationPropertiesLoadException(SERVER_PROPERTIES_FILE_NAME, e.getMessage()); } } return combined; } private static void loadFromFile(Properties props, String directory, String fileName) throws IOException { FileInputStream fis = null; try { File file = new File(directory, fileName); fis = new FileInputStream(file); props.load(fis); } finally { if(fis != null) { fis.close(); } } } private Map<String, String> internalGetProperties() { final Map<String, String> ret = properties; if (ret == null) { throw new ServiceNotStartedException("Configuration"); } return ret; } }