Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable Merging of BibDatabases #6689

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -145,58 +139,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