/* * Copyright (c) 2010-2013 Evolveum * * 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. */ package com.evolveum.midpoint.repo.sql; import com.evolveum.midpoint.repo.api.RepositoryService; import com.evolveum.midpoint.repo.api.RepositoryServiceFactory; import com.evolveum.midpoint.repo.api.RepositoryServiceFactoryException; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import org.apache.commons.configuration.Configuration; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.h2.Driver; import org.h2.tools.Server; import org.hibernate.dialect.H2Dialect; import java.io.File; import java.io.IOException; import java.net.BindException; import java.net.ServerSocket; import java.util.ArrayList; import java.util.List; /** * @author lazyman */ public class SqlRepositoryFactory implements RepositoryServiceFactory { private static final Trace LOGGER = TraceManager.getTrace(SqlRepositoryFactory.class); private static final String USER_HOME_VARIABLE = "user.home"; private static final String MIDPOINT_HOME_VARIABLE = "midpoint.home"; private static final long C3P0_CLOSE_WAIT = 500L; private static final long H2_CLOSE_WAIT = 2000L; private static final String H2_IMPLICIT_RELATIVE_PATH = "h2.implicitRelativePath"; private boolean initialized; private SqlRepositoryConfiguration sqlConfiguration; private Server server; private SqlPerformanceMonitor performanceMonitor; public SqlRepositoryConfiguration getSqlConfiguration() { Validate.notNull(sqlConfiguration, "Sql repository configuration not available (null)."); return sqlConfiguration; } @Override public synchronized void destroy() throws RepositoryServiceFactoryException { if (!initialized) { LOGGER.info("SQL repository was not initialized, nothing to destroy."); return; } if (performanceMonitor != null) { performanceMonitor.shutdown(); } if (!getSqlConfiguration().isEmbedded()) { LOGGER.info("Repository is not running in embedded mode, shutdown complete."); return; } LOGGER.info("Waiting " + C3P0_CLOSE_WAIT + " ms for the connection pool to be closed."); try { Thread.sleep(C3P0_CLOSE_WAIT); } catch (InterruptedException e) { // just ignore } if (getSqlConfiguration().isAsServer()) { LOGGER.info("Shutting down embedded H2"); if (server != null && server.isRunning(true)) server.stop(); } else { LOGGER.info("H2 running as local instance (from file); waiting " + H2_CLOSE_WAIT + " ms for the DB to be closed."); try { Thread.sleep(H2_CLOSE_WAIT); } catch (InterruptedException e) { // just ignore } } LOGGER.info("Shutdown complete."); initialized = false; } @Override public void destroyService(RepositoryService service) throws RepositoryServiceFactoryException { //we don't need destroying service objects, they will be GC correctly } @Override public synchronized void init(Configuration configuration) throws RepositoryServiceFactoryException { if (initialized) { LOGGER.info("SQL repository already initialized."); return; } Validate.notNull(configuration, "Configuration must not be null."); LOGGER.info("Initializing SQL repository factory"); SqlRepositoryConfiguration config = new SqlRepositoryConfiguration(configuration); normalizeConfiguration(config); config.validate(); sqlConfiguration = config; if (getSqlConfiguration().isEmbedded()) { dropDatabaseIfExists(config); if (System.getProperty(H2_IMPLICIT_RELATIVE_PATH) == null) { System.setProperty(H2_IMPLICIT_RELATIVE_PATH, "true"); // to ensure backwards compatibility (H2 1.3.x) } if (getSqlConfiguration().isAsServer()) { LOGGER.info("Starting h2 in server mode."); startServer(); } else { LOGGER.info("H2 prepared to run in local mode (from file)."); } LOGGER.info("H2 files are in '{}'.", new File(sqlConfiguration.getBaseDir()).getAbsolutePath()); } else { LOGGER.info("Repository is not running in embedded mode."); } performanceMonitor = new SqlPerformanceMonitor(); performanceMonitor.initialize(this); LOGGER.info("Repository initialization finished."); initialized = true; } @Override public RepositoryService getRepositoryService() throws RepositoryServiceFactoryException { return new SqlRepositoryServiceImpl(this); } /** * This method checks actual configuration and updates if it's in embedded mode. (build correct * jdbc url, sets default username and password, driver class and hibernate properties) * * @param config * @throws RepositoryServiceFactoryException * this exception is thrown if baseDir defined in configuration xml doesn't exist or it's not a directory */ private void normalizeConfiguration(SqlRepositoryConfiguration config) throws RepositoryServiceFactoryException { if (!config.isEmbedded()) { return; } StringBuilder jdbcUrl = new StringBuilder(prepareJdbcUrlPrefix(config)); jdbcUrl.append(";MVCC=FALSE"); // turn off MVCC, revert to table locking //jdbcUrl.append(";MV_STORE=FALSE"); // use old page store //disable database closing on exit. By default, a database is closed when the last connection is closed. jdbcUrl.append(";DB_CLOSE_ON_EXIT=FALSE"); //Both read locks and write locks are kept until the transaction commits. jdbcUrl.append(";LOCK_MODE=1"); //fix for "Timeout trying to lock table [50200-XXX]" in H2 database. Default value is 1000ms. jdbcUrl.append(";LOCK_TIMEOUT=10000"); //we want to store blob datas (full xml object right in table (it's always only a few kb) jdbcUrl.append(";MAX_LENGTH_INPLACE_LOB=10240"); config.setJdbcUrl(jdbcUrl.toString()); LOGGER.trace("JDBC url created: {}", new Object[]{config.getJdbcUrl()}); config.setJdbcUsername("sa"); config.setJdbcPassword(""); config.setDriverClassName(Driver.class.getName()); config.setHibernateDialect(H2Dialect.class.getName()); config.setHibernateHbm2ddl("update"); } /** * Prepares a prefix (first part) of JDBC URL for embedded database. Used also by configurator of tasks (quartz) * and workflow (activiti) modules; they add their own db names and parameters to this string. * * @param config * @return prefix of JDBC URL like jdbc:h2:file:d:\midpoint\midpoint */ public String prepareJdbcUrlPrefix(SqlRepositoryConfiguration config) throws RepositoryServiceFactoryException { if (StringUtils.isEmpty(config.getFileName())) { config.setFileName("midpoint"); } if (StringUtils.isEmpty(config.getBaseDir())) { LOGGER.debug("Base dir path in configuration was not defined."); if (StringUtils.isNotEmpty(System.getProperty(MIDPOINT_HOME_VARIABLE))) { config.setBaseDir(System.getProperty(MIDPOINT_HOME_VARIABLE)); LOGGER.info("Using {} with value {} as base dir for configuration.", new Object[]{MIDPOINT_HOME_VARIABLE, config.getBaseDir()}); } else if (StringUtils.isNotEmpty(System.getProperty(USER_HOME_VARIABLE))) { config.setBaseDir(System.getProperty(USER_HOME_VARIABLE)); LOGGER.info("Using {} with value {} as base dir for configuration.", new Object[]{USER_HOME_VARIABLE, config.getBaseDir()}); } else { config.setBaseDir("."); LOGGER.info("Using '.' as base dir for configuration ({}, or {} was not defined).", new Object[]{MIDPOINT_HOME_VARIABLE, USER_HOME_VARIABLE}); } } File baseDir = new File(config.getBaseDir()); if (!baseDir.exists() || !baseDir.isDirectory()) { throw new RepositoryServiceFactoryException("File '" + config.getBaseDir() + "' defined as baseDir doesn't exist or is not a directory."); } StringBuilder jdbcUrl = new StringBuilder("jdbc:h2:"); if (config.isAsServer()) { //jdbc:h2:tcp://<server>[:<port>]/[<path>]<databaseName> jdbcUrl.append("tcp://127.0.0.1:"); jdbcUrl.append(config.getPort()); jdbcUrl.append("/"); jdbcUrl.append(config.getFileName()); } else { //jdbc:h2:[file:][<path>]<databaseName> jdbcUrl.append("file:"); File databaseFile = new File(config.getBaseDir(), config.getFileName()); jdbcUrl.append(databaseFile.getAbsolutePath()); } return jdbcUrl.toString(); } private String getRelativeBaseDirPath(String baseDir) { String path = new File(".").toURI().relativize(new File(baseDir).toURI()).getPath(); if (StringUtils.isEmpty(path)) { path = "."; } return path; } private void checkPort(int port) throws RepositoryServiceFactoryException { if (port >= 65635 || port < 0) { throw new RepositoryServiceFactoryException("Port must be in range 0-65634, not '" + port + "'."); } ServerSocket ss = null; try { ss = new ServerSocket(port); ss.setReuseAddress(true); } catch (BindException e) { throw new RepositoryServiceFactoryException("Configured port (" + port + ") for H2 already in use.", e); } catch (IOException e) { LOGGER.error("Reported IO error, while binding ServerSocket to port "+port+" used to test availability " + "of port for H2 Server", e); } finally { try { if (ss != null) { ss.close(); } } catch (IOException ex) { LOGGER.error("Reported IO error, while closing ServerSocket used to test availability " + "of port for H2 Server", ex); } } } private void startServer() throws RepositoryServiceFactoryException { SqlRepositoryConfiguration config = getSqlConfiguration(); checkPort(config.getPort()); try { String[] serverArguments = createArguments(config); if (LOGGER.isTraceEnabled()) { String stringArgs = StringUtils.join(serverArguments, " "); LOGGER.trace("Starting H2 server with arguments: {}", stringArgs); } server = Server.createTcpServer(serverArguments); server.start(); } catch (Exception ex) { throw new RepositoryServiceFactoryException(ex.getMessage(), ex); } } private String[] createArguments(SqlRepositoryConfiguration config) { // [-help] or [-?] Print the list of options // [-web] Start the web server with the H2 Console // [-webAllowOthers] Allow other computers to connect - see below // [-webDaemon] Use a daemon thread // [-webPort <port>] The port (default: 8082) // [-webSSL] Use encrypted (HTTPS) connections // [-browser] Start a browser connecting to the web server // [-tcp] Start the TCP server // [-tcpAllowOthers] Allow other computers to connect - see below // [-tcpDaemon] Use a daemon thread // [-tcpPort <port>] The port (default: 9092) // [-tcpSSL] Use encrypted (SSL) connections // [-tcpPassword <pwd>] The password for shutting down a TCP server // [-tcpShutdown "<url>"] Stop the TCP server; example: tcp://localhost // [-tcpShutdownForce] Do not wait until all connections are closed // [-pg] Start the PG server // [-pgAllowOthers] Allow other computers to connect - see below // [-pgDaemon] Use a daemon thread // [-pgPort <port>] The port (default: 5435) // [-properties "<dir>"] Server properties (default: ~, disable: null) // [-baseDir <dir>] The base directory for H2 databases (all servers) // [-ifExists] Only existing databases may be opened (all servers) // [-trace] Print additional trace information (all servers) List<String> args = new ArrayList<String>(); if (StringUtils.isNotEmpty(config.getBaseDir())) { args.add("-baseDir"); args.add(getRelativeBaseDirPath(config.getBaseDir())); } if (config.isTcpSSL()) { args.add("-tcpSSL"); } if (config.getPort() > 0) { args.add("-tcpPort"); args.add(Integer.toString(config.getPort())); } return args.toArray(new String[args.size()]); } private void dropDatabaseIfExists(SqlRepositoryConfiguration config) throws RepositoryServiceFactoryException { if (!config.isDropIfExists()) { LOGGER.info("Database wont be deleted, dropIfExists=false."); return; } LOGGER.info("Deleting database."); File file = new File(config.getBaseDir()); final String fileName = config.getFileName(); try { //removing files based on http://www.h2database.com/html/features.html#database_file_layout File dbFileOld = new File(file, fileName + ".h2.db"); removeFile(dbFileOld); File dbFile = new File(file, fileName + ".mv.db"); removeFile(dbFile); File lockFile = new File(file, fileName + ".lock.db"); removeFile(lockFile); File traceFile = new File(file, fileName + ".trace.db"); removeFile(traceFile); File[] tempFiles = file.listFiles((parent, name) -> { if (name.matches("^" + fileName + "\\.[0-9]*\\.temp\\.db$")) { return true; } return false; }); if (tempFiles != null) { for (File temp : tempFiles) { removeFile(temp); } } File lobDir = new File(file, fileName + ".lobs.db"); if (lobDir.exists() && lobDir.isDirectory()) { LOGGER.info("Deleting directory '{}'", new Object[]{lobDir.getAbsolutePath()}); FileUtils.deleteDirectory(lobDir); } } catch (Exception ex) { throw new RepositoryServiceFactoryException("Couldn't drop existing database files, reason: " + ex.getMessage(), ex); } } private void removeFile(File file) throws IOException { if (file.exists()) { LOGGER.info("Deleting file '{}', result: {}", new Object[]{file.getAbsolutePath(), file.delete()}); } else { LOGGER.info("File '{}' doesn't exist.", new Object[]{file.getAbsolutePath(), file.delete()}); } } public SqlPerformanceMonitor getPerformanceMonitor() { return performanceMonitor; } }