From e560220bc74430cbeeb58838f9b0db75a6555a03 Mon Sep 17 00:00:00 2001 From: Siedlerchr Date: Wed, 19 Feb 2020 19:17:20 +0100 Subject: [PATCH 1/6] followup fix --- .../java/org/jabref/logic/importer/fetcher/DBLPFetcherTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/jabref/logic/importer/fetcher/DBLPFetcherTest.java b/src/test/java/org/jabref/logic/importer/fetcher/DBLPFetcherTest.java index 8aa622253aa..8cd70f8e345 100644 --- a/src/test/java/org/jabref/logic/importer/fetcher/DBLPFetcherTest.java +++ b/src/test/java/org/jabref/logic/importer/fetcher/DBLPFetcherTest.java @@ -44,7 +44,7 @@ public void setUp() { entry.setField(StandardField.YEAR, "2016"); entry.setField(StandardField.URL, "http://pi.informatik.uni-siegen.de/stt/36_2/./03_Technische_Beitraege/ZEUS2016/beitrag_2.pdf"); - entry.setField(new UnknownField("biburl"), "{https://dblp.org/rec/journals/stt/GeigerHL16.bib"); + entry.setField(new UnknownField("biburl"), "https://dblp.org/rec/journals/stt/GeigerHL16.bib"); entry.setField(new UnknownField("bibsource"), "dblp computer science bibliography, https://dblp.org"); } From 93196eeb819024079e94dd1eea3b22af993723cf Mon Sep 17 00:00:00 2001 From: Abraham Polk Date: Wed, 19 Feb 2020 18:57:05 -0500 Subject: [PATCH 2/6] [WIP] Initial work on DBMSProcessor batch entry insertion into ENTRY table (#5814) * Initial work on DBMSProcessor entry insertion into ENTRY table * Change syntax for Oracle multi-row insert SQL statement * Run tests also when source files changed * Add to comment about Oracle * Assume ResultSet is in order for setting shared IDs * Add insertEntry for DBMSProcessor tests and fix PostgresSQLProcessor * Fix SQL typo * Separate table drops in Oracle tests * Remove CI tests that were added in branch * Work on unit test for DBMSProcessor insertEntries * Fix bug in DBMSProcessorTest and simplify DBMSProcessor.FilterForBibEntryExistence * Remove Oracle connection bug with wrong port * Add Oracle insertIntoEntryTable * Oracle connection fix - taken from fix_fields_sql branch * Fix typo bug * Clean up code * Remove commented blocks * Remove comment about needing a test that probably isn't necessary * Manually merge fix_fields_sql OracleProcessor (just add method) * Emphasize todo * setSharedID into OracleProcessor entry table method * Add shared id to preparedEntryStatement * Make Oracle insertIntoEntryTable iterative - pasted from master - not yet tested * Add fields to fields table in parallel * Reset test trace length * Fix checkStyle * Revert port setting Co-authored-by: Tobias Diez --- .../jabref/logic/shared/DBMSProcessor.java | 141 +++++++++++------- .../jabref/logic/shared/DBMSSynchronizer.java | 5 +- .../jabref/logic/shared/OracleProcessor.java | 59 ++++++-- .../logic/shared/PostgreSQLProcessor.java | 35 +++-- .../logic/shared/DBMSProcessorTest.java | 36 +++-- .../jabref/logic/shared/TestConnector.java | 2 +- 6 files changed, 182 insertions(+), 96 deletions(-) diff --git a/src/main/java/org/jabref/logic/shared/DBMSProcessor.java b/src/main/java/org/jabref/logic/shared/DBMSProcessor.java index 0e6d9550e5c..9ef40561391 100644 --- a/src/main/java/org/jabref/logic/shared/DBMSProcessor.java +++ b/src/main/java/org/jabref/logic/shared/DBMSProcessor.java @@ -16,12 +16,14 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import org.jabref.logic.shared.exception.OfflineLockException; import org.jabref.model.database.shared.DBMSType; import org.jabref.model.database.shared.DatabaseConnection; import org.jabref.model.database.shared.DatabaseConnectionProperties; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.SharedBibEntryData; import org.jabref.model.entry.event.EntriesEventSource; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.FieldFactory; @@ -131,40 +133,61 @@ public void setupSharedDatabase() throws SQLException { abstract String escape(String expression); /** - * Inserts the given bibEntry into shared database. + * For use in test only. Inserts the BibEntry into the shared database. * - * @param bibEntry {@link BibEntry} to be inserted + * @param bibEntry {@link BibEntry} to be inserted. + * */ + public void insertEntry(BibEntry bibEntry) { + insertEntries(Collections.singletonList(bibEntry)); + } + + /** + * Inserts the List of BibEntry into the shared database. + * + * @param bibEntries List of {@link BibEntry} to be inserted */ - public void insertEntry(BibEntry bibEntry) { - if (!checkForBibEntryExistence(bibEntry)) { - insertIntoEntryTable(bibEntry); - insertIntoFieldTable(bibEntry); + public void insertEntries(List bibEntries) { + List notYetExistingEntries = getNotYetExistingEntries(bibEntries); + if (notYetExistingEntries.isEmpty()) { + return; } + insertIntoEntryTable(notYetExistingEntries); + insertIntoFieldTable(notYetExistingEntries); } /** - * Inserts the given bibEntry into ENTRY table. + * Inserts the given List of BibEntry into the ENTRY table. * - * @param bibEntry {@link BibEntry} to be inserted + * @param bibEntries List of {@link BibEntry} to be inserted */ - protected void insertIntoEntryTable(BibEntry bibEntry) { - // This is the only method to get generated keys which is accepted by MySQL, PostgreSQL and Oracle. - String insertIntoEntryQuery = - "INSERT INTO " + - escape("ENTRY") + - "(" + - escape("TYPE") + - ") VALUES(?)"; - - try (PreparedStatement preparedEntryStatement = connection.prepareStatement(insertIntoEntryQuery, - new String[]{"SHARED_ID"})) { + protected void insertIntoEntryTable(List bibEntries) { + StringBuilder insertIntoEntryQuery = new StringBuilder() + .append("INSERT INTO ") + .append(escape("ENTRY")) + .append("(") + .append(escape("TYPE")) + .append(") VALUES(?)"); + // Number of commas is bibEntries.size() - 1 + for (int i = 0; i < bibEntries.size() - 1; i++) { + insertIntoEntryQuery.append(", (?)"); + } - preparedEntryStatement.setString(1, bibEntry.getType().getName()); + try (PreparedStatement preparedEntryStatement = connection.prepareStatement(insertIntoEntryQuery.toString(), + new String[]{"SHARED_ID"})) { + for (int i = 0; i < bibEntries.size(); i++) { + preparedEntryStatement.setString(i + 1, bibEntries.get(i).getType().getName()); + } preparedEntryStatement.executeUpdate(); try (ResultSet generatedKeys = preparedEntryStatement.getGeneratedKeys()) { + // The following assumes that we get the generated keys in the order the entries were inserted + // This should be the case + for (BibEntry bibEntry : bibEntries) { + generatedKeys.next(); + bibEntry.getSharedBibEntryData().setSharedID(generatedKeys.getInt(1)); + } if (generatedKeys.next()) { - bibEntry.getSharedBibEntryData().setSharedID(generatedKeys.getInt(1)); // set generated ID locally + LOGGER.error("Error: Some shared IDs left unassigned"); } } } catch (SQLException e) { @@ -173,48 +196,52 @@ protected void insertIntoEntryTable(BibEntry bibEntry) { } /** - * Checks whether the given bibEntry already exists on shared database. + * Filters a list of BibEntry to and returns those which do not exist in the database * - * @param bibEntry {@link BibEntry} to be checked + * @param bibEntries {@link BibEntry} to be checked * @return true if existent, else false */ - private boolean checkForBibEntryExistence(BibEntry bibEntry) { + private List getNotYetExistingEntries(List bibEntries) { + + List remoteIds = new ArrayList<>(); + List localIds = bibEntries.stream() + .map(BibEntry::getSharedBibEntryData) + .map(SharedBibEntryData::getSharedID) + .filter((id) -> id != -1) + .collect(Collectors.toList()); + if (localIds.isEmpty()) { + return bibEntries; + } try { - // Check if already exists - int sharedID = bibEntry.getSharedBibEntryData().getSharedID(); - if (sharedID != -1) { - String selectQuery = - "SELECT * FROM " + - escape("ENTRY") + - " WHERE " + - escape("SHARED_ID") + - " = ?"; - - try (PreparedStatement preparedSelectStatement = connection.prepareStatement(selectQuery)) { - preparedSelectStatement.setInt(1, sharedID); - try (ResultSet resultSet = preparedSelectStatement.executeQuery()) { - if (resultSet.next()) { - return true; - } - } + StringBuilder selectQuery = new StringBuilder() + .append("SELECT * FROM ") + .append(escape("ENTRY")); + + try (ResultSet resultSet = connection.createStatement().executeQuery(selectQuery.toString())) { + while (resultSet.next()) { + int id = resultSet.getInt("SHARED_ID"); + remoteIds.add(id); } } } catch (SQLException e) { LOGGER.error("SQL Error: ", e); } - return false; - } + return bibEntries.stream().filter((entry) -> + !remoteIds.contains(entry.getSharedBibEntryData().getSharedID())) + .collect(Collectors.toList()); + } /** - * Inserts the given bibEntry into FIELD table. + * Inserts the given list of BibEntry into FIELD table. * - * @param bibEntry {@link BibEntry} to be inserted + * @param bibEntries {@link BibEntry} to be inserted */ - protected void insertIntoFieldTable(BibEntry bibEntry) { + protected void insertIntoFieldTable(List bibEntries) { try { // Inserting into FIELD table // Coerce to ArrayList in order to use List.get() - List fields = new ArrayList<>(bibEntry.getFields()); + List> fields = bibEntries.stream().map(bibEntry -> new ArrayList<>(bibEntry.getFields())) + .collect(Collectors.toList()); StringBuilder insertFieldQuery = new StringBuilder() .append("INSERT INTO ") .append(escape("FIELD")) @@ -225,16 +252,24 @@ protected void insertIntoFieldTable(BibEntry bibEntry) { .append(", ") .append(escape("VALUE")) .append(") VALUES(?, ?, ?)"); + int numFields = 0; + for (List entryFields : fields) { + numFields += entryFields.size(); + } // Number of commas is fields.size() - 1 - for (int i = 0; i < fields.size() - 1; i++) { + for (int i = 0; i < numFields - 1; i++) { insertFieldQuery.append(", (?, ?, ?)"); } try (PreparedStatement preparedFieldStatement = connection.prepareStatement(insertFieldQuery.toString())) { - for (int i = 0; i < fields.size(); i++) { - // columnIndex starts with 1 - preparedFieldStatement.setInt((3 * i) + 1, bibEntry.getSharedBibEntryData().getSharedID()); - preparedFieldStatement.setString((3 * i) + 2, fields.get(i).getName()); - preparedFieldStatement.setString((3 * i) + 3, bibEntry.getField(fields.get(i)).get()); + int fieldsCompleted = 0; + for (int entryIndex = 0; entryIndex < fields.size(); entryIndex++) { + for (int entryFieldsIndex = 0; entryFieldsIndex < fields.get(entryIndex).size(); entryFieldsIndex++) { + // columnIndex starts with 1 + preparedFieldStatement.setInt((3 * fieldsCompleted) + 1, bibEntries.get(entryIndex).getSharedBibEntryData().getSharedID()); + preparedFieldStatement.setString((3 * fieldsCompleted) + 2, fields.get(entryIndex).get(entryFieldsIndex).getName()); + preparedFieldStatement.setString((3 * fieldsCompleted) + 3, bibEntries.get(entryIndex).getField(fields.get(entryIndex).get(entryFieldsIndex)).get()); + fieldsCompleted += 1; + } } preparedFieldStatement.executeUpdate(); } diff --git a/src/main/java/org/jabref/logic/shared/DBMSSynchronizer.java b/src/main/java/org/jabref/logic/shared/DBMSSynchronizer.java index 72b9fc069f2..6499d78a179 100644 --- a/src/main/java/org/jabref/logic/shared/DBMSSynchronizer.java +++ b/src/main/java/org/jabref/logic/shared/DBMSSynchronizer.java @@ -82,12 +82,9 @@ public void listen(EntriesAddedEvent event) { if (isEventSourceAccepted(event) && checkCurrentConnection()) { synchronizeLocalMetaData(); synchronizeLocalDatabase(); // Pull changes for the case that there were some - List entries = event.getBibEntries(); - for (BibEntry entry : entries) { - dbmsProcessor.insertEntry(entry); + dbmsProcessor.insertEntries(event.getBibEntries()); } } - } /** * Listening method. Updates an existing shared {@link BibEntry}. diff --git a/src/main/java/org/jabref/logic/shared/OracleProcessor.java b/src/main/java/org/jabref/logic/shared/OracleProcessor.java index 6e4b820bcf4..f4fd183a1d3 100644 --- a/src/main/java/org/jabref/logic/shared/OracleProcessor.java +++ b/src/main/java/org/jabref/logic/shared/OracleProcessor.java @@ -1,11 +1,13 @@ package org.jabref.logic.shared; import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import java.util.Properties; +import java.util.stream.Collectors; import org.jabref.logic.shared.listener.OracleNotificationListener; import org.jabref.model.database.shared.DatabaseConnection; @@ -103,14 +105,48 @@ public void startNotificationListener(DBMSSynchronizer dbmsSynchronizer) { } @Override - protected void insertIntoFieldTable(BibEntry bibEntry) { + protected void insertIntoEntryTable(List entries) { + try { + for (BibEntry entry : entries) { + String insertIntoEntryQuery = + "INSERT INTO " + + escape("ENTRY") + + "(" + + escape("TYPE") + + ") VALUES(?)"; + + try (PreparedStatement preparedEntryStatement = connection.prepareStatement(insertIntoEntryQuery, + new String[]{"SHARED_ID"})) { + + preparedEntryStatement.setString(1, entry.getType().getName()); + preparedEntryStatement.executeUpdate(); + + try (ResultSet generatedKeys = preparedEntryStatement.getGeneratedKeys()) { + if (generatedKeys.next()) { + entry.getSharedBibEntryData().setSharedID(generatedKeys.getInt(1)); // set generated ID locally + } + } + } + } + } catch (SQLException e) { + LOGGER.error("SQL Error: ", e); + } + } + + @Override + protected void insertIntoFieldTable(List bibEntries) { try { // Inserting into FIELD table // Coerce to ArrayList in order to use List.get() - List fields = new ArrayList<>(bibEntry.getFields()); + List> fields = bibEntries.stream().map(entry -> new ArrayList<>(entry.getFields())) + .collect(Collectors.toList()); StringBuilder insertFieldQuery = new StringBuilder() .append("INSERT ALL"); - for (Field field : fields) { + int numFields = 0; + for (List entryFields : fields) { + numFields += entryFields.size(); + } + for (int i = 0; i < numFields; i++) { insertFieldQuery.append(" INTO ") .append(escape("FIELD")) .append(" (") @@ -123,14 +159,17 @@ protected void insertIntoFieldTable(BibEntry bibEntry) { } insertFieldQuery.append(" SELECT * FROM DUAL"); try (PreparedStatement preparedFieldStatement = connection.prepareStatement(insertFieldQuery.toString())) { - for (int i = 0; i < fields.size(); i++) { - // columnIndex starts with 1 - preparedFieldStatement.setInt((3 * i) + 1, bibEntry.getSharedBibEntryData().getSharedID()); - preparedFieldStatement.setString((3 * i) + 2, fields.get(i).getName()); - preparedFieldStatement.setString((3 * i) + 3, bibEntry.getField(fields.get(i)).get()); + int fieldsCompleted = 0; + for (int entryIndex = 0; entryIndex < fields.size(); entryIndex++) { + for (int entryFieldsIndex = 0; entryFieldsIndex < fields.get(entryIndex).size(); entryFieldsIndex++) { + // columnIndex starts with 1 + preparedFieldStatement.setInt((3 * fieldsCompleted) + 1, bibEntries.get(entryIndex).getSharedBibEntryData().getSharedID()); + preparedFieldStatement.setString((3 * fieldsCompleted) + 2, fields.get(entryIndex).get(entryFieldsIndex).getName()); + preparedFieldStatement.setString((3 * fieldsCompleted) + 3, bibEntries.get(entryIndex).getField(fields.get(entryIndex).get(entryFieldsIndex)).get()); + fieldsCompleted += 1; + } } - preparedFieldStatement.executeUpdate(); - } + preparedFieldStatement.executeUpdate(); } } catch (SQLException e) { LOGGER.error("SQL Error: ", e); } diff --git a/src/main/java/org/jabref/logic/shared/PostgreSQLProcessor.java b/src/main/java/org/jabref/logic/shared/PostgreSQLProcessor.java index 26cb1adfd01..ad53d54fd3b 100644 --- a/src/main/java/org/jabref/logic/shared/PostgreSQLProcessor.java +++ b/src/main/java/org/jabref/logic/shared/PostgreSQLProcessor.java @@ -4,6 +4,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.util.List; import org.jabref.JabRefExecutorService; import org.jabref.logic.shared.listener.PostgresSQLNotificationListener; @@ -49,25 +50,33 @@ public void setUp() throws SQLException { } @Override - protected void insertIntoEntryTable(BibEntry bibEntry) { - // Inserting into ENTRY table + protected void insertIntoEntryTable(List bibEntries) { 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. + .append("INSERT INTO ") + .append(escape("ENTRY")) + .append("(") + .append(escape("TYPE")) + .append(") VALUES(?)"); + // Number of commas is bibEntries.size() - 1 + for (int i = 0; i < bibEntries.size() - 1; i++) { + insertIntoEntryQuery.append(", (?)"); + } try (PreparedStatement preparedEntryStatement = connection.prepareStatement(insertIntoEntryQuery.toString(), - Statement.RETURN_GENERATED_KEYS)) { - - preparedEntryStatement.setString(1, bibEntry.getType().getName()); + Statement.RETURN_GENERATED_KEYS)) { + for (int i = 0; i < bibEntries.size(); i++) { + preparedEntryStatement.setString(i + 1, bibEntries.get(i).getType().getName()); + } preparedEntryStatement.executeUpdate(); try (ResultSet generatedKeys = preparedEntryStatement.getGeneratedKeys()) { + // The following assumes that we get the generated keys in the order the entries were inserted + // This should be the case + for (BibEntry bibEntry : bibEntries) { + generatedKeys.next(); + bibEntry.getSharedBibEntryData().setSharedID(generatedKeys.getInt(1)); + } if (generatedKeys.next()) { - bibEntry.getSharedBibEntryData().setSharedID(generatedKeys.getInt(1)); // set generated ID locally + LOGGER.error("Error: Some shared IDs left unassigned"); } } } catch (SQLException e) { diff --git a/src/test/java/org/jabref/logic/shared/DBMSProcessorTest.java b/src/test/java/org/jabref/logic/shared/DBMSProcessorTest.java index 754e601e0b2..fe7c59b4263 100644 --- a/src/test/java/org/jabref/logic/shared/DBMSProcessorTest.java +++ b/src/test/java/org/jabref/logic/shared/DBMSProcessorTest.java @@ -2,6 +2,7 @@ import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -70,7 +71,7 @@ void testInsertEntry() throws SQLException { dbmsProcessor.insertEntry(expectedEntry); - BibEntry emptyEntry = new BibEntry(); + BibEntry emptyEntry = getBibEntryExample(); emptyEntry.getSharedBibEntryData().setSharedID(1); dbmsProcessor.insertEntry(emptyEntry); // does not insert, due to same sharedID. @@ -97,31 +98,37 @@ void testInsertEntry() throws SQLException { @Test void testInsertMultipleEntries() throws SQLException { - BibEntry firstEntry = getBibEntryExample(); - String firstId = firstEntry.getId(); - BibEntry secondEntry = getBibEntryExample2(); - String secondId = secondEntry.getId(); - BibEntry thirdEntry = getBibEntryExample3(); - String thirdId = thirdEntry.getId(); - - // This must eventually be changed to insertEntries() once that method is implemented - dbmsProcessor.insertEntry(firstEntry); - dbmsProcessor.insertEntry(secondEntry); - dbmsProcessor.insertEntry(thirdEntry); + List entries = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + entries.add(new BibEntry(StandardEntryType.Article).withField(StandardField.JOURNAL, "journal " + i) + .withField(StandardField.ISSUE, Integer.toString(i))); + } + entries.get(3).setType(StandardEntryType.Thesis); + dbmsProcessor.insertEntries(entries); Map> actualFieldMap = new HashMap<>(); try (ResultSet entryResultSet = selectFrom("ENTRY", dbmsConnection, dbmsProcessor)) { assertTrue(entryResultSet.next()); assertEquals(1, entryResultSet.getInt("SHARED_ID")); - assertEquals("inproceedings", entryResultSet.getString("TYPE")); + assertEquals("article", entryResultSet.getString("TYPE")); assertEquals(1, entryResultSet.getInt("VERSION")); assertTrue(entryResultSet.next()); assertEquals(2, entryResultSet.getInt("SHARED_ID")); - assertEquals("inproceedings", entryResultSet.getString("TYPE")); + assertEquals("article", entryResultSet.getString("TYPE")); assertEquals(1, entryResultSet.getInt("VERSION")); assertTrue(entryResultSet.next()); assertEquals(3, entryResultSet.getInt("SHARED_ID")); + assertEquals("article", entryResultSet.getString("TYPE")); + assertEquals(1, entryResultSet.getInt("VERSION")); + assertTrue(entryResultSet.next()); + assertEquals(4, entryResultSet.getInt("SHARED_ID")); + assertEquals("thesis", entryResultSet.getString("TYPE")); + assertEquals(1, entryResultSet.getInt("VERSION")); + assertTrue(entryResultSet.next()); + assertEquals(5, entryResultSet.getInt("SHARED_ID")); + assertEquals("article", entryResultSet.getString("TYPE")); + assertEquals(1, entryResultSet.getInt("VERSION")); assertFalse(entryResultSet.next()); try (ResultSet fieldResultSet = selectFrom("FIELD", dbmsConnection, dbmsProcessor)) { @@ -139,7 +146,6 @@ void testInsertMultipleEntries() throws SQLException { } } } - List entries = Arrays.asList(firstEntry, secondEntry, thirdEntry); Map> expectedFieldMap = entries.stream() .collect(Collectors.toMap(bibEntry -> bibEntry.getSharedBibEntryData().getSharedID(), (bibEntry) -> bibEntry.getFieldMap().entrySet().stream() diff --git a/src/test/java/org/jabref/logic/shared/TestConnector.java b/src/test/java/org/jabref/logic/shared/TestConnector.java index 73d7d9b9a8a..f48bb98e422 100644 --- a/src/test/java/org/jabref/logic/shared/TestConnector.java +++ b/src/test/java/org/jabref/logic/shared/TestConnector.java @@ -24,7 +24,7 @@ public static DBMSConnectionProperties getTestConnectionProperties(DBMSType dbms case POSTGRESQL: return new DBMSConnectionPropertiesBuilder().setType(dbmsType).setHost("localhost").setPort(dbmsType.getDefaultPort()).setDatabase("postgres").setUser("postgres").setPassword("postgres").setUseSSL(false).createDBMSConnectionProperties(); case ORACLE: - return new DBMSConnectionPropertiesBuilder().setType(dbmsType).setHost("localhost").setPort(dbmsType.getDefaultPort()).setDatabase("jabref").setUser("jabref").setPassword("jabref").setUseSSL(false).setPort(32118).createDBMSConnectionProperties(); + return new DBMSConnectionPropertiesBuilder().setType(dbmsType).setHost("localhost").setPort(32118).setDatabase("jabref").setUser("jabref").setPassword("jabref").setUseSSL(false).createDBMSConnectionProperties(); default: return new DBMSConnectionPropertiesBuilder().createDBMSConnectionProperties(); } From 5369b3b9f18234f2cb47578e3f3915fcb4dafcf7 Mon Sep 17 00:00:00 2001 From: Christoph Date: Thu, 20 Feb 2020 08:32:10 +0100 Subject: [PATCH 3/6] Try to fix linux pdf opening again (#5945) * Try to fix linux pdf opening again * execute stream gobbler in own task * add to other open method as well * use process builder and close reader --- .../java/org/jabref/gui/desktop/os/Linux.java | 37 ++++++++++--------- .../org/jabref/gui/util/StreamGobbler.java | 32 ++++++++++++++++ 2 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 src/main/java/org/jabref/gui/util/StreamGobbler.java diff --git a/src/main/java/org/jabref/gui/desktop/os/Linux.java b/src/main/java/org/jabref/gui/desktop/os/Linux.java index a39c6e335d2..579b4986774 100644 --- a/src/main/java/org/jabref/gui/desktop/os/Linux.java +++ b/src/main/java/org/jabref/gui/desktop/os/Linux.java @@ -11,8 +11,10 @@ import java.util.Optional; import java.util.StringJoiner; +import org.jabref.JabRefExecutorService; import org.jabref.gui.externalfiletype.ExternalFileType; import org.jabref.gui.externalfiletype.ExternalFileTypes; +import org.jabref.gui.util.StreamGobbler; import org.jabref.preferences.JabRefPreferences; import org.slf4j.Logger; @@ -22,6 +24,7 @@ import static org.jabref.preferences.JabRefPreferences.USE_PDF_READER; public class Linux implements NativeDesktop { + private static final Logger LOGGER = LoggerFactory.getLogger(Linux.class); @Override @@ -34,14 +37,13 @@ public void openFile(String filePath, String fileType) throws IOException { } else { viewer = "xdg-open"; } - String[] cmdArray = { viewer, filePath }; - Process p = Runtime.getRuntime().exec(cmdArray); - // When the stream is full at some point, then blocks the execution of the program - // See https://stackoverflow.com/questions/10981969/why-is-going-through-geterrorstream-necessary-to-run-a-process. - BufferedReader in = new BufferedReader(new InputStreamReader(p.getErrorStream())); - String line; - line = in.readLine(); - LOGGER.debug("Received output: " + line); + ProcessBuilder processBuilder = new ProcessBuilder(viewer, filePath); + Process process = processBuilder.start(); + StreamGobbler streamGobblerInput = new StreamGobbler(process.getInputStream(), LOGGER::debug); + StreamGobbler streamGobblerError = new StreamGobbler(process.getErrorStream(), LOGGER::debug); + + JabRefExecutorService.INSTANCE.execute(streamGobblerInput); + JabRefExecutorService.INSTANCE.execute(streamGobblerError); } @Override @@ -53,17 +55,18 @@ public void openFileWithApplication(String filePath, String application) throws } else { openWith = new String[] {"xdg-open"}; } - String[] cmdArray = new String[openWith.length + 1]; System.arraycopy(openWith, 0, cmdArray, 0, openWith.length); cmdArray[cmdArray.length - 1] = filePath; - Process p = Runtime.getRuntime().exec(cmdArray); - // When the stream is full at some point, then blocks the execution of the program - // See https://stackoverflow.com/questions/10981969/why-is-going-through-geterrorstream-necessary-to-run-a-process. - BufferedReader in = new BufferedReader(new InputStreamReader(p.getErrorStream())); - String line; - line = in.readLine(); - LOGGER.debug("Received output: " + line); + + ProcessBuilder processBuilder = new ProcessBuilder(cmdArray); + Process process = processBuilder.start(); + + StreamGobbler streamGobblerInput = new StreamGobbler(process.getInputStream(), LOGGER::debug); + StreamGobbler streamGobblerError = new StreamGobbler(process.getErrorStream(), LOGGER::debug); + + JabRefExecutorService.INSTANCE.execute(streamGobblerInput); + JabRefExecutorService.INSTANCE.execute(streamGobblerError); } @Override @@ -118,7 +121,7 @@ public void openPdfWithParameters(String filePath, List parameters) thro openFileWithApplication(filePath, sj.toString()); } else { - openFile( filePath, "PDF"); + openFile(filePath, "PDF"); } } diff --git a/src/main/java/org/jabref/gui/util/StreamGobbler.java b/src/main/java/org/jabref/gui/util/StreamGobbler.java new file mode 100644 index 00000000000..b2131968f87 --- /dev/null +++ b/src/main/java/org/jabref/gui/util/StreamGobbler.java @@ -0,0 +1,32 @@ +package org.jabref.gui.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class StreamGobbler implements Runnable { + + private static final Logger LOGGER = LoggerFactory.getLogger(StreamGobbler.class); + + private InputStream inputStream; + private Consumer consumer; + + public StreamGobbler(InputStream inputStream, Consumer consumer) { + this.inputStream = inputStream; + this.consumer = consumer; + } + + @Override + public void run() { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + reader.lines().forEach(consumer); + } catch (IOException e) { + LOGGER.error("Error when reading process stream from external application", e); + } + } +} From 505fc74f7bda3cefb83e76d1a523558941f3ea5d Mon Sep 17 00:00:00 2001 From: Tobias Diez Date: Thu, 20 Feb 2020 12:17:13 +0100 Subject: [PATCH 4/6] Remove background command line window (#5965) Fixes #5474 --- CHANGELOG.md | 1 + build.gradle | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae9e8816e5d..a004de40081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ We refer to [GitHub issues](https://github.com/JabRef/jabref/issues) by using `# ### Fixed +- We fixed an issue where the command line console was always opened in the background. [#5474](https://github.com/JabRef/jabref/issues/5474) - We fixed and issue where pdf files will not open under some KDE linux distributions when using okular. [#5253](https://github.com/JabRef/jabref/issues/5253) - We fixed an issue where the Medline fetcher was only working when JabRef was running from source. [#5645](https://github.com/JabRef/jabref/issues/5645) - We fixed some visual issues in the dark theme. [#5764](https://github.com/JabRef/jabref/pull/5764) [#5753](https://github.com/JabRef/jabref/issues/5753) diff --git a/build.gradle b/build.gradle index c91a97a42c7..bb116c4ae2c 100644 --- a/build.gradle +++ b/build.gradle @@ -255,7 +255,7 @@ dependencyUpdates.resolutionStrategy = { } rules.withModule("de.jensd:fontawesomefx-materialdesignfont") { ComponentSelection selection -> if (selection.candidate.version ==~ /2.0.26-9.1.2/ - || selection.candidate.version ==~ /2.0.26-9.1.1/ + || selection.candidate.version ==~ /2.0.26-9.1.1/ || selection.candidate.version ==~ /2.0.26-9.1.0/) { selection.reject('1.7.22-11 is actually newer (strange version system)') } @@ -590,7 +590,7 @@ jlink { requires 'java.rmi' requires 'java.xml' requires 'com.sun.xml.txw2' - requires 'com.google.gson'; + requires 'com.google.gson' requires 'java.desktop' requires 'java.security.jgss' requires 'jdk.jsobject' @@ -628,7 +628,6 @@ jlink { // This requires WiX to be installed: https://github.com/wixtoolset/wix3/releases installerType = "msi" imageOptions = [ - '--win-console', '--icon', "${projectDir}/src/main/resources/icons/jabref.ico", ] installerOptions = [ From e0e837e5ac97d9ca4cccd0a506ac1e1de0e8a575 Mon Sep 17 00:00:00 2001 From: Christoph Date: Thu, 20 Feb 2020 13:36:06 +0100 Subject: [PATCH 5/6] update jlink plugin and gradle to 6.2 (#5964) * update jlink plugin * update gradle * add gradle validation workflow - https://github.com/gradle/wrapper-validation-action Co-authored-by: Oliver Kopp --- .../workflows/gradle-wrapper-validation.yml | 14 ++++++++++++++ build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 58702 -> 58695 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/gradle-wrapper-validation.yml diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml new file mode 100644 index 00000000000..6cb3bb97290 --- /dev/null +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -0,0 +1,14 @@ +# This is https://github.com/marketplace/actions/gradle-wrapper-validation +# It ensures that the jar file is from gradle and not by a strange third party. + +name: "Validate Gradle Wrapper" +on: [push, pull_request] + +jobs: + validation: + name: "Validation" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: gradle/wrapper-validation-action@v1 + diff --git a/build.gradle b/build.gradle index bb116c4ae2c..e942fe2262b 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ plugins { id 'com.github.ben-manes.versions' version '0.27.0' id 'org.javamodularity.moduleplugin' version '1.5.0' id 'org.openjfx.javafxplugin' version '0.0.8' - id 'org.beryx.jlink' version '2.17.1' + id 'org.beryx.jlink' version '2.17.2' // nicer test outputs during running and completion id 'com.adarshr.test-logger' version '2.0.0' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index cc4fdc293d0e50b0ad9b65c16e7ddd1db2f6025b..f3d88b1c2faf2fc91d853cd5d4242b5547257070 100644 GIT binary patch delta 12333 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1b16c34a71c..b7c8c5dbf58 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 5fa1dcfe0430c933eea1257e4b0a3705f41304ec Mon Sep 17 00:00:00 2001 From: NikodemKch Date: Thu, 20 Feb 2020 13:45:42 +0100 Subject: [PATCH 6/6] =?UTF-8?q?Add=20option=20to=20parse=20new=20reference?= =?UTF-8?q?s=20from=20plain=20text=20using=20GROBID=E2=80=A6=20(#5614)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GROBID integration using new fetcher Add the possibility to extract references from plain text using the GROBID service. GROBID is called over a custom server. See also pull request #5614 Co-Authored-By: joeyzgraggen Co-Authored-By: guenesaydin Co-Authored-By: obsluk00 Co-Authored-By: nikodemkch --- CHANGELOG.md | 1 + src/main/java/org/jabref/gui/JabRefFrame.java | 3 +- .../jabref/gui/actions/StandardActions.java | 3 +- .../BibtexExtractorViewModel.java | 52 +++++++---- .../bibtexextractor/ExtractBibtexDialog.fxml | 10 ++- .../bibtexextractor/ExtractBibtexDialog.java | 32 ++++--- .../java/org/jabref/gui/icon/IconTheme.java | 3 +- .../org/jabref/gui/keyboard/KeyBinding.java | 2 +- .../jabref/logic/importer/WebFetchers.java | 2 + .../fetcher/GrobidCitationFetcher.java | 77 ++++++++++++++++ .../logic/importer/util/GrobidService.java | 57 ++++++++++++ src/main/resources/l10n/JabRef_en.properties | 14 +-- .../fetcher/GrobidCitationFetcherTest.java | 88 +++++++++++++++++++ .../importer/util/GrobidServiceTest.java | 54 ++++++++++++ 14 files changed, 357 insertions(+), 41 deletions(-) create mode 100644 src/main/java/org/jabref/logic/importer/fetcher/GrobidCitationFetcher.java create mode 100644 src/main/java/org/jabref/logic/importer/util/GrobidService.java create mode 100644 src/test/java/org/jabref/logic/importer/fetcher/GrobidCitationFetcherTest.java create mode 100644 src/test/java/org/jabref/logic/importer/util/GrobidServiceTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index a004de40081..88539afa5ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ We refer to [GitHub issues](https://github.com/JabRef/jabref/issues) by using `# ### Changed +- We reintroduced the possibility to extract references from plain text (using GROBID) [#5614](https://github.com/JabRef/jabref/pull/5614) - We changed the open office panel to show buttons in rows of three instead of going straight down to save space as the button expanded out to take up unnecessary horizontal space. [#5479](https://github.com/JabRef/jabref/issues/5479) - We cleaned up the group add/edit dialog. [#5826](https://github.com/JabRef/jabref/pull/5826) - We reintroduced the index column. [#5844](https://github.com/JabRef/jabref/pull/5844) diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index 8eef085787b..c1c68a51590 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -481,6 +481,7 @@ private Node createToolbar() { HBox rightSide = new HBox( factory.createIconButton(StandardActions.NEW_ARTICLE, new NewEntryAction(this, StandardEntryType.Article, dialogService, Globals.prefs, stateManager)), factory.createIconButton(StandardActions.NEW_ENTRY, new NewEntryAction(this, dialogService, Globals.prefs, stateManager)), + factory.createIconButton(StandardActions.NEW_ENTRY_FROM_PLAIN_TEXT, new ExtractBibtexAction(stateManager)), factory.createIconButton(StandardActions.DELETE_ENTRY, new OldDatabaseCommandWrapper(Actions.DELETE, this, stateManager)), new Separator(Orientation.VERTICAL), factory.createIconButton(StandardActions.UNDO, new OldDatabaseCommandWrapper(Actions.UNDO, this, stateManager)), @@ -729,6 +730,7 @@ private MenuBar createMenu() { //@formatter:off library.getItems().addAll( factory.createMenuItem(StandardActions.NEW_ENTRY, new NewEntryAction(this, dialogService, Globals.prefs, stateManager)), + factory.createMenuItem(StandardActions.NEW_ENTRY_FROM_PLAIN_TEXT, new ExtractBibtexAction(stateManager)), factory.createMenuItem(StandardActions.DELETE_ENTRY, new OldDatabaseCommandWrapper(Actions.DELETE, this, stateManager)), new SeparatorMenuItem(), @@ -768,7 +770,6 @@ private MenuBar createMenu() { factory.createMenuItem(StandardActions.FIND_UNLINKED_FILES, new FindUnlinkedFilesAction(this, stateManager)), factory.createMenuItem(StandardActions.WRITE_XMP, new OldDatabaseCommandWrapper(Actions.WRITE_XMP, this, stateManager)), factory.createMenuItem(StandardActions.COPY_LINKED_FILES, new CopyFilesAction(stateManager, this.getDialogService())), - factory.createMenuItem(StandardActions.EXTRACT_BIBTEX, new ExtractBibtexAction(stateManager)), new SeparatorMenuItem(), diff --git a/src/main/java/org/jabref/gui/actions/StandardActions.java b/src/main/java/org/jabref/gui/actions/StandardActions.java index 87f48f1b4a5..245c20e30d5 100644 --- a/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -120,7 +120,7 @@ public enum StandardActions implements Action { NEW_ENTRY(Localization.lang("New entry"), IconTheme.JabRefIcons.ADD_ENTRY, KeyBinding.NEW_ENTRY), NEW_ARTICLE(Localization.lang("New article"), IconTheme.JabRefIcons.ADD_ARTICLE), - NEW_ENTRY_FROM_PLAINTEX(Localization.lang("New entry from plain text"), KeyBinding.NEW_FROM_PLAIN_TEXT), + NEW_ENTRY_FROM_PLAIN_TEXT(Localization.lang("New entry from plain text"), IconTheme.JabRefIcons.NEW_ENTRY_FROM_PLAIN_TEXT, KeyBinding.NEW_ENTRY_FROM_PLAIN_TEXT), LIBRARY_PROPERTIES(Localization.lang("Library properties")), EDIT_PREAMBLE(Localization.lang("Edit preamble")), EDIT_STRINGS(Localization.lang("Edit string constants"), IconTheme.JabRefIcons.EDIT_STRINGS, KeyBinding.EDIT_STRINGS), @@ -138,7 +138,6 @@ public enum StandardActions implements Action { DOWNLOAD_FULL_TEXT(Localization.lang("Search full text documents online"), IconTheme.JabRefIcons.FILE_SEARCH, KeyBinding.DOWNLOAD_FULL_TEXT), CLEANUP_ENTRIES(Localization.lang("Cleanup entries"), IconTheme.JabRefIcons.CLEANUP_ENTRIES, KeyBinding.CLEANUP), SET_FILE_LINKS(Localization.lang("Automatically set file links"), KeyBinding.AUTOMATICALLY_LINK_FILES), - EXTRACT_BIBTEX(Localization.lang("Extract BibTeX from plain text")), HELP(Localization.lang("Online help"), IconTheme.JabRefIcons.HELP, KeyBinding.HELP), HELP_KEY_PATTERNS(Localization.lang("Help on key patterns"), IconTheme.JabRefIcons.HELP, KeyBinding.HELP), diff --git a/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractorViewModel.java b/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractorViewModel.java index 8eb694bd692..c68ae79c3a8 100644 --- a/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractorViewModel.java +++ b/src/main/java/org/jabref/gui/bibtexextractor/BibtexExtractorViewModel.java @@ -3,40 +3,60 @@ import java.util.HashMap; import java.util.Map; +import javax.swing.undo.UndoManager; + import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import org.jabref.Globals; +import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; +import org.jabref.gui.externalfiles.ImportHandler; +import org.jabref.gui.externalfiletype.ExternalFileTypes; +import org.jabref.gui.util.BackgroundTask; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.importer.fetcher.GrobidCitationFetcher; +import org.jabref.logic.l10n.Localization; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.types.EntryType; -import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.model.util.FileUpdateMonitor; +import org.jabref.preferences.JabRefPreferences; public class BibtexExtractorViewModel { private final StringProperty inputTextProperty = new SimpleStringProperty(""); - private final BibDatabaseContext bibdatabaseContext; - - public BibtexExtractorViewModel(BibDatabaseContext bibdatabaseContext) { - this.bibdatabaseContext = bibdatabaseContext; + private DialogService dialogService; + private GrobidCitationFetcher currentCitationfetcher; + private TaskExecutor taskExecutor; + private ImportHandler importHandler; + + public BibtexExtractorViewModel(BibDatabaseContext bibdatabaseContext, DialogService dialogService, + JabRefPreferences jabRefPreferences, FileUpdateMonitor fileUpdateMonitor, TaskExecutor taskExecutor, UndoManager undoManager, StateManager stateManager) { + this.dialogService = dialogService; + currentCitationfetcher = new GrobidCitationFetcher(jabRefPreferences.getImportFormatPreferences()); + this.taskExecutor = taskExecutor; + this.importHandler = new ImportHandler(dialogService, bibdatabaseContext, ExternalFileTypes.getInstance(), jabRefPreferences.getFilePreferences(), jabRefPreferences.getImportFormatPreferences(), jabRefPreferences.getUpdateFieldPreferences(), fileUpdateMonitor, undoManager, stateManager); } public StringProperty inputTextProperty() { return this.inputTextProperty; } - public void startExtraction() { - - BibtexExtractor extractor = new BibtexExtractor(); - BibEntry entity = extractor.extract(inputTextProperty.getValue()); - this.bibdatabaseContext.getDatabase().insertEntry(entity); - trackNewEntry(StandardEntryType.Article); + public void startParsing() { + BackgroundTask.wrap(() -> currentCitationfetcher.performSearch(inputTextProperty.getValue())) + .onRunning(() -> dialogService.notify(Localization.lang("Your text is being parsed..."))) + .onSuccess(parsedEntries -> { + dialogService.notify(Localization.lang("%0 entries were parsed from your query.", String.valueOf(parsedEntries.size()))); + importHandler.importEntries(parsedEntries); + for (BibEntry bibEntry : parsedEntries) { + trackNewEntry(bibEntry); + } + }).executeWith(taskExecutor); } - private void trackNewEntry(EntryType type) { + private void trackNewEntry(BibEntry bibEntry) { Map properties = new HashMap<>(); - properties.put("EntryType", type.getName()); - - Globals.getTelemetryClient().ifPresent(client -> client.trackEvent("NewEntry", properties, new HashMap<>())); + properties.put("EntryType", bibEntry.typeProperty().getValue().getName()); + Globals.getTelemetryClient().ifPresent(client -> client.trackEvent("ParseWithGrobid", properties, new HashMap<>())); } } diff --git a/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.fxml b/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.fxml index 053024c2a6a..d2182cbaf9c 100644 --- a/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.fxml +++ b/src/main/java/org/jabref/gui/bibtexextractor/ExtractBibtexDialog.fxml @@ -4,11 +4,17 @@ + + -