/*
* ProActive Parallel Suite(TM):
* The Open Source library for parallel and distributed
* Workflows & Scheduling, Orchestration, Cloud Automation
* and Big Data Analysis on Enterprise Grids & Clouds.
*
* Copyright (c) 2007 - 2017 ActiveEon
* Contact: contact@activeeon.com
*
* This library 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: version 3 of
* the License.
*
* 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/>.
*
* If needed, contact us to obtain a release under GPL Version 2 or 3
* or a different license than the AGPL.
*/
package org.ow2.proactive.scheduler.util;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Properties;
import org.apache.log4j.Logger;
import org.hsqldb.Server;
import org.hsqldb.persist.HsqlProperties;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.AbstractIdleService;
/**
* This class encapsulates the logic to create an HSQLDB instance in server mode.
* <p>
* The server manages catalogs. Each catalog depicts an isolated database.
* <p>
* When HSQLDB is started in server mode, multiple clients can connect to it
* at the same time, which is not the case with the file or in-memory mode.
*
* @author ActiveEon Team
*/
public class HsqldbServer extends AbstractIdleService {
private static final Logger logger = Logger.getLogger(HsqldbServer.class);
/**
* Hibernate properties.
*/
protected static final String PROP_HIBERNATE_CONNECTION_URL = "hibernate.connection.url";
protected static final String PROP_HIBERNATE_CONNECTION_USERNAME = "hibernate.connection.username";
@SuppressWarnings("squid:S2068")
protected static final String PROP_HIBERNATE_CONNECTION_PASSWORD = "hibernate.connection.password";
/*
* HSQLDB properties.
*/
protected static final String HSQLDB_DOCUMENTATION_URL = "http://hsqldb.org/doc/guide/dbproperties-chapt.html";
// custom property used by tests to be able to pass catalog location in Hibernate configuration file
protected static final String PROP_HSQLDB_PREFIX_CATALOG_PATH = "catalog.path";
protected static final String PROP_HSQLDB_PREFIX_DBNAME = "server.dbname";
protected static final String PROP_HSQLDB_PREFIX_SERVER_DATABASE = "server.database";
// this property is not a common HSQLDB property but a custom one to configure catalog options
protected static final String PROP_HSQLDB_SERVER_CATALOGS_OPTION_LINE = "server.catalogs.option_line";
/*
* Instance variables.
*/
private int catalogIndex;
private final String catalogOptionLine;
private final HsqlProperties hsqlProperties;
private Server server;
/**
* Creates a new instance of HSQLDB server.
*/
HsqldbServer() {
catalogOptionLine = "";
hsqlProperties = new HsqlProperties();
}
/**
* Creates a new instance of HSQLDB server.
*
* @param configuration the path to the HSQLDB server properties file.
*
* @throws IOException if an error occurs while reading the configuration file.
*/
public HsqldbServer(Path configuration) throws IOException {
Properties hsqldbServerProperties = loadProperties(configuration);
catalogOptionLine = hsqldbServerProperties.getProperty(PROP_HSQLDB_SERVER_CATALOGS_OPTION_LINE);
hsqldbServerProperties.remove(PROP_HSQLDB_SERVER_CATALOGS_OPTION_LINE);
hsqlProperties = new HsqlProperties(hsqldbServerProperties);
}
public void addCatalog(Path defaultLocation, Path hibernateConfiguration) throws IOException {
Properties hibernateProperties = loadProperties(hibernateConfiguration);
String connectionUrl = hibernateProperties.getProperty(PROP_HIBERNATE_CONNECTION_URL);
String catalogLocation = identifyCatalogLocationFromConnectionUrl(connectionUrl);
String catalogName = identifyCatalogNameFromConnectionUrl(connectionUrl);
String catalogPassword = hibernateProperties.getProperty(PROP_HIBERNATE_CONNECTION_PASSWORD);
String catalogUsername = hibernateProperties.getProperty(PROP_HIBERNATE_CONNECTION_USERNAME);
Path catalogPath;
if (catalogLocation == null) {
// Catalog path references a file.
// Double resolve is used to get a dedicated folder with the catalog name.
catalogPath = defaultLocation.resolve(catalogName).resolve(catalogName);
} else {
catalogPath = Paths.get(catalogLocation);
}
addCatalog(catalogPath, catalogName, catalogUsername, catalogPassword);
}
public void addCatalog(Path catalogLocation, String catalogName, String catalogUsername, String catalogPassword) {
if (state() != State.NEW) {
throw new IllegalStateException("Catalogs configuration must be done before starting the server");
}
String catalogIndexAsString = Integer.toString(catalogIndex);
// See documentation, Table 14.1:
// http://hsqldb.org/doc/guide/listeners-chapt.html
hsqlProperties.setProperty(PROP_HSQLDB_PREFIX_SERVER_DATABASE + "." + catalogIndexAsString,
createCatalogOptions(catalogLocation,
catalogOptionLine,
catalogUsername,
catalogPassword));
hsqlProperties.setProperty(PROP_HSQLDB_PREFIX_DBNAME + "." + catalogIndexAsString, catalogName);
catalogIndex++;
}
@VisibleForTesting
String createCatalogOptions(Path catalogPath, String catalogOptionLine, String catalogUsername,
String catalogPassword) {
return "file:" + catalogPath.toString() + ";user=" + catalogUsername + ";password=" + catalogPassword + ";" +
catalogOptionLine;
}
// See HSQLDB documentation, table 13.4 (Server Database URL) for input examples
// http://hsqldb.org/doc/guide/dbproperties-chapt.html#dpc_db_props_url
@VisibleForTesting
String identifyCatalogNameFromConnectionUrl(String connectionUrl) {
String[] chunks = connectionUrl.split(":");
if (chunks.length != 4 && chunks.length != 5) {
throw new IllegalArgumentException("Invalid connection URL: " + connectionUrl +
". Please look at the HSQLDB documentation: " +
HSQLDB_DOCUMENTATION_URL);
}
// discard connection options
chunks = chunks[chunks.length - 1].split(";");
// split to identify catalog name
chunks = chunks[0].split("/");
return chunks[chunks.length - 1];
}
@VisibleForTesting
String identifyCatalogLocationFromConnectionUrl(String connectionUrl) {
String[] chunks = connectionUrl.split(";");
for (String chunk : chunks) {
String[] split = chunk.split("=");
if (split.length == 1) {
continue;
}
String key = split[0];
String value = split[1];
if (PROP_HSQLDB_PREFIX_CATALOG_PATH.equals(key)) {
return value;
}
}
return null;
}
@VisibleForTesting
HsqlProperties getHsqlProperties() {
return hsqlProperties;
}
@Override
protected void startUp() throws Exception {
server = new Server();
server.setProperties(hsqlProperties);
server.setErrWriter(null);
server.setLogWriter(null);
server.start();
}
@Override
protected void shutDown() throws Exception {
server.stop();
}
public static boolean isServerModeRequired(Path hibernateConfiguration, Path... others) throws IOException {
List<Path> hibernateConfigurations = ImmutableList.<Path> builder()
.add(hibernateConfiguration)
.add(others)
.build();
boolean result = false;
for (Path config : hibernateConfigurations) {
Properties hibernateProperties = loadProperties(config);
result |= isServerConnectionConfigured(hibernateProperties);
}
return result;
}
/**
* Check if the server connection for HSQLDB is set for the given set of properties.
*
* @param hibernateProperties the properties to look at for checking configuration.
* @return a boolean that says if starting HSQLDB in server mode is required.
*/
@VisibleForTesting
static boolean isServerConnectionConfigured(Properties hibernateProperties) {
String dbConnectionUrl = hibernateProperties.getProperty(PROP_HIBERNATE_CONNECTION_URL);
if (dbConnectionUrl == null) {
return false;
} else if (dbConnectionUrl.startsWith("jdbc:hsqldb:hsql")) {
return true;
} else if (dbConnectionUrl.startsWith("jdbc:hsqldb:")) {
String message = "Non server database URL detected. HSQLDB must be configured in server mode to " +
"work smoothly with ProActive Workflows and Scheduling. Please look at HSQLDB documentation: " +
HSQLDB_DOCUMENTATION_URL;
logger.warn(message);
return false;
} else {
return false;
}
}
private static Properties loadProperties(Path configuration) throws IOException {
Properties dbProperties = new Properties();
try (InputStream stream = Files.newInputStream(configuration)) {
dbProperties.load(stream);
}
return dbProperties;
}
}