Skip to content

Commit

Permalink
Enable Merging of BibDatabases (#6689)
Browse files Browse the repository at this point in the history
* Clean up commit and remove unwanted changes.

Signed-off-by: Dominik Voigt <dominik.ingo.voigt@gmail.com>

* Move Merger and DuplicateCheck into database package

Signed-off-by: Dominik Voigt <dominik.ingo.voigt@gmail.com>

* Move meta data merging into DatabaseMerger
Add DatabaseContext merging capability to DatabaseMerger

Signed-off-by: Dominik Voigt <dominik.ingo.voigt@gmail.com>

* Add one meta data merge test (unfinished)

Signed-off-by: Dominik Voigt <dominik.ingo.voigt@gmail.com>

* Add meta data merging tests.

Signed-off-by: Dominik Voigt <dominik.ingo.voigt@gmail.com>

* Reduce test example

Signed-off-by: Dominik Voigt <dominik.ingo.voigt@gmail.com>
  • Loading branch information
DominikVoigt authored Sep 1, 2020
1 parent 5ab494e commit 35f5078
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 121 deletions.
2 changes: 1 addition & 1 deletion src/main/java/org/jabref/gui/EntryTypeViewModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

import org.jabref.Globals;
import org.jabref.gui.duplicationFinder.DuplicateResolverDialog;
import org.jabref.logic.bibtex.DuplicateCheck;
import org.jabref.logic.citationkeypattern.CitationKeyGenerator;
import org.jabref.logic.database.DuplicateCheck;
import org.jabref.logic.importer.FetcherException;
import org.jabref.logic.importer.IdBasedFetcher;
import org.jabref.logic.importer.WebFetchers;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import org.jabref.gui.undo.UndoableRemoveEntries;
import org.jabref.gui.util.BackgroundTask;
import org.jabref.gui.util.DefaultTaskExecutor;
import org.jabref.logic.bibtex.DuplicateCheck;
import org.jabref.logic.database.DuplicateCheck;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.database.BibDatabaseMode;
Expand Down
59 changes: 7 additions & 52 deletions src/main/java/org/jabref/gui/importer/ImportAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,14 @@
import org.jabref.gui.util.BackgroundTask;
import org.jabref.gui.util.DefaultTaskExecutor;
import org.jabref.gui.util.TaskExecutor;
import org.jabref.logic.database.DatabaseMerger;
import org.jabref.logic.importer.ImportException;
import org.jabref.logic.importer.ImportFormatReader;
import org.jabref.logic.importer.Importer;
import org.jabref.logic.importer.ParserResult;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.UpdateField;
import org.jabref.model.database.BibDatabase;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.BibtexString;
import org.jabref.model.groups.AllEntriesGroup;
import org.jabref.model.groups.ExplicitGroup;
import org.jabref.model.groups.GroupHierarchyType;
import org.jabref.model.metadata.ContentSelector;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -135,55 +130,15 @@ private ParserResult mergeImportResults(List<ImportFormatReader.UnknownFormatImp
continue;
}
ParserResult parserResult = importResult.parserResult;
List<BibEntry> entries = parserResult.getDatabase().getEntries();
resultDatabase.insertEntries(entries);
resultDatabase.insertEntries(parserResult.getDatabase().getEntries());

if (ImportFormatReader.BIBTEX_FORMAT.equals(importResult.format)) {
// additional treatment of BibTeX
// merge into existing database

// Merge strings
for (BibtexString bibtexString : parserResult.getDatabase().getStringValues()) {
String bibtexStringName = bibtexString.getName();
if (resultDatabase.hasStringByName(bibtexStringName)) {
String importedContent = bibtexString.getContent();
String existingContent = resultDatabase.getStringByName(bibtexStringName).get().getContent();
if (!importedContent.equals(existingContent)) {
LOGGER.warn("String contents differ for {}: {} != {}", bibtexStringName, importedContent, existingContent);
// TODO: decide what to do here (in case the same string exits)
}
} else {
resultDatabase.addString(bibtexString);
}
}

// Merge groups
// Adds the specified node as a child of the current root. The group contained in <b>newGroups </b> must not be of
// type AllEntriesGroup, since every tree has exactly one AllEntriesGroup (its root). The <b>newGroups </b> are
// inserted directly, i.e. they are not deepCopy()'d.
parserResult.getMetaData().getGroups().ifPresent(newGroups -> {
// ensure that there is always only one AllEntriesGroup in the resulting database
// "Rename" the AllEntriesGroup of the imported database to "Imported"
if (newGroups.getGroup() instanceof AllEntriesGroup) {
// create a dummy group
try {
// This will cause a bug if the group already exists
// There will be group where the two groups are merged
String newGroupName = importResult.parserResult.getFile().map(File::getName).orElse("unknown");
ExplicitGroup group = new ExplicitGroup("Imported " + newGroupName, GroupHierarchyType.INDEPENDENT,
Globals.prefs.getKeywordDelimiter());
newGroups.setGroup(group);
group.add(parserResult.getDatabase().getEntries());
} catch (IllegalArgumentException e) {
LOGGER.error("Problem appending entries to group", e);
}
}
result.getMetaData().getGroups().ifPresent(newGroups::moveTo);
});

for (ContentSelector selector : parserResult.getMetaData().getContentSelectorList()) {
result.getMetaData().addContentSelector(selector);
}
new DatabaseMerger().mergeMetaData(
result.getMetaData(),
parserResult.getMetaData(),
importResult.parserResult.getFile().map(File::getName).orElse("unknown"),
parserResult.getDatabase().getEntries());
}
// TODO: collect errors into ParserResult, because they are currently ignored (see caller of this method)
}
Expand Down
68 changes: 8 additions & 60 deletions src/main/java/org/jabref/gui/importer/ImportEntriesViewModel.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.jabref.gui.importer;

import java.io.File;
import java.util.List;
import java.util.Optional;

Expand All @@ -19,23 +20,16 @@
import org.jabref.gui.externalfiles.ImportHandler;
import org.jabref.gui.externalfiletype.ExternalFileTypes;
import org.jabref.gui.fieldeditors.LinkedFileViewModel;
import org.jabref.gui.groups.GroupTreeNodeViewModel;
import org.jabref.gui.groups.UndoableAddOrRemoveGroup;
import org.jabref.gui.undo.NamedCompound;
import org.jabref.gui.undo.UndoableInsertEntries;
import org.jabref.gui.undo.UndoableInsertString;
import org.jabref.gui.util.BackgroundTask;
import org.jabref.gui.util.TaskExecutor;
import org.jabref.logic.bibtex.DuplicateCheck;
import org.jabref.logic.database.DatabaseMerger;
import org.jabref.logic.database.DuplicateCheck;
import org.jabref.logic.importer.ParserResult;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.BibtexString;
import org.jabref.model.entry.LinkedFile;
import org.jabref.model.groups.GroupTreeNode;
import org.jabref.model.metadata.FilePreferences;
import org.jabref.model.metadata.MetaData;
import org.jabref.model.util.FileUpdateMonitor;
import org.jabref.preferences.PreferencesService;

Expand Down Expand Up @@ -152,58 +146,12 @@ public void importEntries(List<BibEntry> entriesToImport, boolean shouldDownload
}
}

NamedCompound namedCompound = new NamedCompound(Localization.lang("Import file"));
namedCompound.addEdit(new UndoableInsertEntries(databaseContext.getDatabase(), entriesToImport));
new DatabaseMerger().mergeStrings(databaseContext.getDatabase(), parserResult.getDatabase());
new DatabaseMerger().mergeMetaData(databaseContext.getMetaData(),
parserResult.getMetaData(),
parserResult.getFile().map(File::getName).orElse("unknown"),
parserResult.getDatabase().getEntries());

// merge strings into target database
for (BibtexString bibtexString : parserResult.getDatabase().getStringValues()) {
String bibtexStringName = bibtexString.getName();
if (databaseContext.getDatabase().hasStringByName(bibtexStringName)) {
String importedContent = bibtexString.getContent();
String existingContent = databaseContext.getDatabase().getStringByName(bibtexStringName).get().getContent();
if (!importedContent.equals(existingContent)) {
LOGGER.warn("String contents differ for {}: {} != {}", bibtexStringName, importedContent, existingContent);
// TODO: decide what to do here (in case the same string exits)
}
} else {
databaseContext.getDatabase().addString(bibtexString);
// FIXME: this prevents this method to be moved to logic - we need to implement a new undo/redo data model
namedCompound.addEdit(new UndoableInsertString(databaseContext.getDatabase(), bibtexString));
}
}

// copy content selectors to target database
MetaData targetMetada = databaseContext.getMetaData();
parserResult.getMetaData()
.getContentSelectorList()
.forEach(targetMetada::addContentSelector);
// TODO undo of content selectors (currently not implemented)

// copy groups to target database
parserResult.getMetaData().getGroups().ifPresent(
newGroupsTreeNode -> {
if (targetMetada.getGroups().isPresent()) {
GroupTreeNode groupTreeNode = targetMetada.getGroups().get();
newGroupsTreeNode.moveTo(groupTreeNode);
namedCompound.addEdit(
new UndoableAddOrRemoveGroup(
new GroupTreeNodeViewModel(groupTreeNode),
new GroupTreeNodeViewModel(newGroupsTreeNode),
UndoableAddOrRemoveGroup.ADD_NODE));
} else {
// target does not contain any groups, so we can just use the new groups
targetMetada.setGroups(newGroupsTreeNode);
namedCompound.addEdit(
new UndoableAddOrRemoveGroup(
new GroupTreeNodeViewModel(newGroupsTreeNode),
new GroupTreeNodeViewModel(newGroupsTreeNode),
UndoableAddOrRemoveGroup.ADD_NODE));
}
}
);

namedCompound.end();
Globals.undoManager.addEdit(namedCompound);
JabRefGUI.getMainFrame().getCurrentBasePanel().markBaseChanged();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import java.util.Optional;
import java.util.Set;

import org.jabref.logic.bibtex.DuplicateCheck;
import org.jabref.logic.database.DuplicateCheck;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.StandardField;
Expand Down
134 changes: 134 additions & 0 deletions src/main/java/org/jabref/logic/database/DatabaseMerger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package org.jabref.logic.database;

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import org.jabref.model.database.BibDatabase;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.database.BibDatabaseModeDetection;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.BibEntryTypesManager;
import org.jabref.model.entry.BibtexString;
import org.jabref.model.groups.AllEntriesGroup;
import org.jabref.model.groups.ExplicitGroup;
import org.jabref.model.groups.GroupHierarchyType;
import org.jabref.model.metadata.ContentSelector;
import org.jabref.model.metadata.MetaData;
import org.jabref.preferences.JabRefPreferences;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DatabaseMerger {

private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseMerger.class);

/**
* Merges all entries and strings of the other database into the target database. Any duplicates are ignored.
* In case a string has a different content, it is added with a new unique name.
* The unique name is generated by suffix "_i", where i runs from 1 onwards.
*
* @param other The other databases that is merged into this database
*/
public synchronized void merge(BibDatabase target, BibDatabase other) {
mergeEntries(target, other);
mergeStrings(target, other);
}

/**
* Merges all entries, strings, and metaData of the other database context into the target database context. Any duplicates are ignored.
* In case a string has a different content, it is added with a new unique name.
* The unique name is generated by suffix "_i", where i runs from 1 onwards.
*
* @param other The other databases that is merged into this database
*/
public synchronized void merge(BibDatabaseContext target, BibDatabaseContext other, String otherFileName) {
mergeEntries(target.getDatabase(), other.getDatabase());
mergeStrings(target.getDatabase(), other.getDatabase());
mergeMetaData(target.getMetaData(), other.getMetaData(), otherFileName, other.getEntries());
}

private void mergeEntries(BibDatabase target, BibDatabase other) {
DuplicateCheck duplicateCheck = new DuplicateCheck(new BibEntryTypesManager());
List<BibEntry> newEntries = other.getEntries().stream()
// Remove all entries that are already part of the database (duplicate)
.filter(entry -> duplicateCheck.containsDuplicate(target, entry, BibDatabaseModeDetection.inferMode(target)).isEmpty())
.collect(Collectors.toList());
target.insertEntries(newEntries);
}

public void mergeStrings(BibDatabase target, BibDatabase other) {
for (BibtexString bibtexString : other.getStringValues()) {
String bibtexStringName = bibtexString.getName();
if (target.hasStringByName(bibtexStringName)) {
String importedContent = bibtexString.getContent();
String existingContent = target.getStringByName(bibtexStringName).get().getContent();
if (!importedContent.equals(existingContent)) {
LOGGER.info("String contents differ for {}: {} != {}", bibtexStringName, importedContent, existingContent);
int suffix = 1;
String newName = bibtexStringName + "_" + suffix;
while (target.hasStringByName(newName)) {
suffix++;
newName = bibtexStringName + "_" + suffix;
}
BibtexString newBibtexString = new BibtexString(newName, importedContent);
// TODO undo/redo
target.addString(newBibtexString);
LOGGER.info("New string added: {} = {}", newBibtexString.getName(), newBibtexString.getContent());
}
} else {
// TODO undo/redo
target.addString(bibtexString);
}
}
}

/**
* @param target the metaData that is the merge target
* @param other the metaData to merge into the target
* @param otherFilename the filename of the other library. Pass "unknown" if not known.
*/
public void mergeMetaData(MetaData target, MetaData other, String otherFilename, List<BibEntry> allOtherEntries) {
Objects.requireNonNull(other);
Objects.requireNonNull(otherFilename);
Objects.requireNonNull(allOtherEntries);

mergeGroups(target, other, otherFilename, allOtherEntries);
mergeContentSelectors(target, other);
}

private void mergeGroups(MetaData target, MetaData other, String otherFilename, List<BibEntry> allOtherEntries) {
// Adds the specified node as a child of the current root. The group contained in <b>newGroups</b> must not be of
// type AllEntriesGroup, since every tree has exactly one AllEntriesGroup (its root). The <b>newGroups</b> are
// inserted directly, i.e. they are not deepCopy()'d.
other.getGroups().ifPresent(newGroups -> {
// ensure that there is always only one AllEntriesGroup in the resulting database
// "Rename" the AllEntriesGroup of the imported database to "Imported"
if (newGroups.getGroup() instanceof AllEntriesGroup) {
// create a dummy group
try {
// This will cause a bug if the group already exists
// There will be group where the two groups are merged
String newGroupName = otherFilename;
ExplicitGroup group = new ExplicitGroup("Imported " + newGroupName, GroupHierarchyType.INDEPENDENT,
JabRefPreferences.getInstance().getKeywordDelimiter());
newGroups.setGroup(group);
group.add(allOtherEntries);
} catch (IllegalArgumentException e) {
LOGGER.error("Problem appending entries to group", e);
}
}
target.getGroups().ifPresentOrElse(
newGroups::moveTo,
// target does not contain any groups, so we can just use the new groups
() -> target.setGroups(newGroups));
});
}

private void mergeContentSelectors(MetaData target, MetaData other) {
for (ContentSelector selector : other.getContentSelectorList()) {
target.addContentSelector(selector);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.jabref.logic.bibtex;
package org.jabref.logic.database;

import java.util.Collection;
import java.util.HashMap;
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/org/jabref/model/database/BibDatabase.java
Original file line number Diff line number Diff line change
Expand Up @@ -548,9 +548,9 @@ public void setEpilog(String epilog) {
* Registers an listener object (subscriber) to the internal event bus.
* The following events are posted:
*
* - {@link EntryAddedEvent}
* - {@link EntryChangedEvent}
* - {@link EntriesRemovedEvent}
* - {@link EntriesAddedEvent}
* - {@link EntryChangedEvent}
* - {@link EntriesRemovedEvent}
*
* @param listener listener (subscriber) to add
*/
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/org/jabref/model/metadata/MetaData.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import org.jabref.model.metadata.event.MetaDataChangedEvent;

import com.google.common.eventbus.EventBus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MetaData {

Expand All @@ -41,6 +43,8 @@ public class MetaData {
public static final char SEPARATOR_CHARACTER = ';';
public static final String SEPARATOR_STRING = String.valueOf(SEPARATOR_CHARACTER);

private static final Logger LOGGER = LoggerFactory.getLogger(MetaData.class);

private final EventBus eventBus = new EventBus();
private final Map<EntryType, String> citeKeyPatterns = new HashMap<>(); // <BibType, Pattern>
private final Map<String, String> userFileDirectory = new HashMap<>(); // <User, FilePath>
Expand Down Expand Up @@ -266,7 +270,7 @@ public void setEncoding(Charset encoding) {
}

/**
* This Method (with additional parameter) has been introduced to avoid event loops while saving a database.
* This method (with additional parameter) has been introduced to avoid event loops while saving a database.
*/
public void setEncoding(Charset encoding, ChangePropagation postChanges) {
this.encoding = Objects.requireNonNull(encoding);
Expand Down
Loading

0 comments on commit 35f5078

Please sign in to comment.