package org.jabref.shared; import java.sql.Connection; import java.sql.SQLException; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import org.jabref.logic.exporter.BibDatabaseWriter; import org.jabref.logic.exporter.MetaDataSerializer; import org.jabref.logic.importer.ParseException; import org.jabref.logic.importer.util.MetaDataParser; import org.jabref.model.bibtexkeypattern.GlobalBibtexKeyPattern; import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.event.EntryAddedEvent; import org.jabref.model.database.event.EntryRemovedEvent; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.event.EntryEvent; import org.jabref.model.entry.event.EntryEventSource; import org.jabref.model.entry.event.FieldChangedEvent; import org.jabref.model.metadata.MetaData; import org.jabref.model.metadata.event.MetaDataChangedEvent; import org.jabref.shared.event.ConnectionLostEvent; import org.jabref.shared.event.SharedEntryNotPresentEvent; import org.jabref.shared.event.UpdateRefusedEvent; import org.jabref.shared.exception.DatabaseNotSupportedException; import org.jabref.shared.exception.InvalidDBMSConnectionPropertiesException; import org.jabref.shared.exception.OfflineLockException; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Synchronizes the shared or local databases with their opposite side. * Local changes are pushed by {@link EntryEvent} using Google's Guava EventBus. */ public class DBMSSynchronizer { private static final Log LOGGER = LogFactory.getLog(DBMSSynchronizer.class); private DBMSProcessor dbmsProcessor; private DBMSType dbmsType; private String dbName; private final BibDatabaseContext bibDatabaseContext; private MetaData metaData; private final BibDatabase bibDatabase; private final EventBus eventBus; private Connection currentConnection; private final Character keywordSeparator; private GlobalBibtexKeyPattern globalCiteKeyPattern; public DBMSSynchronizer(BibDatabaseContext bibDatabaseContext, Character keywordSeparator, GlobalBibtexKeyPattern globalCiteKeyPattern) { this.bibDatabaseContext = Objects.requireNonNull(bibDatabaseContext); this.bibDatabase = bibDatabaseContext.getDatabase(); this.metaData = bibDatabaseContext.getMetaData(); this.eventBus = new EventBus(); this.keywordSeparator = keywordSeparator; this.globalCiteKeyPattern = Objects.requireNonNull(globalCiteKeyPattern); } /** * Listening method. Inserts a new {@link BibEntry} into shared database. * * @param event {@link EntryAddedEvent} object */ @Subscribe public void listen(EntryAddedEvent event) { // While synchronizing the local database (see synchronizeLocalDatabase() below), some EntryEvents may be posted. // In this case DBSynchronizer should not try to insert the bibEntry entry again (but it would not harm). if (isEventSourceAccepted(event) && checkCurrentConnection()) { dbmsProcessor.insertEntry(event.getBibEntry()); synchronizeLocalMetaData(); synchronizeLocalDatabase(); // Pull changes for the case that there were some } } /** * Listening method. Updates an existing shared {@link BibEntry}. * * @param event {@link FieldChangedEvent} object */ @Subscribe public void listen(FieldChangedEvent event) { // While synchronizing the local database (see synchronizeLocalDatabase() below), some EntryEvents may be posted. // In this case DBSynchronizer should not try to update the bibEntry entry again (but it would not harm). if (isPresentLocalBibEntry(event.getBibEntry()) && isEventSourceAccepted(event) && checkCurrentConnection()) { synchronizeLocalMetaData(); BibEntry bibEntry = event.getBibEntry(); synchronizeSharedEntry(bibEntry); synchronizeLocalDatabase(); // Pull changes for the case that there were some } } /** * Listening method. Deletes the given {@link BibEntry} from shared database. * * @param event {@link EntryRemovedEvent} object */ @Subscribe public void listen(EntryRemovedEvent event) { // While synchronizing the local database (see synchronizeLocalDatabase() below), some EntryEvents may be posted. // In this case DBSynchronizer should not try to delete the bibEntry entry again (but it would not harm). if (isEventSourceAccepted(event) && checkCurrentConnection()) { dbmsProcessor.removeEntry(event.getBibEntry()); synchronizeLocalMetaData(); synchronizeLocalDatabase(); // Pull changes for the case that there where some } } /** * Listening method. Synchronizes the shared {@link MetaData} and applies them locally. * * @param event */ @Subscribe public void listen(MetaDataChangedEvent event) { if (checkCurrentConnection()) { synchronizeSharedMetaData(event.getMetaData(), globalCiteKeyPattern); synchronizeLocalDatabase(); applyMetaData(); dbmsProcessor.notifyClients(); } } @Subscribe public void listen(EntryEvent event) { if (isEventSourceAccepted(event)) { dbmsProcessor.notifyClients(); } } /** * Sets the table structure of shared database if needed and pulls all shared entries * to the new local database. * * @throws DatabaseNotSupportedException if the version of shared database does not match * the version of current shared database support ({@link DBMSProcessor}). */ public void initializeDatabases() throws DatabaseNotSupportedException, SQLException { if (!dbmsProcessor.checkBaseIntegrity()) { LOGGER.info("Integrity check failed. Fixing..."); dbmsProcessor.setupSharedDatabase(); // This check should only be performed once on initial database setup. // Calling dbmsProcessor.setupSharedDatabase() lets dbmsProcessor.checkBaseIntegrity() be true. if (dbmsProcessor.checkForPre3Dot6Intergrity()) { throw new DatabaseNotSupportedException(); } } dbmsProcessor.startNotificationListener(this); synchronizeLocalMetaData(); synchronizeLocalDatabase(); } /** * Synchronizes the local database with shared one. * Possible update types are removal, update or insert of a {@link BibEntry}. */ public void synchronizeLocalDatabase() { if (!checkCurrentConnection()) { return; } List<BibEntry> localEntries = bibDatabase.getEntries(); Map<Integer, Integer> idVersionMap = dbmsProcessor.getSharedIDVersionMapping(); // remove old entries locally removeNotSharedEntries(localEntries, idVersionMap.keySet()); // compare versions and update local entry if needed for (Map.Entry<Integer, Integer> idVersionEntry : idVersionMap.entrySet()) { boolean match = false; for (BibEntry localEntry : localEntries) { if (idVersionEntry.getKey() == localEntry.getSharedBibEntryData().getSharedID()) { match = true; if (idVersionEntry.getValue() > localEntry.getSharedBibEntryData().getVersion()) { Optional<BibEntry> sharedEntry = dbmsProcessor.getSharedEntry(idVersionEntry.getKey()); if (sharedEntry.isPresent()) { // update fields localEntry.setType(sharedEntry.get().getType(), EntryEventSource.SHARED); localEntry.getSharedBibEntryData() .setVersion(sharedEntry.get().getSharedBibEntryData().getVersion()); for (String field : sharedEntry.get().getFieldNames()) { localEntry.setField(field, sharedEntry.get().getField(field), EntryEventSource.SHARED); } Set<String> redundantLocalEntryFields = localEntry.getFieldNames(); redundantLocalEntryFields.removeAll(sharedEntry.get().getFieldNames()); // remove not existing fields for (String redundantField : redundantLocalEntryFields) { localEntry.clearField(redundantField, EntryEventSource.SHARED); } } } } } if (!match) { Optional<BibEntry> bibEntry = dbmsProcessor.getSharedEntry(idVersionEntry.getKey()); if (bibEntry.isPresent()) { bibDatabase.insertEntry(bibEntry.get(), EntryEventSource.SHARED); } } } } /** * Removes all local entries which are not present on shared database. * * @param localEntries List of {@link BibEntry} the entries should be removed from * @param sharedIDs Set of all IDs which are present on shared database */ private void removeNotSharedEntries(List<BibEntry> localEntries, Set<Integer> sharedIDs) { for (int i = 0; i < localEntries.size(); i++) { BibEntry localEntry = localEntries.get(i); boolean match = false; for (int sharedID : sharedIDs) { if (localEntry.getSharedBibEntryData().getSharedID() == sharedID) { match = true; break; } } if (!match) { eventBus.post(new SharedEntryNotPresentEvent(localEntry)); bibDatabase.removeEntry(localEntry, EntryEventSource.SHARED); // Should not reach the listeners above. i--; // due to index shift on localEntries } } } /** * Synchronizes the shared {@link BibEntry} with the local one. */ public void synchronizeSharedEntry(BibEntry bibEntry) { if (!checkCurrentConnection()) { return; } try { BibDatabaseWriter.applySaveActions(bibEntry, metaData); // perform possibly existing save actions dbmsProcessor.updateEntry(bibEntry); } catch (OfflineLockException exception) { eventBus.post(new UpdateRefusedEvent(bibDatabaseContext, exception.getLocalBibEntry(), exception.getSharedBibEntry())); } catch (SQLException e) { LOGGER.error("SQL Error: ", e); } } /** * Synchronizes all meta data locally. */ public void synchronizeLocalMetaData() { if (!checkCurrentConnection()) { return; } try { MetaDataParser.parse(metaData, dbmsProcessor.getSharedMetaData(), keywordSeparator); } catch (ParseException e) { LOGGER.error("Parse error", e); } } /** * Synchronizes all shared meta data. */ private void synchronizeSharedMetaData(MetaData data, GlobalBibtexKeyPattern globalCiteKeyPattern) { if (!checkCurrentConnection()) { return; } try { dbmsProcessor.setSharedMetaData(MetaDataSerializer.getSerializedStringMap(data, globalCiteKeyPattern)); } catch (SQLException e) { LOGGER.error("SQL Error: ", e); } } /** * Applies the {@link MetaData} on all local and shared BibEntries. */ public void applyMetaData() { if (!checkCurrentConnection()) { return; } for (BibEntry bibEntry : bibDatabase.getEntries()) { // synchronize only if changes were present if (!BibDatabaseWriter.applySaveActions(bibEntry, metaData).isEmpty()) { try { dbmsProcessor.updateEntry(bibEntry); } catch (OfflineLockException exception) { eventBus.post(new UpdateRefusedEvent(bibDatabaseContext, exception.getLocalBibEntry(), exception.getSharedBibEntry())); } catch (SQLException e) { LOGGER.error("SQL Error: ", e); } } } } /** * Synchronizes the local BibEntries and applies the fetched MetaData on them. */ public void pullChanges() { if (!checkCurrentConnection()) { return; } synchronizeLocalDatabase(); synchronizeLocalMetaData(); } /** * Checks whether the current SQL connection is valid. * In case that the connection is not valid a new {@link ConnectionLostEvent} is going to be sent. * * @return <code>true</code> if the connection is valid, else <code>false</code>. */ public boolean checkCurrentConnection() { try { boolean isValid = currentConnection.isValid(0); if (!isValid) { eventBus.post(new ConnectionLostEvent(bibDatabaseContext)); } return isValid; } catch (SQLException e) { LOGGER.error("SQL Error:", e); return false; } } /** * Checks whether the {@link EntryEventSource} of an {@link EntryEvent} is crucial for this class. * * @param event An {@link EntryEvent} * @return <code>true</code> if the event is able to trigger operations in {@link DBMSSynchronizer}, else <code>false</code> */ public boolean isEventSourceAccepted(EntryEvent event) { EntryEventSource eventSource = event.getEntryEventSource(); return ((eventSource == EntryEventSource.LOCAL) || (eventSource == EntryEventSource.UNDO)); } public void openSharedDatabase(DBMSConnection connection) throws DatabaseNotSupportedException, SQLException { this.dbmsType = connection.getProperties().getType(); this.dbName = connection.getProperties().getDatabase(); this.currentConnection = connection.getConnection(); this.dbmsProcessor = DBMSProcessor.getProcessorInstance(connection); initializeDatabases(); } public void openSharedDatabase(DBMSConnectionProperties properties) throws SQLException, DatabaseNotSupportedException, InvalidDBMSConnectionPropertiesException { openSharedDatabase(new DBMSConnection(properties)); } public void closeSharedDatabase() { try { dbmsProcessor.stopNotificationListener(); currentConnection.close(); } catch (SQLException e) { LOGGER.error("SQL Error:", e); } } private boolean isPresentLocalBibEntry(BibEntry bibEntry) { return bibDatabase.getEntries().contains(bibEntry); } public String getDBName() { return dbName; } public DBMSType getDBType() { return this.dbmsType; } public DBMSProcessor getDBProcessor() { return dbmsProcessor; } public void setMetaData(MetaData metaData) { this.metaData = metaData; } public void registerListener(Object listener) { eventBus.register(listener); } }