All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.svws_nrw.db.ConnectionManager Maven / Gradle / Ivy

Go to download

Diese Bibliothek regelt den Zugriff auf Datenbanken für die Schulverwaltungssoftware in NRW

The newest version!
package de.svws_nrw.db;

import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLInvalidAuthorizationSpecException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.sessions.server.ConnectionPool;
import org.eclipse.persistence.sessions.server.ServerSession;

import com.healthmarketscience.jackcess.Database;
import com.healthmarketscience.jackcess.DatabaseBuilder;

import de.svws_nrw.core.logger.LogLevel;
import de.svws_nrw.core.logger.Logger;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import jakarta.persistence.PersistenceException;
import jakarta.validation.constraints.NotNull;

/**
 * Ein Manager für die Datenbank-Verbindungen der Anwendung.
 */
public final class ConnectionManager {

	/*
	 * Initialisiert den Shutdown-Hook, um alle nicht mehr benötigten
	 * Datenbank-Verbindungen, d.h. die zugehörigen {@link EntityManagerFactory}
	 * zu schließen.
	 */
	static {
		Runtime.getRuntime().addShutdownHook(new Thread(ConnectionManager::closeAll));
	}

	/** Ein Zufallszahlen-Generator */
	private static final Random random = new Random();

	/**
	 * Eine HashMap für den Zugriff auf einen Connection-Manager, der einer
	 * Datenbank-Konfiguration zugeordnet ist
	 */
	private static final HashMap mapManager = new HashMap<>();

	/** Die verwendete Datenbank-Konfiguration {@link DBConfig} */
	private final @NotNull DBConfig config;

	/**
	 * Die zum Erzeugen der {@link EntityManager} verwendete Instanz der
	 * {@link EntityManagerFactory}
	 */
	private final @NotNull EntityManagerFactory emf;

	/**
	 * Erstellt einen neuen Connection-Manager
	 *
	 * @param config die Konfiguration für den Connection-Manager
	 */
	private ConnectionManager(final @NotNull DBConfig config) {
		this.config = config;
		this.emf = createEntityManagerFactory();
	}

	/**
	 * Gibt einen neuen JPA {@link EntityManager} zurück. Diese Methode wird
	 * innerhalb dieses Packages vom DBEntityManager bei der Erneuerung der
	 * Verbindung verwendet.
	 *
	 * @return der neue JPA {@link EntityManager}
	 */
	EntityManager getNewJPAEntityManager() {
		return emf.createEntityManager();
	}

	/**
	 * Gibt einen neuen JPA {@link EntityManager} zurück. Diese Methode wird
	 * innerhalb dieses Packages vom DBEntityManager bei der Erneuerung der
	 * Verbindung verwendet.
	 * Bei dieser Variante werden mehrere Versuche für einen Verbindungsaufbau
	 * durchgeführt. Zwischen den Versuchen wird eine angebene Zeit in Millisekunden
	 * abgewartet.
	 *
	 * @param connectionRetries   die Anzahl der Verbindungsversuche, bevor eine Exception weitergereicht wird
	 * @param retryTimeout   die Zeit in Millisekunden
	 *
	 * @return der neue JPA {@link EntityManager}
	 */
	EntityManager getNewJPAEntityManager(final int connectionRetries, final long retryTimeout) {
		PersistenceException resultingException = null;
		int triesLeft = connectionRetries + 1;
		do {
			triesLeft--;
			try {
				return getNewJPAEntityManager();
			} catch (final PersistenceException e) {
				resultingException = e;
				if (triesLeft <= 0) {
					throw resultingException;
				}
				try {
					Thread.sleep(retryTimeout);
				} catch (@SuppressWarnings("unused") final InterruptedException ie) {
					Thread.currentThread().interrupt();
				}
			}
		} while (triesLeft > 0);
		throw resultingException;
	}

	/**
	 * Gibt die Datenbank-Konfiguration dieses Verbindungs-Managers zurück.
	 *
	 * @return die Datenbank-Konfiguration dieses Verbindungs-Managers
	 */
	public DBConfig getConfig() {
		return this.config;
	}

	/**
	 * Intern genutzte Methode, um eine {@link EntityManagerFactory} für diesen
	 * {@link ConnectionManager} zu erstellen. Hierbei werden auch die
	 * Standardeinstellungen für die Datenbankverbindung in Form der
	 * Property-Map hinzugefügt (siehe
	 * {@link Persistence#createEntityManagerFactory(String, java.util.Map)})
	 *
	 * @return die {@link EntityManagerFactory}
	 */
	private @NotNull EntityManagerFactory createEntityManagerFactory() {
		final HashMap propertyMap = new HashMap<>();
		propertyMap.put("jakarta.persistence.jdbc.driver", config.getDBDriver().getJDBCDriver());
		final String username = config.getUsername();
		String password = config.getPassword();
		String url = config.getDBDriver().getJDBCUrl(config.getDBLocation(), config.getDBSchema());
		if (config.getDBDriver() == DBDriver.MDB) {
			try (Database db = DatabaseBuilder.open(new File(config.getDBLocation()))) {
				password = db.getDatabasePassword();
			} catch (@SuppressWarnings("unused") final IOException e) {
				password = "";
			}
			if (config.createDBFile())
				url += ";newdatabaseversion=V2000";
		}
		final String sessionName = "SVWSDB_url=" + url + "_user=" + config.getUsername() + "_random=" + random.ints(48, 123)  // from 0 to z
				.filter(i -> ((i <= 57) || (i >= 65)) && ((i <= 90) || (i >= 97)))  // filter some unicode characters
				.limit(40)
				.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
				.toString();
		propertyMap.put("jakarta.persistence.jdbc.url", url);
		propertyMap.put("jakarta.persistence.jdbc.user", username);
		propertyMap.put("jakarta.persistence.jdbc.password", password);
		propertyMap.put("eclipselink.session-name", sessionName);
		propertyMap.put("eclipselink.flush", "true");
		propertyMap.put("eclipselink.persistence-context.flush-mode", "commit");
		propertyMap.put("eclipselink.allow-zero-id", "true");
		propertyMap.put("eclipselink.logging.level", config.useDBLogging() ? "WARNING" : "OFF");
		// propertyMap.put("eclipselink.logging.level", config.useDBLogging() ? "INFO" : "OFF");
		// propertyMap.put("eclipselink.logging.level", "ALL");
		// propertyMap.put("eclipselink.logging.level.sql", "FINE");
		// propertyMap.put("eclipselink.logging.parameters", "true");
		// propertyMap.put("eclipselink.profiler","PerformanceProfiler");
		propertyMap.put("eclipselink.cache.shared.default", "false");
		// propertyMap.put("eclipselink.exception-handler",
		// "de.svws_nrw.db.DBExceptionHandler");
		if (config.getDBDriver() == DBDriver.SQLITE) {
			propertyMap.put("eclipselink.target-database", "Database");
			// Einstellungen des SQ-Lite-Treibers
			// READWRITE (2) + CREATE (4) + OPEN_URI (64) = 70 bzw. READWRITE (2) + OPEN_URI (64) = 66
			propertyMap.put("open_mode", (config.createDBFile()) ? "70" : "66");
			propertyMap.put("foreign_keys", "true");
		}
		return Persistence.createEntityManagerFactory("SVWSDB", propertyMap);
		// TODO avoid Persistence Unit "SVWSDB" as xml file
	}


	/**
	 * Schließt den Verbindungs-Manager
	 */
	private void close() {
		emf.close();
	}

	/**
	 * Gibt den Manager für die Datenbank-Verbindung für die übergebene
	 * Konfiguration zurück. Sollt keine Verbindung bestehen, so wird eine neue
	 * Verbindung erzeugt.
	 *
	 * @param config die Konfiguration der Datenbank-Verbindung
	 *
	 * @return der Manager
	 *
	 * @throws DBException bei einer fehlschlagenden Authentifizierung
	 */
	public static @NotNull ConnectionManager get(final DBConfig config) throws DBException {
		ConnectionManager man = mapManager.get(config);
		if (man != null) {
			final Map curProps = man.emf.getProperties();
			final String curUser = (String) curProps.get("jakarta.persistence.jdbc.user");
			final String curPassword = (String) curProps.get("jakarta.persistence.jdbc.password");
			if (!config.getUsername().equals(curUser) || !config.getPassword().equals(curPassword)) {
				mapManager.remove(config);
				man.close();
				man = null;
			}
		}
		if (man == null) {
			man = new ConnectionManager(config);
			try {
				try (EntityManager em = man.getNewJPAEntityManager()) {
					mapManager.put(config, man);
				}
			} catch (final PersistenceException pe) {
				if ((pe.getCause() instanceof final DatabaseException de) && (de.getCause() instanceof final SQLInvalidAuthorizationSpecException ae)) {
					man.close();
					throw new DBException("Fehler beim Aufbau der Verbindung. Überprüfen Sie Benutzername und Kennwort.", ae);
				}
				if (pe.getCause() instanceof DatabaseException) {
					man.close();
					throw new DBException("Fehler beim Aufbau der Verbindung. Überprüfen Sie die Verbindungsparameter.");
				}
				if (pe.getMessage().startsWith("java.lang.IllegalStateException: Could not determine FileFormat")) {
					man.close();
					throw new DBException("Fehlerhaftes oder zu altes MDB-Datei-Format.");
				}
				throw pe;
			}
		} else {
			// Führe eine Dummy-DB-Abfrage aus, um Probleme mit der
			// Server-seitigen Beendung einer Verbindung zu erkennen
			try {
				try (EntityManager em = man.getNewJPAEntityManager()) {
					try {
						em.getTransaction().begin();
						@SuppressWarnings("resource") final Connection conn = em.unwrap(Connection.class);
						try (Statement stmt = conn.createStatement()) {
							try (ResultSet rs = stmt.executeQuery("SELECT 1")) {
								rs.next();
								rs.getInt(1);
							}
						}
						em.getTransaction().commit();
						em.clear();
					} catch (@SuppressWarnings("unused") SQLException | DatabaseException e) {
						// Bestimme die Anzahl der verfügbaren Verbindungen
						final ServerSession serverSession = em.unwrap(ServerSession.class);
						final ConnectionPool pool = serverSession.getConnectionPools().get("default");
						if (pool == null) {
							Logger.global().logLn(LogLevel.ERROR, "Fehler beim Zugriff auf den DB-Connection-Pool default");
						} else {
							Logger.global().logLn(LogLevel.ERROR, "INFO: Verbindung zur Datenbank unterbrochen - versuche sie neu aufzubauen...");
							Logger.global().logLn(LogLevel.ERROR, "Total number of connections: " + pool.getTotalNumberOfConnections());
							Logger.global().logLn(LogLevel.ERROR, "Available number of connections: " + pool.getConnectionsAvailable().size());
							pool.resetConnections();
						}
					}
				}
			} catch (final PersistenceException pe) {
				if ((pe.getCause() instanceof final DatabaseException de) && (de.getCause() instanceof final SQLInvalidAuthorizationSpecException ae)) {
					mapManager.remove(config);
					man.close();
					throw new DBException(ae);
				}
				throw pe;
			}
		}
		return man;
	}

	/**
	 * Schließt den Connection-Manager für die übergebene Config und entfernt
	 * ihn aus der Liste der Manager
	 *
	 * @param config die Konfiguration des zu schließenden Managers
	 */
	private static void closeSingle(final DBConfig config) {
		final ConnectionManager manager = mapManager.get(config);
		if (manager == null) {
			Logger.global().logLn(LogLevel.ERROR, "Fehler beim Schließen des Verbindungs-Managers zu %s (Schema: %s), Datenbank-Benutzer: %s"
					.formatted(config.getDBLocation(), config.getDBSchema(), config.getUsername()));
			return;
		}
		manager.close();
		mapManager.remove(config);
		Logger.global().logLn(LogLevel.INFO, "Verbindungs-Manager des Datenbank-Benutzers %s zu %s (Schema: %s) geschlossen."
				.formatted(config.getUsername(), config.getDBLocation(), config.getDBSchema()));
	}

	/**
	 * Schließt alle noch offenenen Datenbank-Verbindungen.
	 */
	private static void closeAll() {
		final List configs = mapManager.keySet().stream().toList();
		for (final DBConfig config : configs)
			closeSingle(config);
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy