ServerProperties.java

/**
 * ServerProperties.java This file is part of WattDepot.
 * <p/>
 * Copyright (C) 2013  Cam Moore
 * <p/>
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * <p/>
 * 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 General Public License for more details.
 * <p/>
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.wattdepot.server;

import org.wattdepot.common.domainmodel.UserInfo;
import org.wattdepot.common.domainmodel.UserPassword;
import org.wattdepot.common.util.UserHome;
import org.wattdepot.extension.WattDepotExtension;

import java.io.FileInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.logging.Logger;

/**
 * ServerProperties - Provides access to the values stored in the
 * wattdepot-server.properties file.
 *
 * @author Cam Moore
 */
public class ServerProperties {
  /**
   * The full path to the server's home directory.
   */
  public static final String SERVER_HOME_DIR = "wattdepot-server.homedir";
  /**
   * The hostname key.
   */
  public static final String HOSTNAME_KEY = "wattdepot-server.hostname";
  /**
   * Name of property used to store the admin username.
   */
  public static final String ADMIN_USER_NAME = "wattdepot-server.admin.name";
  /**
   * The environment variable for storing the admin's name.
   */
  public static final String ADMIN_USER_NAME_ENV = "WATTDEPOT_ADMIN_NAME";
  /**
   * Name of property used to store the admin password.
   */
  public static final String ADMIN_USER_PASSWORD = "wattdepot-server.admin.password";
  /**
   * The environment variable for storing the admin's password.
   */
  public static final String ADMIN_USER_PASSWORD_ENV = "WATTDEPOT_ADMIN_PASSWORD";
  /**
   * The environment variable name that holds the salt used for encrypting
   * passwords in WattDepot.
   */
  public static final String WATTDEPOT_SALT_ENV = "WATTDEPOT_SALT";
  /**
   * The WattDepot implementation class.
   */
  public static final String WATT_DEPOT_IMPL_KEY = "wattdepot-server.wattdepot.impl";
  /**
   * The wattdepot server port key.
   */
  public static final String PORT_KEY = "wattdepot-server.port";
  /**
   * The context root key.
   */
  public static final String CONTEXT_ROOT_KEY = "wattdepot-server.context.root";

  /** The option to enable SSL. */
  public static final String SSL = "wattdepot-server.ssl";
  /** The path to the keystore holding the certificate. */
  public static final String SSL_KEYSTORE_PATH = "wattdepot-server.ssl.keystore.path";
  /** The password for the keystore. */
  public static final String SSL_KEYSTORE_PASSWORD = "wattdepot-server.ssl.keystore.password";
  /** The password for the key. */
  public static final String SSL_KEYSTORE_KEY_PASSWORD = "wattdepot-server.ssl.keystore.key.password";
  /** The type of keystore. */
  public static final String SSL_KEYSTORE_TYPE = "wattdepot-server.ssl.keystore.type";

  /** The database connection driver class. */
  public static final String DB_CONNECTION_DRIVER = "wattdepot-server.db.connection.driver";
  /**
   * The database connection driver url.
   */
  public static final String DB_CONNECTION_URL = "wattdepot-server.db.connection.url";
  /**
   * The database connection driver url environment variable.
   */
  public static final String DB_CONNECTION_URL_ENV = "WATTDEPOT_DATABASE_URL";
  /**
   * The database url.
   */
  public static final String DATABASE_URL = "wattdepot-server.database.url";
  /**
   * The database username.
   */
  public static final String DB_USER_NAME = "wattdepot-server.db.username";
  /**
   * The database password.
   */
  public static final String DB_PASSWORD = "wattdepot-server.db.password";
  /**
   * The database show sql.
   */
  public static final String DB_SHOW_SQL = "wattdepot-server.db.show.sql";
  /**
   * The database drop&create tables. valid values are 'validate' | 'update' |
   * 'create' | 'create-drop'.
   */
  public static final String DB_TABLE_UPDATE = "wattdepot-server.db.update";
  /**
   * Enable logging in the server. Logging may hide some stacktraces. Should be
   * True for production.
   */
  public static final String ENABLE_LOGGING_KEY = "wattdepot-server.enable.logging";
  /**
   * Check the Session opens vs close.
   */
  public static final String CHECK_SESSIONS = "wattdepot-server.check.sessions";
  /**
   * The logging level key.
   */
  public static final String LOGGING_LEVEL_KEY = "wattdepot-server.logging.level";
  /**
   * Enable timing of Server operations.
   */
  public static final String SERVER_TIMING_KEY = "wattdepot-server.enable.timing";
  /**
   * The WattDepot implementation class during testing.
   */
  public static final String TEST_WATT_DEPOT_IMPL_KEY = "wattdepot-server.test.wattdepot.impl";
  /**
   * The wattdepot server port key during testing.
   */
  public static final String TEST_PORT_KEY = "wattdepot-server.test.port";
  /**
   * Heroku key.
   */
  public static final String USE_HEROKU_KEY = "wattdepot-server.heroku";
  /**
   * Heroku test key.
   */
  public static final String TEST_HEROKU_KEY = "wattdepot-server.test.heroku";
  /**
   * The hostname for Heroku.
   */
  public static final String HEROKU_HOSTNAME_KEY = "wattdepot-server.heroku.hostname";
  /**
   * The heroku database URL.
   */
  public static final String HEROKU_DATABASE_URL_KEY = "wattdepot-server.heroku.db.url";

  /**
   * String for false.
   */
  public static final String FALSE = "false";
  /**
   * String for true.
   */
  public static final String TRUE = "true";

  /**
   * String for the beginning of the WattDepotServer extension definitions.
   */
  public static final String WATTDEPOT_EXTENSION = "wattdepot-server.extension";
  /**
   * Property key that holds all the WattDepotServer extensions defined in the properties file.
   */
  public static final String WATTDEPOT_EXTENSIONS = WATTDEPOT_EXTENSION + "s";

  /**
   * Where we store the properties.
   */
  private Properties properties;

  /**
   * Creates a new ServerProperties instance using the default filename. Prints
   * an error to the console if problems occur on loading.
   *
   * @throws Exception if errors occur.
   */
  public ServerProperties() throws Exception {
    this(null);
  }

  /**
   * Creates a new ServerProperties instance loaded from the given filename.
   * Prints an error to the console if problems occur on loading.
   *
   * @param serverSubdir The name of the subdirectory used to store all files
   *                     for this server.
   * @throws Exception if errors occur.
   */
  public ServerProperties(String serverSubdir) throws Exception {
    initializeProperties(serverSubdir);
  }

  /**
   * Sets the properties to their "test" equivalents and updates the system
   * properties.
   */
  public void setTestProperties() {
    properties.setProperty(PORT_KEY, properties.getProperty(TEST_PORT_KEY));
    properties.setProperty(WATT_DEPOT_IMPL_KEY, properties.getProperty(TEST_WATT_DEPOT_IMPL_KEY));
    // turn off logging during testing.
    properties.setProperty(LOGGING_LEVEL_KEY, "SEVERE");
    trimProperties();
    // update the system properties object to reflect these new values.
    Properties systemProperties = System.getProperties();
    systemProperties.putAll(properties);
    System.setProperties(systemProperties);
  }

  /**
   * Returns a string containing all current properties in alphabetical order.
   * Properties with the word "password" in their key list "((hidden))" rather
   * than their actual value for security reasons. This is not super
   * sophisticated - other properties such as database URLs might benefit from
   * being hidden too - but it should work for now.
   *
   * @return A string with the properties.
   */
  public String echoProperties() {
    String cr = System.getProperty("line.separator");
    String eq = " = ";
    String pad = "                ";
    // Adding them to a treemap has the effect of alphabetizing them.
    TreeMap<String, String> alphaProps = new TreeMap<String, String>();
    for (Map.Entry<Object, Object> entry : this.properties.entrySet()) {
      String propName = entry.getKey().toString();
      String propValue = entry.getValue().toString();
      alphaProps.put(propName, propValue);
    }
    StringBuffer buff = new StringBuffer(25);
    buff.append("Server Properties:").append(cr);
    for (String key : alphaProps.keySet()) {
      if (key.contains("password") || key.contains("database.url")) {
        buff.append(pad).append(key).append(eq).append("((hidden))").append(cr);
      }
      else {
        buff.append(pad).append(key).append(eq).append(get(key)).append(cr);
      }
    }
    return buff.toString();
  }

  /**
   * Returns the value of the Server Property specified by the key.
   *
   * @param key Should be one of the public static final strings in this class.
   * @return The value of the key, or null if not found.
   */
  public String get(String key) {
    return this.properties.getProperty(key);
  }

  /**
   * Returns the instance.
   *
   * @param key The key.
   * @return The Object associated with the key.
   */
  public Object getPropertyInstance(Object key) {
    return this.properties.get(key);
  }

  /**
   * Sets the given property.
   *
   * @param key   the key.
   * @param value the value
   */
  public void set(String key, String value) {
    this.properties.setProperty(key, value);
  }

  /**
   * Ensures that the there is no leading or trailing whitespace in the property
   * values. The fact that we need to do this indicates a bug in Java's
   * Properties implementation to me.
   */
  public void trimProperties() {
    // Have to do this iteration in a Java 5 compatible manner. no
    // stringPropertyNames().
    for (Map.Entry<Object, Object> entry : properties.entrySet()) {
      String propName = (String) entry.getKey();
      if (properties.getProperty(propName) != null) {
        properties.setProperty(propName, properties.getProperty(propName).trim());
      }
    }
  }

  /**
   * Reads in the properties in ~/.wattdepot3/server/wattdepot-server.properties
   * if this file exists, and provides default values for all properties not
   * mentioned in this file. Will also add any pre-existing System properties
   * that start with "wattdepot-server.".
   *
   * @param serverSubdir The name of the subdirectory used to store all files
   *                     for this server.
   * @throws Exception if errors occur.
   */
  private void initializeProperties(String serverSubdir) throws Exception {
    Logger logger = Logger.getLogger("org.wattdepot.properties");
    String userHome = UserHome.getHomeString();
    String wattDepot3Home = userHome + "/.wattdepot3/";
    String serverHome = null;
    if (serverSubdir == null) {
      serverHome = wattDepot3Home + "server";
    }
    else {
      serverHome = wattDepot3Home + serverSubdir;
    }
    String propFileName = serverHome + "/wattdepot-server.properties";
    this.properties = new Properties();
    // Use properties from file, if they exist.
    FileInputStream stream = null;
    try {
      stream = new FileInputStream(propFileName);
      properties.load(stream);
      logger.info("Loading Server properties from: " + propFileName);

    }
    catch (IOException e) {
      logger.info(propFileName + " not found. Using default server properties.");
    }
    finally {
      if (stream != null) {
        stream.close();
      }
    }
    processDatabaseURL(properties.getProperty(DATABASE_URL));
    processWattDepotExtensions(properties);
//    processDatabaseURL(properties.getProperty(DB_CONNECTION_URL));
    // grab all of the properties in the environment
    Map<String, String> systemProps = System.getenv();
    for (Map.Entry<String, String> prop : systemProps.entrySet()) {
      if (prop.getKey().startsWith(ADMIN_USER_NAME_ENV)) {
        properties.setProperty(ADMIN_USER_NAME, prop.getValue());
      }
      if (prop.getKey().startsWith(ADMIN_USER_PASSWORD_ENV)) {
        properties.setProperty(ADMIN_USER_PASSWORD, prop.getValue());
      }
      if (prop.getKey().startsWith(DB_CONNECTION_URL_ENV)) {
        processDatabaseURL(prop.getValue());
      }
      if (prop.getKey().startsWith("DATABASE_URL")) {
        processDatabaseURL(prop.getValue());
      }
    }
    if (!properties.containsKey(ADMIN_USER_NAME) || !properties.containsKey(ADMIN_USER_PASSWORD) || !properties.containsKey(DB_CONNECTION_URL)) {
      StringBuilder sb = new StringBuilder();
      if (!properties.containsKey(ADMIN_USER_NAME)) {
        sb.append("WattDepot Admin name not set. ");
      }
      if (!properties.containsKey(ADMIN_USER_PASSWORD)) {
        sb.append("WattDepot Admin password not set. ");
      }
      if (!(properties.containsKey(DB_CONNECTION_URL_ENV) || properties.containsKey("DATABASE_URL"))) {
        sb.append("WattDepot database url not set. ");
      }
      throw new SecurityException(sb.toString());
    }
    if (properties.containsKey(SSL) && properties.getProperty(SSL).equals(TRUE)) {
      StringBuilder sb = new StringBuilder();
      if (!properties.containsKey(SSL_KEYSTORE_PASSWORD)) {
        sb.append("Keystore password is not set. ");
      }
      if (!properties.containsKey(SSL_KEYSTORE_KEY_PASSWORD)) {
        sb.append("Keystore key password is not set. ");
      }
      if (sb.length() != 0) {
        throw new SecurityException(sb.toString());
      }
    }

    UserInfo.ROOT.setUid(properties.getProperty(ADMIN_USER_NAME));
    UserInfo.ROOT.setPassword(properties.getProperty(ADMIN_USER_PASSWORD));
    UserPassword.ROOT.setUid(properties.getProperty(ADMIN_USER_NAME));
    UserPassword.ROOT.setPassword(properties.getProperty(ADMIN_USER_PASSWORD));
    String defaultWattDepotImpl = "org.wattdepot.server.depository.impl.hibernate.WattDepotPersistenceImpl";
    // Set default values if not set
    if (!properties.containsKey(SERVER_HOME_DIR)) {
      properties.setProperty(SERVER_HOME_DIR, serverHome);
    }
    if (!properties.containsKey(WATT_DEPOT_IMPL_KEY)) {
      properties.setProperty(WATT_DEPOT_IMPL_KEY, defaultWattDepotImpl);
    }
    if (!properties.containsKey(HOSTNAME_KEY)) {
      properties.setProperty(HOSTNAME_KEY, "localhost");
    }
    if (!properties.containsKey(PORT_KEY)) {
      properties.setProperty(PORT_KEY, "8192");
    }
    if (!properties.containsKey(SSL)) {
      properties.setProperty(SSL, FALSE);
    }
    if (!properties.containsKey(SSL_KEYSTORE_PATH)) {
      properties.setProperty(SSL_KEYSTORE_PATH, serverHome + "/wattdepot.jks");
    }
    if (!properties.containsKey(SSL_KEYSTORE_TYPE)) {
      properties.setProperty(SSL_KEYSTORE_TYPE, "JKS");
    }
    if (!properties.containsKey(DB_CONNECTION_DRIVER)) {
      properties.setProperty(DB_CONNECTION_DRIVER, "org.postgresql.Driver");
    }
    if (!properties.containsKey(DB_CONNECTION_URL)) {
      properties.setProperty(DB_CONNECTION_URL, "jdbc:postgresql://localhost:5432/wattdepot");
    }
    if (!properties.containsKey(DB_SHOW_SQL)) {
      properties.setProperty(DB_SHOW_SQL, FALSE);
    }
    if (!properties.containsKey(DB_TABLE_UPDATE)) {
      properties.setProperty(DB_TABLE_UPDATE, "update");
    }
    if (!properties.containsKey(ENABLE_LOGGING_KEY)) {
      properties.setProperty(ENABLE_LOGGING_KEY, TRUE);
    }
    if (!properties.containsKey(CHECK_SESSIONS)) {
      properties.setProperty(CHECK_SESSIONS, FALSE);
    }
    if (!properties.containsKey(LOGGING_LEVEL_KEY)) {
      properties.setProperty(LOGGING_LEVEL_KEY, "INFO");
    }
    if (!properties.containsKey(CONTEXT_ROOT_KEY)) {
      properties.setProperty(CONTEXT_ROOT_KEY, "wattdepot");
    }
    if (!properties.containsKey(SERVER_TIMING_KEY)) {
      properties.setProperty(SERVER_TIMING_KEY, FALSE);
    }
    if (!properties.containsKey(TEST_PORT_KEY)) {
      properties.setProperty(TEST_PORT_KEY, "8194");
    }
    if (!properties.containsKey(TEST_WATT_DEPOT_IMPL_KEY)) {
      properties.setProperty(TEST_WATT_DEPOT_IMPL_KEY, defaultWattDepotImpl);
    }
    if (!properties.containsKey(USE_HEROKU_KEY)) {
      properties.setProperty(USE_HEROKU_KEY, FALSE);
    }
    if (!properties.containsKey(TEST_HEROKU_KEY)) {
      properties.setProperty(TEST_HEROKU_KEY, FALSE);
    }
    logger.finest(echoProperties());
    trimProperties();
    logger.finest(echoProperties());

    // get PORT and DATABASE_URL for heroku
    String webPort = System.getenv("PORT");
    if (webPort != null && !webPort.isEmpty()) {
      properties.setProperty(PORT_KEY, webPort);
    }

    String databaseURL = System.getenv("DATABASE_URL");
    if (databaseURL != null && !databaseURL.isEmpty()) {
      URI dbUri = new URI(databaseURL);
      String username = dbUri.getUserInfo().split(":")[0];
      String password = dbUri.getUserInfo().split(":")[1];
      String dbUrl = "jdbc:postgresql://" + dbUri.getHost() + dbUri.getPath();
      properties.setProperty(DB_USER_NAME, username);
      properties.setProperty(DB_PASSWORD, password);
      properties.setProperty(DB_CONNECTION_URL, dbUrl);
    }
//    logger.severe(echoProperties());
  }

  /**
   * Consolidates all the WattDepotServer extensions.
   *
   * @param properties The ServerProperties.
   */
  private void processWattDepotExtensions(Properties properties) {
    ArrayList<WattDepotExtension> extensions = new ArrayList<WattDepotExtension>();
    for (Object key : properties.keySet()) {
      if (key.toString().startsWith(WATTDEPOT_EXTENSION)) {
        String extensionClass = properties.getProperty(key.toString());
        try {
          WattDepotExtension extension = (WattDepotExtension) Class.forName(extensionClass).newInstance();
          extensions.add(extension);
        }
        catch (InstantiationException e) {
          e.printStackTrace();
        }
        catch (IllegalAccessException e) {
          e.printStackTrace();
        }
        catch (ClassNotFoundException e) {
          e.printStackTrace();
        }
      }
    }
    properties.put(WATTDEPOT_EXTENSIONS, extensions);
  }

  /**
   * Parses the given database url to set the database username, password, and connection url.
   *
   * @param url the database url.
   * @throws java.net.URISyntaxException if there is a problem with the url.
   */
  private void processDatabaseURL(String url) throws URISyntaxException {
    if (url != null && !url.isEmpty()) {
      URI dbUri = new URI(url);
      if (dbUri.getUserInfo() != null && dbUri.getUserInfo().indexOf(":") != -1) {
        String username = dbUri.getUserInfo().split(":")[0];
        String password = dbUri.getUserInfo().split(":")[1];
        properties.setProperty(DB_USER_NAME, username);
        properties.setProperty(DB_PASSWORD, password);
      }
      String dbUrl = "jdbc:postgresql://" + dbUri.getHost() + ":" + dbUri.getPort() + dbUri.getPath();
      properties.setProperty(DB_CONNECTION_URL, dbUrl);
    }

  }

  /**
   * Returns the fully qualified host name, such as
   * "http://localhost:9876/wattdepot/". Note, the String will end with "/", so
   * there is no need to append another if you are constructing a URI.
   *
   * @return The fully qualified host name.
   */
  public String getFullHost() {
    // We have a special case for Heroku here, which leaves out the port number.
    // This is needed
    // because Heroku apps listen on a private port on localhost, but remote
    // connections into
    // the server always come on port 80. This causes problems because the port
    // number is used
    // in at least 3 places: by the Server to decide what port number to bind to
    // (on Heroku this
    // is the private port # given by the $PORT environment variable), the
    // announced URL of the
    // server to the public (always 80 on Heroku, though usually left out of URI
    // to default to
    // 80), and the URI of parent resources such as a Source in a SensorData
    // resource (should be
    // the public port on Heroku).
    if (properties.getProperty(USE_HEROKU_KEY).equals(TRUE)
        || properties.getProperty(TEST_HEROKU_KEY).equals(TRUE)) {
      return "http://" + get(HOSTNAME_KEY) + ":80/" + get(CONTEXT_ROOT_KEY) + "/";
    }
    else {
      return "http://" + get(HOSTNAME_KEY) + ":" + get(PORT_KEY) + "/" + get(CONTEXT_ROOT_KEY)
          + "/";
    }
  }

}