Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We added "All" option to the citation fetcher combo box, which queries all providers (CrossRef, OpenAlex, OpenCitations, SemanticScholar) and merges the results into a single deduplicated list.
- We added a quick setting toggle to enable cover images download. [#15322](https://github.com/JabRef/jabref/pull/15322)
- We now support refreshing existing CSL citations with respect to their in-text nature in the LibreOffice integration. [#15369](https://github.com/JabRef/jabref/pull/15369)
- We added automatic source groups to SLR results and fixed group merging to preserve all source groups. [#12542](https://github.com/JabRef/jabref/issues/12542)

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,21 @@ public class BatchEntryMergeTask extends BackgroundTask<Void> {
private final MergingIdBasedFetcher fetcher;
private final UndoManager undoManager;
private final NotificationService notificationService;
private final char keywordSeparator;

private int processedEntries;
private int successfulUpdates;

public BatchEntryMergeTask(List<BibEntry> entries,
MergingIdBasedFetcher fetcher,
UndoManager undoManager,
NotificationService notificationService) {
NotificationService notificationService,
char keywordSeparator) {
this.entries = entries;
this.fetcher = fetcher;
this.undoManager = undoManager;
this.notificationService = notificationService;
this.keywordSeparator = keywordSeparator;

this.compoundEdit = new NamedCompoundEdit(Localization.lang("Merge entries"));
this.processedEntries = 0;
Expand Down Expand Up @@ -107,7 +110,7 @@ private Optional<String> processSingleEntry(BibEntry entry) {
return fetcher.fetchEntry(entry)
.filter(MergingIdBasedFetcher.FetcherResult::hasChanges)
.flatMap(result -> {
boolean changesApplied = MergeEntriesHelper.mergeEntries(result.mergedEntry(), entry, compoundEdit);
boolean changesApplied = MergeEntriesHelper.mergeEntries(result.mergedEntry(), entry, compoundEdit, keywordSeparator);
if (changesApplied) {
successfulUpdates++;
return entry.getCitationKey();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ public void execute() {
entries,
fetcher,
undoManager,
notificationService);
notificationService,
preferences.getBibEntryPreferences().getKeywordSeparator());

mergeTask.executeWith(taskExecutor);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
import org.jabref.logic.bibtex.comparator.ComparisonResult;
import org.jabref.logic.bibtex.comparator.plausibility.PlausibilityComparatorFactory;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.KeywordList;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.FieldFactory;
import org.jabref.model.entry.field.StandardField;
import org.jabref.model.entry.types.EntryType;

import org.slf4j.Logger;
Expand All @@ -32,12 +34,13 @@ private MergeEntriesHelper() {
/// @param entryFromFetcher The entry containing new information (source, from the fetcher)
/// @param entryFromLibrary The entry to be updated (target, from the library)
/// @param namedCompoundEdit Compound edit to collect undo information
public static boolean mergeEntries(BibEntry entryFromFetcher, BibEntry entryFromLibrary, NamedCompoundEdit namedCompoundEdit) {
/// @param keywordSeparator Separator character used for union-merging the groups field
public static boolean mergeEntries(BibEntry entryFromFetcher, BibEntry entryFromLibrary, NamedCompoundEdit namedCompoundEdit, char keywordSeparator) {
LOGGER.debug("Entry from fetcher: {}", entryFromFetcher);
LOGGER.debug("Entry from library: {}", entryFromLibrary);

boolean typeChanged = mergeEntryType(entryFromFetcher, entryFromLibrary, namedCompoundEdit);
boolean fieldsChanged = mergeFields(entryFromFetcher, entryFromLibrary, namedCompoundEdit);
boolean fieldsChanged = mergeFields(entryFromFetcher, entryFromLibrary, namedCompoundEdit, keywordSeparator);
boolean fieldsRemoved = removeFieldsNotPresentInFetcher(entryFromFetcher, entryFromLibrary, namedCompoundEdit);

return typeChanged || fieldsChanged || fieldsRemoved;
Expand All @@ -56,7 +59,7 @@ private static boolean mergeEntryType(BibEntry entryFromFetcher, BibEntry entryF
return false;
}

private static boolean mergeFields(BibEntry entryFromFetcher, BibEntry entryFromLibrary, NamedCompoundEdit namedCompoundEdit) {
private static boolean mergeFields(BibEntry entryFromFetcher, BibEntry entryFromLibrary, NamedCompoundEdit namedCompoundEdit, char keywordSeparator) {
Set<Field> allFields = new LinkedHashSet<>();
allFields.addAll(entryFromFetcher.getFields());
allFields.addAll(entryFromLibrary.getFields());
Expand All @@ -67,7 +70,17 @@ private static boolean mergeFields(BibEntry entryFromFetcher, BibEntry entryFrom
Optional<String> fetcherValue = entryFromFetcher.getField(field);
Optional<String> libraryValue = entryFromLibrary.getField(field);

if (fetcherValue.isPresent() && shouldUpdateField(field, fetcherValue.get(), libraryValue)) {
if (field == StandardField.GROUPS && fetcherValue.isPresent()) {
// Always union-merge groups so no source group is ever lost
String merged = KeywordList.merge(libraryValue.orElse(""), fetcherValue.get(), keywordSeparator)
.getAsString(keywordSeparator);
if (!merged.equals(libraryValue.orElse(""))) {
LOGGER.debug("Union-merging groups: {} + {} -> {}", libraryValue.orElse(""), fetcherValue.get(), merged);
entryFromLibrary.setField(field, merged);
namedCompoundEdit.addEdit(new UndoableFieldChange(entryFromLibrary, field, libraryValue.orElse(null), merged));
anyFieldsChanged = true;
}
} else if (fetcherValue.isPresent() && shouldUpdateField(field, fetcherValue.get(), libraryValue)) {
LOGGER.debug("Updating field {}: {} -> {}", field, libraryValue.orElse(null), fetcherValue.get());
entryFromLibrary.setField(field, fetcherValue.get());
namedCompoundEdit.addEdit(new UndoableFieldChange(entryFromLibrary, field, libraryValue.orElse(null), fetcherValue.get()));
Expand All @@ -88,6 +101,10 @@ private static boolean removeFieldsNotPresentInFetcher(BibEntry entryFromFetcher
continue;
}

if (field == StandardField.GROUPS) {
continue;
}

Optional<String> value = entryFromLibrary.getField(field);
if (value.isPresent()) {
LOGGER.debug("Removing obsolete field {} with value {}", field, value.get());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package org.jabref.gui.mergeentries;

import org.jabref.gui.undo.NamedCompoundEdit;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.StandardField;
import org.jabref.model.entry.types.StandardEntryType;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

class MergeEntriesHelperTest {

private static final char KEYWORD_SEPARATOR = ',';

private NamedCompoundEdit compoundEdit;

@BeforeEach
void setup() {
compoundEdit = new NamedCompoundEdit("test");
}

@Test
void groupsFieldIsNotRemovedWhenFetcherHasNoGroups() {
BibEntry entryFromFetcher = new BibEntry(StandardEntryType.Article)
.withField(StandardField.TITLE, "Some Title");
BibEntry entryFromLibrary = new BibEntry(StandardEntryType.Article)
.withField(StandardField.TITLE, "Some Title")
.withField(StandardField.GROUPS, "IEEE");

MergeEntriesHelper.mergeEntries(entryFromFetcher, entryFromLibrary, compoundEdit, KEYWORD_SEPARATOR);

assertEquals("IEEE", entryFromLibrary.getField(StandardField.GROUPS).orElse(""));
}

@Test
void groupsAreUnionMergedWhenBothEntriesHaveGroups() {
BibEntry entryFromFetcher = new BibEntry(StandardEntryType.Article)
.withField(StandardField.TITLE, "Some Title")
.withField(StandardField.GROUPS, "Springer");
BibEntry entryFromLibrary = new BibEntry(StandardEntryType.Article)
.withField(StandardField.TITLE, "Some Title")
.withField(StandardField.GROUPS, "IEEE");

MergeEntriesHelper.mergeEntries(entryFromFetcher, entryFromLibrary, compoundEdit, KEYWORD_SEPARATOR);

String groups = entryFromLibrary.getField(StandardField.GROUPS).orElse("");
assertEquals("IEEE, Springer", entryFromLibrary.getField(StandardField.GROUPS).orElse(""));
}

@Test
void groupsAreNotDuplicatedOnRepeatedMerge() {
BibEntry entryFromFetcher = new BibEntry(StandardEntryType.Article)
.withField(StandardField.TITLE, "Some Title")
.withField(StandardField.GROUPS, "IEEE");
BibEntry entryFromLibrary = new BibEntry(StandardEntryType.Article)
.withField(StandardField.TITLE, "Some Title")
.withField(StandardField.GROUPS, "IEEE");

MergeEntriesHelper.mergeEntries(entryFromFetcher, entryFromLibrary, compoundEdit, KEYWORD_SEPARATOR);

assertEquals("IEEE", entryFromLibrary.getField(StandardField.GROUPS).orElse(""));
}

@Test
void groupsFromFetcherAreAddedWhenLibraryHasNoGroups() {
BibEntry entryFromFetcher = new BibEntry(StandardEntryType.Article)
.withField(StandardField.TITLE, "Some Title")
.withField(StandardField.GROUPS, "Springer");
BibEntry entryFromLibrary = new BibEntry(StandardEntryType.Article)
.withField(StandardField.TITLE, "Some Title");

MergeEntriesHelper.mergeEntries(entryFromFetcher, entryFromLibrary, compoundEdit, KEYWORD_SEPARATOR);

assertEquals("Springer", entryFromLibrary.getField(StandardField.GROUPS).orElse(""));
}

@Test
void regularObsoleteFieldIsRemovedWhenNotInFetcher() {
BibEntry entryFromFetcher = new BibEntry(StandardEntryType.Article)
.withField(StandardField.TITLE, "Some Title");
BibEntry entryFromLibrary = new BibEntry(StandardEntryType.Article)
.withField(StandardField.TITLE, "Some Title")
.withField(StandardField.NOTE, "Obsolete note");

MergeEntriesHelper.mergeEntries(entryFromFetcher, entryFromLibrary, compoundEdit, KEYWORD_SEPARATOR);

assertFalse(entryFromLibrary.hasField(StandardField.NOTE));
}
}
54 changes: 54 additions & 0 deletions jablib/src/main/java/org/jabref/logic/crawler/StudyRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

Expand All @@ -28,7 +31,12 @@
import org.jabref.logic.util.io.FileNameCleaner;
import org.jabref.model.database.BibDatabase;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.BibEntryTypesManager;
import org.jabref.model.groups.AllEntriesGroup;
import org.jabref.model.groups.ExplicitGroup;
import org.jabref.model.groups.GroupHierarchyType;
import org.jabref.model.groups.GroupTreeNode;
import org.jabref.model.metadata.SaveOrder;
import org.jabref.model.metadata.SelfContainedSaveOrder;
import org.jabref.model.study.FetchResult;
Expand Down Expand Up @@ -353,6 +361,7 @@ private String computeIDForQuery(String query) {
private void persistResults(List<QueryResult> crawlResults) throws IOException, SaveException {
DatabaseMerger merger = new DatabaseMerger(preferences.getBibEntryPreferences().getKeywordSeparator());
BibDatabase newStudyResultEntries = new BibDatabase();
Map<String, List<BibEntry>> fetcherEntryMap = new LinkedHashMap<>();

for (QueryResult result : crawlResults) {
BibDatabase queryResultEntries = new BibDatabase();
Expand All @@ -366,6 +375,11 @@ private void persistResults(List<QueryResult> crawlResults) throws IOException,
// Create citation keys for all entries that do not have one
generateCiteKeys(existingFetcherResult, fetcherEntries);

// tag entries + add group to per-fetcher .bib
addFetcherGroup(existingFetcherResult, fetcherResult.getFetcherName(), fetcherEntries.getEntries());
fetcherEntryMap.computeIfAbsent(fetcherResult.getFetcherName(), k -> new ArrayList<>())
.addAll(fetcherEntries.getEntries());

// Aggregate each fetcher result into the query result
merger.merge(queryResultEntries, fetcherEntries);

Expand All @@ -385,6 +399,10 @@ private void persistResults(List<QueryResult> crawlResults) throws IOException,
// Merge new entries into study result file
merger.merge(existingStudyResultEntries.getDatabase(), newStudyResultEntries);

// Add fetcher groups to final result.bib
fetcherEntryMap.forEach((fetcherName, entries) ->
addFetcherGroup(existingStudyResultEntries, fetcherName, entries));

writeResultToFile(getPathToStudyResultFile(), existingStudyResultEntries);
}

Expand Down Expand Up @@ -414,6 +432,42 @@ private void writeResultToFile(Path pathToFile, BibDatabaseContext context) thro
}
}

/// Creates an {@link ExplicitGroup} named after the fetcher and assigns all its entries to it.
/// If the group already exists in the database, new entries are added to it.
/// If no group tree exists yet in the context, a root {@link AllEntriesGroup} is created first.
///
/// @param context The database context to add the group to
/// @param fetcherName The name of the fetcher used as the group name
/// @param entries The entries fetched from that fetcher to assign to the group
private void addFetcherGroup(BibDatabaseContext context, String fetcherName, List<BibEntry> entries) {
try {
ExplicitGroup group = new ExplicitGroup(
fetcherName,
GroupHierarchyType.INDEPENDENT,
preferences.getBibEntryPreferences().getKeywordSeparator());

group.add(entries);

// Get existing root node or create a new AllEntriesGroup root if none exists yet
GroupTreeNode root = context.getMetaData().getGroups().orElseGet(() -> {
GroupTreeNode newRoot = GroupTreeNode.fromGroup(new AllEntriesGroup("All Entries"));
context.getMetaData().setGroups(newRoot);
return newRoot;
});

// add new entries to the existing group instead of creating a duplicate
root.getChildren().stream()
.filter(child -> fetcherName.equals(child.getGroup().getName()))
.findFirst()
.ifPresentOrElse(
existingNode -> existingNode.addEntriesToGroup(entries),
() -> root.addSubgroup(group)
);
} catch (IllegalArgumentException e) {
LOGGER.error("Problem adding fetcher group '{}' to database", fetcherName, e);
}
}

private Path getPathToFetcherResultFile(String query, String fetcherName) {
return repositoryPath.resolve(trimNameAndAddID(query)).resolve(FileNameCleaner.cleanFileName(fetcherName) + ".bib");
}
Expand Down
Loading
Loading