HibernateConfig.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Carsten Hammer
 *******************************************************************************/
package org.eclipse.jgit.server.config;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.eclipse.jgit.storage.hibernate.config.HibernateSessionFactoryProvider;

/**
 * Creates a Hibernate {@link HibernateSessionFactoryProvider} from environment
 * variables.
 * <p>
 * Supported environment variables:
 * <ul>
 * <li>{@code JGIT_DB_URL} — JDBC connection URL</li>
 * <li>{@code JGIT_DB_USER} — database username</li>
 * <li>{@code JGIT_DB_PASSWORD} — database password</li>
 * <li>{@code JGIT_DB_PASSWORD_FILE} — path to a file containing the
 * database password (Docker secrets pattern). Takes precedence over
 * {@code JGIT_DB_PASSWORD}.</li>
 * <li>{@code JGIT_DB_DIALECT} — Hibernate dialect class name</li>
 * <li>{@code JGIT_DB_DRIVER} — JDBC driver class name (optional)</li>
 * <li>{@code JGIT_DB_DDL_AUTO} — Hibernate DDL auto strategy (default:
 * update)</li>
 * </ul>
 */
public class HibernateConfig {

	private static final Logger LOG = Logger
			.getLogger(HibernateConfig.class.getName());

	private HibernateConfig() {
		// utility class
	}

	/**
	 * Create a session factory provider configured from environment variables.
	 *
	 * @return the configured session factory provider
	 */
	public static HibernateSessionFactoryProvider createSessionFactoryProvider() {
		Properties props = buildProperties();
		LOG.log(Level.INFO,
				"Creating Hibernate SessionFactory with URL: {0}", //$NON-NLS-1$
				props.getProperty("hibernate.connection.url")); //$NON-NLS-1$
		return new HibernateSessionFactoryProvider(props);
	}

	/**
	 * Create a session factory provider from explicit properties.
	 *
	 * @param properties
	 *            Hibernate configuration properties
	 * @return the configured session factory provider
	 */
	public static HibernateSessionFactoryProvider createSessionFactoryProvider(
			Properties properties) {
		return new HibernateSessionFactoryProvider(properties);
	}

	/**
	 * Build Hibernate properties from environment variables.
	 *
	 * @return the Hibernate configuration properties
	 */
	public static Properties buildProperties() {
		Properties props = new Properties();

		String url = getEnvOrDefault("JGIT_DB_URL", //$NON-NLS-1$
				"jdbc:h2:mem:jgit;DB_CLOSE_DELAY=-1"); //$NON-NLS-1$
		String user = getEnvOrDefault("JGIT_DB_USER", "sa"); //$NON-NLS-1$ //$NON-NLS-2$
		String password = readPasswordFromFileOrEnv();
		String dialect = getEnvOrDefault("JGIT_DB_DIALECT", //$NON-NLS-1$
				"org.hibernate.dialect.H2Dialect"); //$NON-NLS-1$
		String driver = System.getenv("JGIT_DB_DRIVER"); //$NON-NLS-1$
		String ddlAuto = getEnvOrDefault("JGIT_DB_DDL_AUTO", "update"); //$NON-NLS-1$ //$NON-NLS-2$

		props.put("hibernate.connection.url", url); //$NON-NLS-1$
		props.put("hibernate.connection.username", user); //$NON-NLS-1$
		props.put("hibernate.connection.password", password); //$NON-NLS-1$
		props.put("hibernate.dialect", dialect); //$NON-NLS-1$
		props.put("hibernate.hbm2ddl.auto", ddlAuto); //$NON-NLS-1$

		if (driver != null && !driver.isEmpty()) {
			props.put("hibernate.connection.driver_class", driver); //$NON-NLS-1$
		}

		// Reasonable defaults for connection pooling
		props.put("hibernate.connection.pool_size", "10"); //$NON-NLS-1$ //$NON-NLS-2$
		props.put("hibernate.show_sql", "false"); //$NON-NLS-1$ //$NON-NLS-2$

		// Apply Hibernate Search / Elasticsearch configuration
		ElasticsearchConfig.applySearchProperties(props);

		return props;
	}

	private static String getEnvOrDefault(String name, String defaultValue) {
		String val = System.getenv(name);
		return (val != null && !val.isEmpty()) ? val : defaultValue;
	}

	/**
	 * Read the database password from a file (Docker secrets pattern) or fall
	 * back to the environment variable.
	 * <p>
	 * If {@code JGIT_DB_PASSWORD_FILE} is set and the file exists, the
	 * password is read from the file (leading/trailing whitespace stripped).
	 * Otherwise, {@code JGIT_DB_PASSWORD} is used.
	 *
	 * @return the database password, or empty string if not configured
	 */
	private static String readPasswordFromFileOrEnv() {
		String passwordFile = System.getenv("JGIT_DB_PASSWORD_FILE"); //$NON-NLS-1$
		if (passwordFile != null && !passwordFile.isEmpty()) {
			try {
				Path path = Path.of(passwordFile);
				long size = Files.size(path);
				if (size > 4096) {
					LOG.log(Level.WARNING,
							"Password file too large ({0} bytes), ignoring: {1}", //$NON-NLS-1$
							new Object[] { Long.toString(size),
									passwordFile });
				} else {
					return Files.readString(path,
							StandardCharsets.UTF_8).trim();
				}
			} catch (IOException e) {
				LOG.log(Level.WARNING,
						"Failed to read password from file: {0}", //$NON-NLS-1$
						passwordFile);
			}
		}
		return getEnvOrDefault("JGIT_DB_PASSWORD", ""); //$NON-NLS-1$ //$NON-NLS-2$
	}
}