package org.jabref.shared;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jabref.model.entry.BibEntry;
import org.jabref.shared.listener.PostgresSQLNotificationListener;
import com.impossibl.postgres.api.jdbc.PGConnection;
import com.impossibl.postgres.jdbc.PGDataSource;
import com.impossibl.postgres.jdbc.ThreadedHousekeeper;
/**
* Processes all incoming or outgoing bib data to PostgreSQL database and manages its structure.
*/
public class PostgreSQLProcessor extends DBMSProcessor {
private PGConnection pgConnection;
private PostgresSQLNotificationListener listener;
public PostgreSQLProcessor(DBMSConnection connection) {
super(connection);
}
/**
* Creates and sets up the needed tables and columns according to the database type.
*
* @throws SQLException
*/
@Override
public void setUp() throws SQLException {
connection.createStatement().executeUpdate(
"CREATE TABLE IF NOT EXISTS \"ENTRY\" (" +
"\"SHARED_ID\" SERIAL PRIMARY KEY, " +
"\"TYPE\" VARCHAR, " +
"\"VERSION\" INTEGER DEFAULT 1)");
connection.createStatement().executeUpdate(
"CREATE TABLE IF NOT EXISTS \"FIELD\" (" +
"\"ENTRY_SHARED_ID\" INTEGER REFERENCES \"ENTRY\"(\"SHARED_ID\") ON DELETE CASCADE, " +
"\"NAME\" VARCHAR, " +
"\"VALUE\" TEXT)");
connection.createStatement().executeUpdate(
"CREATE TABLE IF NOT EXISTS \"METADATA\" ("
+ "\"KEY\" VARCHAR,"
+ "\"VALUE\" TEXT)");
}
@Override
protected void insertIntoEntryTable(BibEntry bibEntry) {
// Inserting into ENTRY table
StringBuilder insertIntoEntryQuery = new StringBuilder()
.append("INSERT INTO ")
.append(escape("ENTRY"))
.append("(")
.append(escape("TYPE"))
.append(") VALUES(?)");
// This is the only method to get generated keys which is accepted by MySQL, PostgreSQL and Oracle.
try (PreparedStatement preparedEntryStatement = connection.prepareStatement(insertIntoEntryQuery.toString(),
Statement.RETURN_GENERATED_KEYS)) {
preparedEntryStatement.setString(1, bibEntry.getType());
preparedEntryStatement.executeUpdate();
try (ResultSet generatedKeys = preparedEntryStatement.getGeneratedKeys()) {
if (generatedKeys.next()) {
bibEntry.getSharedBibEntryData().setSharedID(generatedKeys.getInt(1)); // set generated ID locally
}
}
} catch (SQLException e) {
LOGGER.error("SQL Error: ", e);
}
}
@Override
String escape(String expression) {
return "\"" + expression + "\"";
}
@Override
public void startNotificationListener(DBMSSynchronizer dbmsSynchronizer) {
// Disable cleanup output of ThreadedHousekeeper
Logger.getLogger(ThreadedHousekeeper.class.getName()).setLevel(Level.SEVERE);
this.listener = new PostgresSQLNotificationListener(dbmsSynchronizer);
PGDataSource dataSource = new PGDataSource();
dataSource.setHost(connectionProperties.getHost());
dataSource.setPort(connectionProperties.getPort());
dataSource.setDatabase(connectionProperties.getDatabase());
dataSource.setUser(connectionProperties.getUser());
dataSource.setPassword(connectionProperties.getPassword());
try {
pgConnection = (PGConnection) dataSource.getConnection();
pgConnection.createStatement().execute("LISTEN jabrefLiveUpdate");
// Do not use `new PostgresSQLNotificationListener(...)` as the object has to exist continuously!
// Otherwise the listener is going to be deleted by GC.
pgConnection.addNotificationListener(listener);
} catch (SQLException e) {
LOGGER.error("SQL Error: ", e);
}
}
@Override
public void stopNotificationListener() {
try {
pgConnection.close();
} catch (SQLException e) {
LOGGER.error("SQL Error: ", e);
}
}
@Override
public void notifyClients() {
try {
pgConnection.createStatement().execute("NOTIFY jabrefLiveUpdate, '" + PROCESSOR_ID + "';");
} catch (SQLException e) {
LOGGER.error("SQL Error: ", e);
}
}
}