From e3226f8878c41e9451e1c4f05b328ea547677af1 Mon Sep 17 00:00:00 2001 From: tsantalis Date: Sun, 27 Oct 2024 17:16:08 -0400 Subject: [PATCH] Initial fix for #792 --- .../decomposition/ReplacementAlgorithm.java | 19 +- .../decomposition/UMLOperationBodyMapper.java | 123 +++++++++++- .../test/TestStatementMappings.java | 34 ++++ .../test/TestStatementMappingsJunit4.java | 34 ++++ .../mappings/FetchAndMergeEntry-v1.txt | 186 ++++++++++++++++++ .../mappings/FetchAndMergeEntry-v2.txt | 164 +++++++++++++++ src/test/resources/mappings/jabref-12025.txt | 42 ++++ 7 files changed, 598 insertions(+), 4 deletions(-) create mode 100644 src/test/resources/mappings/FetchAndMergeEntry-v1.txt create mode 100644 src/test/resources/mappings/FetchAndMergeEntry-v2.txt create mode 100644 src/test/resources/mappings/jabref-12025.txt diff --git a/src/main/java/gr/uom/java/xmi/decomposition/ReplacementAlgorithm.java b/src/main/java/gr/uom/java/xmi/decomposition/ReplacementAlgorithm.java index c934c1b857..9e16003037 100644 --- a/src/main/java/gr/uom/java/xmi/decomposition/ReplacementAlgorithm.java +++ b/src/main/java/gr/uom/java/xmi/decomposition/ReplacementAlgorithm.java @@ -2398,6 +2398,23 @@ else if(invocationCoveringTheEntireStatement1 != null && (r = invocationCovering } } } + else if(statement1 instanceof AbstractExpression && invocation1.getName().equals("isPresent") && invocation1.getExpression() != null && + (invocationCoveringTheEntireStatement2.getName().equals("ifPresent") || invocationCoveringTheEntireStatement2.getName().equals("ifPresentOrElse")) && + invocationCoveringTheEntireStatement2.arguments().size() >= 1) { + if(invocationCoveringTheEntireStatement2.arguments().get(0).startsWith(invocation1.getExpression() + JAVA.LAMBDA_ARROW)) { + Replacement replacement = new MethodInvocationReplacement(invocation1.actualString(), + invocationCoveringTheEntireStatement2.actualString(), invocation1, invocationCoveringTheEntireStatement2, ReplacementType.METHOD_INVOCATION); + replacementInfo.addReplacement(replacement); + return replacementInfo.getReplacements(); + } + else if(invocationCoveringTheEntireStatement2.getExpression() != null && invocationCoveringTheEntireStatement2.getExpression().equals(invocation1.getExpression()) && + invocationCoveringTheEntireStatement2.arguments().get(0).contains(JAVA.LAMBDA_ARROW)) { + Replacement replacement = new MethodInvocationReplacement(invocation1.actualString(), + invocationCoveringTheEntireStatement2.actualString(), invocation1, invocationCoveringTheEntireStatement2, ReplacementType.METHOD_INVOCATION); + replacementInfo.addReplacement(replacement); + return replacementInfo.getReplacements(); + } + } } } } @@ -4767,7 +4784,7 @@ else if(invocations1.size() == 0 && invocations2.size() > 0) { } protected static boolean streamAPIName(String name) { - return name.equals("stream") || name.equals("filter") || name.equals("forEach") || name.equals("collect") || name.equals("map") || name.equals("removeIf"); + return name.equals("stream") || name.equals("filter") || name.equals("forEach") || name.equals("collect") || name.equals("map") || name.equals("flatMap") || name.equals("removeIf") || name.equals("ifPresent") || name.equals("ifPresentOrElse"); } protected static List streamAPICalls(AbstractCodeFragment leaf) { diff --git a/src/main/java/gr/uom/java/xmi/decomposition/UMLOperationBodyMapper.java b/src/main/java/gr/uom/java/xmi/decomposition/UMLOperationBodyMapper.java index 5ed1c2352f..4aa3d6e3ed 100644 --- a/src/main/java/gr/uom/java/xmi/decomposition/UMLOperationBodyMapper.java +++ b/src/main/java/gr/uom/java/xmi/decomposition/UMLOperationBodyMapper.java @@ -1765,6 +1765,24 @@ else if(composite.getLocationInfo().subsumes(m.getFragment2().getLocationInfo()) private void processStreamAPIStatements(List leaves1, List leaves2, List innerNodes1, Set streamAPIStatements2) throws RefactoringMinerTimedOutException { + Map> map = new LinkedHashMap>(); + int mapSize = 0; + streamAPICallsInExtractedMethods(container2, leaves2, streamAPIStatements2, map); + while(mapSize < map.size()) { + int i=0; + int tmpMapSize = map.size(); + for(VariableDeclarationContainer key : new LinkedHashSet<>(map.keySet())) { + if(i >= mapSize) { + streamAPICallsInExtractedMethods(key, map.get(key), streamAPIStatements2, map); + } + i++; + } + mapSize = tmpMapSize; + } + List newLeaves2 = new ArrayList(); + for(VariableDeclarationContainer key : map.keySet()) { + newLeaves2.addAll(map.get(key)); + } //match expressions in inner nodes from T1 with leaves from T2 List expressionsT1 = new ArrayList(); for(CompositeStatementObject composite : innerNodes1) { @@ -1776,6 +1794,8 @@ private void processStreamAPIStatements(List leaves1, List } int numberOfMappings = mappings.size(); processLeaves(expressionsT1, leaves2, new LinkedHashMap(), false); + boolean onlyNestedMappings = this.mappings.size() == numberOfMappings; + processLeaves(expressionsT1, newLeaves2, new LinkedHashMap(), false); List mappings = new ArrayList<>(this.mappings); if(numberOfMappings == mappings.size()) { @@ -1921,7 +1941,63 @@ else if(streamAPICall.getName().equals("stream")) { AbstractCodeFragment fragment2 = mapping.getFragment2(); for(ListIterator innerNodeIterator1 = innerNodes1.listIterator(); innerNodeIterator1.hasNext();) { CompositeStatementObject composite = innerNodeIterator1.next(); - if(composite.getExpressions().contains(fragment1)) { + if((composite.getLocationInfo().getCodeElementType().equals(CodeElementType.FOR_STATEMENT) || + composite.getLocationInfo().getCodeElementType().equals(CodeElementType.ENHANCED_FOR_STATEMENT) || + composite.getLocationInfo().getCodeElementType().equals(CodeElementType.WHILE_STATEMENT) || + composite.getLocationInfo().getCodeElementType().equals(CodeElementType.DO_STATEMENT)) && + composite.getLocationInfo().subsumes(fragment1.getLocationInfo()) && + onlyNestedMappings) { + AbstractCodeFragment streamAPICallStatement = null; + List streamAPICalls = null; + for(AbstractCodeFragment leaf2 : streamAPIStatements2) { + if(leaves2.contains(leaf2)) { + streamAPICallStatement = leaf2; + streamAPICalls = streamAPICalls(leaf2); + break; + } + } + if(streamAPICallStatement != null && streamAPICalls != null) { + List lambdaParameters = nestedLambdaParameters(streamAPICallStatement.getLambdas()); + Set additionallyMatchedStatements1 = new LinkedHashSet<>(); + additionallyMatchedStatements1.add(composite); + Set additionallyMatchedStatements2 = new LinkedHashSet<>(); + additionallyMatchedStatements2.add(streamAPICallStatement); + for(AbstractCall streamAPICall : streamAPICalls) { + if(streamAPICall.getName().equals("forEach")) { + CompositeReplacement replacement = new CompositeReplacement(composite.getString(), streamAPICallStatement.getString(), additionallyMatchedStatements1, additionallyMatchedStatements2); + Set replacements = new LinkedHashSet<>(); + replacements.add(replacement); + LeafMapping newMapping = createLeafMapping(composite, streamAPICallStatement, new LinkedHashMap(), false); + newMapping.addReplacements(replacements); + TreeSet mappingSet = new TreeSet<>(); + mappingSet.add(newMapping); + for(VariableDeclaration lambdaParameter : lambdaParameters) { + for(VariableDeclaration compositeParameter : composite.getVariableDeclarations()) { + if(lambdaParameter.getVariableName().equals(compositeParameter.getVariableName())) { + Pair pair = Pair.of(compositeParameter, lambdaParameter); + matchedVariables.add(pair); + } + else { + for(Replacement r : mapping.getReplacements()) { + if(r.getBefore().equals(compositeParameter.getVariableName()) && r.getAfter().equals(lambdaParameter.getVariableName())) { + Pair pair = Pair.of(compositeParameter, lambdaParameter); + matchedVariables.add(pair); + break; + } + } + } + } + } + ReplaceLoopWithPipelineRefactoring ref = new ReplaceLoopWithPipelineRefactoring(additionallyMatchedStatements1, additionallyMatchedStatements2, container1, container2); + newMapping.addRefactoring(ref); + addToMappings(newMapping, mappingSet); + leaves2.remove(newMapping.getFragment2()); + innerNodeIterator1.remove(); + } + } + } + } + else if(composite.getExpressions().contains(fragment1)) { AbstractCodeFragment streamAPICallStatement = null; List streamAPICalls = null; for(AbstractCodeFragment leaf2 : streamAPIStatements2) { @@ -1933,6 +2009,11 @@ else if(streamAPICall.getName().equals("stream")) { break; } } + else if(fragment2.equals(leaf2)) { + streamAPICallStatement = leaf2; + streamAPICalls = streamAPICalls(leaf2); + break; + } } if(streamAPICallStatement != null && streamAPICalls != null) { List lambdaParameters = nestedLambdaParameters(streamAPICallStatement.getLambdas()); @@ -2060,8 +2141,20 @@ else if(composite.getLocationInfo().subsumes(m.getFragment1().getLocationInfo()) } } } - ReplaceLoopWithPipelineRefactoring ref = new ReplaceLoopWithPipelineRefactoring(additionallyMatchedStatements1, additionallyMatchedStatements2, container1, container2); - newMapping.addRefactoring(ref); + boolean loopFound = false; + for(AbstractCodeFragment fragment : additionallyMatchedStatements1) { + if(fragment.getLocationInfo().getCodeElementType().equals(CodeElementType.FOR_STATEMENT) || + fragment.getLocationInfo().getCodeElementType().equals(CodeElementType.ENHANCED_FOR_STATEMENT) || + fragment.getLocationInfo().getCodeElementType().equals(CodeElementType.WHILE_STATEMENT) || + fragment.getLocationInfo().getCodeElementType().equals(CodeElementType.DO_STATEMENT)) { + loopFound = true; + break; + } + } + if(loopFound) { + ReplaceLoopWithPipelineRefactoring ref = new ReplaceLoopWithPipelineRefactoring(additionallyMatchedStatements1, additionallyMatchedStatements2, container1, container2); + newMapping.addRefactoring(ref); + } addToMappings(newMapping, mappingSet); leaves2.remove(newMapping.getFragment2()); innerNodeIterator1.remove(); @@ -2071,6 +2164,30 @@ else if(composite.getLocationInfo().subsumes(m.getFragment1().getLocationInfo()) } } + private void streamAPICallsInExtractedMethods(VariableDeclarationContainer callerOperation, List leaves2, Set streamAPIStatements2, Map> map) { + if(classDiff != null) { + for(AbstractCodeFragment leaf2 : leaves2) { + List calls = leaf2.getMethodInvocations(); + for(AbstractCall call : calls) { + UMLOperation addedOperation = classDiff.matchesOperation(call, classDiff.getAddedOperations(), callerOperation); + if(addedOperation != null && !map.keySet().contains(addedOperation)) { + List newLeaves2 = new ArrayList(); + if(!addedOperation.hasEmptyBody()) { + Set newStreamAPIStatements2 = statementsWithStreamAPICalls(addedOperation.getBody().getCompositeStatement().getLeaves()); + for(AbstractCodeFragment streamAPICall : newStreamAPIStatements2) { + if(streamAPICall.getLambdas().size() > 0) { + streamAPIStatements2.add(streamAPICall); + expandAnonymousAndLambdas(streamAPICall, newLeaves2, new ArrayList(), new LinkedHashSet<>(), new LinkedHashSet<>(), anonymousClassList2(), codeFragmentOperationMap2, container2, false); + } + } + } + map.put(addedOperation, newLeaves2); + } + } + } + } + } + protected UMLOperationBodyMapper(UMLOperation anonymousClassOperation, LambdaExpressionObject lambda2, UMLOperationBodyMapper parentMapper) throws RefactoringMinerTimedOutException { this.classDiff = parentMapper.classDiff; this.modelDiff = classDiff != null ? classDiff.getModelDiff() : null; diff --git a/src/test/java/org/refactoringminer/test/TestStatementMappings.java b/src/test/java/org/refactoringminer/test/TestStatementMappings.java index 3926051a0f..43346d4b5d 100644 --- a/src/test/java/org/refactoringminer/test/TestStatementMappings.java +++ b/src/test/java/org/refactoringminer/test/TestStatementMappings.java @@ -240,6 +240,40 @@ public void handle(String commitId, List refactorings) { Assertions.assertTrue(expected.size() == actual.size() && expected.containsAll(actual) && actual.containsAll(expected)); } + @Test + public void testNestedExtractMethodStatementMappingsWithStreamsMigration() throws Exception { + final List actual = new ArrayList<>(); + Map fileContentsBefore = new LinkedHashMap(); + Map fileContentsCurrent = new LinkedHashMap(); + String contentsV1 = FileUtils.readFileToString(new File(EXPECTED_PATH + "FetchAndMergeEntry-v1.txt")); + String contentsV2 = FileUtils.readFileToString(new File(EXPECTED_PATH + "FetchAndMergeEntry-v2.txt")); + fileContentsBefore.put("src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java", contentsV1); + fileContentsCurrent.put("src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java", contentsV2); + UMLModel parentUMLModel = GitHistoryRefactoringMinerImpl.createModel(fileContentsBefore, new LinkedHashSet()); + UMLModel currentUMLModel = GitHistoryRefactoringMinerImpl.createModel(fileContentsCurrent, new LinkedHashSet()); + + UMLModelDiff modelDiff = parentUMLModel.diff(currentUMLModel); + List refactorings = modelDiff.getRefactorings(); + List parentMappers = new ArrayList<>(); + for (Refactoring ref : refactorings) { + if(ref instanceof ExtractOperationRefactoring) { + ExtractOperationRefactoring ex = (ExtractOperationRefactoring)ref; + UMLOperationBodyMapper bodyMapper = ex.getBodyMapper(); + if(!bodyMapper.isNested()) { + if(!parentMappers.contains(bodyMapper.getParentMapper())) { + parentMappers.add(bodyMapper.getParentMapper()); + } + } + mapperInfo(bodyMapper, actual); + } + } + for(UMLOperationBodyMapper parentMapper : parentMappers) { + mapperInfo(parentMapper, actual); + } + List expected = IOUtils.readLines(new FileReader(EXPECTED_PATH + "jabref-12025.txt")); + Assertions.assertTrue(expected.size() == actual.size() && expected.containsAll(actual) && actual.containsAll(expected)); + } + @Test public void testDuplicatedExtractMethodStatementMappings() throws Exception { GitHistoryRefactoringMinerImpl miner = new GitHistoryRefactoringMinerImpl(); diff --git a/src/test/java/org/refactoringminer/test/TestStatementMappingsJunit4.java b/src/test/java/org/refactoringminer/test/TestStatementMappingsJunit4.java index 6eb5cdae2e..745c3111ea 100644 --- a/src/test/java/org/refactoringminer/test/TestStatementMappingsJunit4.java +++ b/src/test/java/org/refactoringminer/test/TestStatementMappingsJunit4.java @@ -382,6 +382,40 @@ public void handle(String commitId, List refactorings) { Assert.assertTrue(expected.size() == actual.size() && expected.containsAll(actual) && actual.containsAll(expected)); } + @Test + public void testNestedExtractMethodStatementMappingsWithStreamsMigration() throws Exception { + final List actual = new ArrayList<>(); + Map fileContentsBefore = new LinkedHashMap(); + Map fileContentsCurrent = new LinkedHashMap(); + String contentsV1 = FileUtils.readFileToString(new File(EXPECTED_PATH + "FetchAndMergeEntry-v1.txt")); + String contentsV2 = FileUtils.readFileToString(new File(EXPECTED_PATH + "FetchAndMergeEntry-v2.txt")); + fileContentsBefore.put("src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java", contentsV1); + fileContentsCurrent.put("src/main/java/org/jabref/gui/mergeentries/FetchAndMergeEntry.java", contentsV2); + UMLModel parentUMLModel = GitHistoryRefactoringMinerImpl.createModel(fileContentsBefore, new LinkedHashSet()); + UMLModel currentUMLModel = GitHistoryRefactoringMinerImpl.createModel(fileContentsCurrent, new LinkedHashSet()); + + UMLModelDiff modelDiff = parentUMLModel.diff(currentUMLModel); + List refactorings = modelDiff.getRefactorings(); + List parentMappers = new ArrayList<>(); + for (Refactoring ref : refactorings) { + if(ref instanceof ExtractOperationRefactoring) { + ExtractOperationRefactoring ex = (ExtractOperationRefactoring)ref; + UMLOperationBodyMapper bodyMapper = ex.getBodyMapper(); + if(!bodyMapper.isNested()) { + if(!parentMappers.contains(bodyMapper.getParentMapper())) { + parentMappers.add(bodyMapper.getParentMapper()); + } + } + mapperInfo(bodyMapper, actual); + } + } + for(UMLOperationBodyMapper parentMapper : parentMappers) { + mapperInfo(parentMapper, actual); + } + List expected = IOUtils.readLines(new FileReader(EXPECTED_PATH + "jabref-12025.txt")); + Assert.assertTrue(expected.size() == actual.size() && expected.containsAll(actual) && actual.containsAll(expected)); + } + @Test public void testNestedExtractMethodStatementMappingsWithIntermediateDelegate() throws Exception { GitHistoryRefactoringMinerImpl miner = new GitHistoryRefactoringMinerImpl(); diff --git a/src/test/resources/mappings/FetchAndMergeEntry-v1.txt b/src/test/resources/mappings/FetchAndMergeEntry-v1.txt new file mode 100644 index 0000000000..c14e0770c1 --- /dev/null +++ b/src/test/resources/mappings/FetchAndMergeEntry-v1.txt @@ -0,0 +1,186 @@ +package org.jabref.gui.mergeentries; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; + +import javax.swing.undo.UndoManager; + +import org.jabref.gui.DialogService; +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.gui.undo.NamedCompound; +import org.jabref.gui.undo.UndoableChangeType; +import org.jabref.gui.undo.UndoableFieldChange; +import org.jabref.logic.importer.EntryBasedFetcher; +import org.jabref.logic.importer.FetcherClientException; +import org.jabref.logic.importer.FetcherServerException; +import org.jabref.logic.importer.IdBasedFetcher; +import org.jabref.logic.importer.ImportCleanup; +import org.jabref.logic.importer.WebFetcher; +import org.jabref.logic.importer.WebFetchers; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.util.BackgroundTask; +import org.jabref.logic.util.TaskExecutor; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +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; +import org.slf4j.LoggerFactory; + +/** + * Class for fetching and merging bibliographic information + */ +public class FetchAndMergeEntry { + + // All identifiers listed here should also appear at {@link org.jabref.logic.importer.CompositeIdFetcher#performSearchById} + public static List SUPPORTED_FIELDS = Arrays.asList(StandardField.DOI, StandardField.EPRINT, StandardField.ISBN); + + private static final Logger LOGGER = LoggerFactory.getLogger(FetchAndMergeEntry.class); + private final DialogService dialogService; + private final UndoManager undoManager; + private final BibDatabaseContext bibDatabaseContext; + private final TaskExecutor taskExecutor; + private final GuiPreferences preferences; + + public FetchAndMergeEntry(BibDatabaseContext bibDatabaseContext, + TaskExecutor taskExecutor, + GuiPreferences preferences, + DialogService dialogService, + UndoManager undoManager) { + this.bibDatabaseContext = bibDatabaseContext; + this.taskExecutor = taskExecutor; + this.preferences = preferences; + this.dialogService = dialogService; + this.undoManager = undoManager; + } + + public void fetchAndMerge(BibEntry entry) { + fetchAndMerge(entry, SUPPORTED_FIELDS); + } + + public void fetchAndMerge(BibEntry entry, Field field) { + fetchAndMerge(entry, Collections.singletonList(field)); + } + + public void fetchAndMerge(BibEntry entry, List fields) { + for (Field field : fields) { + Optional fieldContent = entry.getField(field); + if (fieldContent.isPresent()) { + Optional fetcher = WebFetchers.getIdBasedFetcherForField(field, preferences.getImportFormatPreferences()); + if (fetcher.isPresent()) { + BackgroundTask.wrap(() -> fetcher.get().performSearchById(fieldContent.get())) + .onSuccess(fetchedEntry -> { + ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode(), preferences.getFieldPreferences()); + String type = field.getDisplayName(); + if (fetchedEntry.isPresent()) { + cleanup.doPostCleanup(fetchedEntry.get()); + showMergeDialog(entry, fetchedEntry.get(), fetcher.get()); + } else { + dialogService.notify(Localization.lang("Cannot get info based on given %0: %1", type, fieldContent.get())); + } + }) + .onFailure(exception -> { + LOGGER.error("Error while fetching bibliographic information", exception); + if (exception instanceof FetcherClientException) { + dialogService.showInformationDialogAndWait(Localization.lang("Fetching information using %0", fetcher.get().getName()), Localization.lang("No data was found for the identifier")); + } else if (exception instanceof FetcherServerException) { + dialogService.showInformationDialogAndWait(Localization.lang("Fetching information using %0", fetcher.get().getName()), Localization.lang("Server not available")); + } else { + dialogService.showInformationDialogAndWait(Localization.lang("Fetching information using %0", fetcher.get().getName()), Localization.lang("Error occurred %0", exception.getMessage())); + } + }) + .executeWith(taskExecutor); + } + } else { + dialogService.notify(Localization.lang("No %0 found", field.getDisplayName())); + } + } + } + + private void showMergeDialog(BibEntry originalEntry, BibEntry fetchedEntry, WebFetcher fetcher) { + MergeEntriesDialog dialog = new MergeEntriesDialog(originalEntry, fetchedEntry, preferences); + dialog.setTitle(Localization.lang("Merge entry with %0 information", fetcher.getName())); + dialog.setLeftHeaderText(Localization.lang("Original entry")); + dialog.setRightHeaderText(Localization.lang("Entry from %0", fetcher.getName())); + Optional mergedEntry = dialogService.showCustomDialogAndWait(dialog).map(EntriesMergeResult::mergedEntry); + + if (mergedEntry.isPresent()) { + NamedCompound ce = new NamedCompound(Localization.lang("Merge entry with %0 information", fetcher.getName())); + + // Updated the original entry with the new fields + Set jointFields = new TreeSet<>(Comparator.comparing(Field::getName)); + jointFields.addAll(mergedEntry.get().getFields()); + Set originalFields = new TreeSet<>(Comparator.comparing(Field::getName)); + originalFields.addAll(originalEntry.getFields()); + boolean edited = false; + + // entry type + EntryType oldType = originalEntry.getType(); + EntryType newType = mergedEntry.get().getType(); + + if (!oldType.equals(newType)) { + originalEntry.setType(newType); + ce.addEdit(new UndoableChangeType(originalEntry, oldType, newType)); + edited = true; + } + + // fields + for (Field field : jointFields) { + Optional originalString = originalEntry.getField(field); + Optional mergedString = mergedEntry.get().getField(field); + if (originalString.isEmpty() || !originalString.equals(mergedString)) { + originalEntry.setField(field, mergedString.get()); // mergedString always present + ce.addEdit(new UndoableFieldChange(originalEntry, field, originalString.orElse(null), + mergedString.get())); + edited = true; + } + } + + // Remove fields which are not in the merged entry, unless they are internal fields + for (Field field : originalFields) { + if (!jointFields.contains(field) && !FieldFactory.isInternalField(field)) { + Optional originalString = originalEntry.getField(field); + originalEntry.clearField(field); + ce.addEdit(new UndoableFieldChange(originalEntry, field, originalString.get(), null)); // originalString always present + edited = true; + } + } + + if (edited) { + ce.end(); + undoManager.addEdit(ce); + dialogService.notify(Localization.lang("Updated entry with info from %0", fetcher.getName())); + } else { + dialogService.notify(Localization.lang("No information added")); + } + } else { + dialogService.notify(Localization.lang("Canceled merging entries")); + } + } + + public void fetchAndMerge(BibEntry entry, EntryBasedFetcher fetcher) { + BackgroundTask.wrap(() -> fetcher.performSearch(entry).stream().findFirst()) + .onSuccess(fetchedEntry -> { + if (fetchedEntry.isPresent()) { + ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode(), preferences.getFieldPreferences()); + cleanup.doPostCleanup(fetchedEntry.get()); + showMergeDialog(entry, fetchedEntry.get(), fetcher); + } else { + dialogService.notify(Localization.lang("Could not find any bibliographic information.")); + } + }) + .onFailure(exception -> { + LOGGER.error("Error while fetching entry with {} ", fetcher.getName(), exception); + dialogService.showErrorDialogAndWait(Localization.lang("Error while fetching from %0", fetcher.getName()), exception); + }) + .executeWith(taskExecutor); + } +} diff --git a/src/test/resources/mappings/FetchAndMergeEntry-v2.txt b/src/test/resources/mappings/FetchAndMergeEntry-v2.txt new file mode 100644 index 0000000000..9e7261792f --- /dev/null +++ b/src/test/resources/mappings/FetchAndMergeEntry-v2.txt @@ -0,0 +1,164 @@ + +package org.jabref.gui.mergeentries; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import javax.swing.undo.UndoManager; + +import org.jabref.gui.DialogService; +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.gui.undo.NamedCompound; +import org.jabref.logic.importer.EntryBasedFetcher; +import org.jabref.logic.importer.IdBasedFetcher; +import org.jabref.logic.importer.ImportCleanup; +import org.jabref.logic.importer.WebFetcher; +import org.jabref.logic.importer.WebFetchers; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.util.BackgroundTask; +import org.jabref.logic.util.TaskExecutor; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.StandardField; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FetchAndMergeEntry { + + + public static final List SUPPORTED_IDENTIFIER_FIELDS = Arrays.asList(StandardField.DOI, StandardField.EPRINT, StandardField.ISBN); + + private static final Logger LOGGER = LoggerFactory.getLogger(FetchAndMergeEntry.class); + + private final DialogService dialogService; + private final UndoManager undoManager; + private final BibDatabaseContext bibDatabaseContext; + private final TaskExecutor taskExecutor; + private final GuiPreferences preferences; + + public FetchAndMergeEntry(BibDatabaseContext bibDatabaseContext, + TaskExecutor taskExecutor, + GuiPreferences preferences, + DialogService dialogService, + UndoManager undoManager) { + this.bibDatabaseContext = bibDatabaseContext; + this.taskExecutor = taskExecutor; + this.preferences = preferences; + this.dialogService = dialogService; + this.undoManager = undoManager; + } + + public void fetchAndMerge(BibEntry entry) { + fetchAndMerge(entry, SUPPORTED_IDENTIFIER_FIELDS); + } + + public void fetchAndMerge(BibEntry entry, Field field) { + fetchAndMerge(entry, List.of(field)); + } + + public void fetchAndMerge(BibEntry entry, List fields) { + fields.forEach(field -> fetchAndMergeEntry(entry, field)); + } + + private void fetchAndMergeEntry(BibEntry entry, Field field) { + entry.getField(field) + .flatMap(fieldContent -> WebFetchers.getIdBasedFetcherForField(field, preferences.getImportFormatPreferences())) + .ifPresent(fetcher -> executeFetchTask(fetcher, field, entry)); + } + + private void executeFetchTask(IdBasedFetcher fetcher, Field field, BibEntry entry) { + entry.getField(field).ifPresent(fieldContent -> + BackgroundTask.wrap(() -> fetcher.performSearchById(fieldContent)) + .onSuccess(fetchedEntry -> processFetchedEntry(fetchedEntry, entry, fetcher)) + .onFailure(exception -> handleFetchException(exception, fetcher)) + .executeWith(taskExecutor) + ); + } + + private void processFetchedEntry(Optional fetchedEntry, BibEntry originalEntry, IdBasedFetcher fetcher) { + ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode(), preferences.getFieldPreferences()); + fetchedEntry.ifPresentOrElse( + entry -> { + cleanup.doPostCleanup(entry); + showMergeDialog(originalEntry, entry, fetcher); + }, + () -> notifyNoInfo(originalEntry) + ); + } + + private void notifyNoInfo(BibEntry entry) { + dialogService.notify(Localization.lang("Cannot get info based on given %0: %1", + entry.getType().getDisplayName(), + entry.getCitationKey().orElse(""))); + } + + private void handleFetchException(Exception exception, WebFetcher fetcher) { + LOGGER.error("Error while fetching bibliographic information", exception); + dialogService.showErrorDialogAndWait( + Localization.lang("Error while fetching from %0", fetcher.getName()), + exception + ); + } + + private void showMergeDialog(BibEntry originalEntry, BibEntry fetchedEntry, WebFetcher fetcher) { + MergeEntriesDialog dialog = createMergeDialog(originalEntry, fetchedEntry, fetcher); + Optional mergedEntry = dialogService.showCustomDialogAndWait(dialog) + .map(EntriesMergeResult::mergedEntry); + + mergedEntry.ifPresentOrElse( + entry -> processMergedEntry(originalEntry, entry, fetcher), + () -> notifyCanceledMerge(originalEntry) + ); + } + + private MergeEntriesDialog createMergeDialog(BibEntry originalEntry, BibEntry fetchedEntry, WebFetcher fetcher) { + MergeEntriesDialog dialog = new MergeEntriesDialog(originalEntry, fetchedEntry, preferences); + dialog.setTitle(Localization.lang("Merge entry with %0 information", fetcher.getName())); + dialog.setLeftHeaderText(Localization.lang("Original entry")); + dialog.setRightHeaderText(Localization.lang("Entry from %0", fetcher.getName())); + return dialog; + } + + private void processMergedEntry(BibEntry originalEntry, BibEntry mergedEntry, WebFetcher fetcher) { + NamedCompound ce = new NamedCompound(Localization.lang("Merge entry with %0 information", fetcher.getName())); + MergeEntriesHelper.mergeEntries(originalEntry, mergedEntry, ce); + + if (ce.hasEdits()) { + ce.end(); + undoManager.addEdit(ce); + } + + dialogService.notify(Localization.lang("Updated entry with info from %0", fetcher.getName())); + } + + private void notifyCanceledMerge(BibEntry entry) { + String citationKey = entry.getCitationKey().orElse(entry.getAuthorTitleYear(40)); + dialogService.notify(Localization.lang("Canceled merging entries") + " [" + citationKey + "]"); + } + + public void fetchAndMerge(BibEntry entry, EntryBasedFetcher fetcher) { + BackgroundTask.wrap(() -> fetcher.performSearch(entry).stream().findFirst()) + .onSuccess(fetchedEntry -> processFetchedEntryForEntryBasedFetcher(fetchedEntry, entry, fetcher)) + .onFailure(exception -> handleFetchException(exception, fetcher)) + .executeWith(taskExecutor); + } + + private void processFetchedEntryForEntryBasedFetcher(Optional fetchedEntry, BibEntry originalEntry, EntryBasedFetcher fetcher) { + fetchedEntry + .map(this::cleanupFetchedEntry) + .ifPresentOrElse( + fe -> showMergeDialog(originalEntry, fe, fetcher), + () -> dialogService.notify(Localization.lang("Could not find any bibliographic information.")) + ); + } + + private BibEntry cleanupFetchedEntry(BibEntry fetchedEntry) { + ImportCleanup cleanup = ImportCleanup.targeting(bibDatabaseContext.getMode(), preferences.getFieldPreferences()); + cleanup.doPostCleanup(fetchedEntry); + return fetchedEntry; + } +} + diff --git a/src/test/resources/mappings/jabref-12025.txt b/src/test/resources/mappings/jabref-12025.txt new file mode 100644 index 0000000000..5379f9da5b --- /dev/null +++ b/src/test/resources/mappings/jabref-12025.txt @@ -0,0 +1,42 @@ +public fetchAndMerge(entry BibEntry, fields List) : void -> private executeFetchTask(fetcher IdBasedFetcher, field Field, entry BibEntry) : void +line range:75-75==line range:73-78 +public fetchAndMerge(entry BibEntry, fields List) : void -> private fetchAndMergeEntry(entry BibEntry, field Field) : void +line range:78-78==line range:67-69 +line range:77-77==line range:68-68 +public fetchAndMerge(entry BibEntry, fields List) : void -> private notifyNoInfo(entry BibEntry) : void +line range:87-87==line range:93-95 +private showMergeDialog(originalEntry BibEntry, fetchedEntry BibEntry, fetcher WebFetcher) : void -> private createMergeDialog(originalEntry BibEntry, fetchedEntry BibEntry, fetcher WebFetcher) : MergeEntriesDialog +line range:109-109==line range:118-118 +line range:110-110==line range:119-119 +line range:111-111==line range:120-120 +line range:112-112==line range:121-121 +private showMergeDialog(originalEntry BibEntry, fetchedEntry BibEntry, fetcher WebFetcher) : void -> private processMergedEntry(originalEntry BibEntry, mergedEntry BibEntry, fetcher WebFetcher) : void +line range:116-116==line range:126-126 +line range:158-158==line range:130-130 +line range:159-159==line range:131-131 +line range:160-160==line range:134-134 +line range:157-163==line range:129-132 +line range:157-161==line range:129-132 +private showMergeDialog(originalEntry BibEntry, fetchedEntry BibEntry, fetcher WebFetcher) : void -> private notifyCanceledMerge(entry BibEntry) : void +line range:165-165==line range:139-139 +public fetchAndMerge(entry BibEntry, fetcher EntryBasedFetcher) : void -> private handleFetchException(exception Exception, fetcher WebFetcher) : void +line range:182-182==line range:100-103 +line range:181-181==line range:99-99 +public fetchAndMerge(entry BibEntry, fetcher EntryBasedFetcher) : void -> private cleanupFetchedEntry(fetchedEntry BibEntry) : BibEntry +line range:173-173==line range:159-159 +line range:174-174==line range:160-160 +public fetchAndMerge(entry BibEntry, fetcher EntryBasedFetcher) : void -> private processFetchedEntryForEntryBasedFetcher(fetchedEntry Optional, originalEntry BibEntry, fetcher EntryBasedFetcher) : void +line range:175-175==line range:153-153 +line range:177-177==line range:154-154 +public fetchAndMerge(entry BibEntry, fields List) : void -> public fetchAndMerge(entry BibEntry, fields List) : void +line range:78-101==line range:67-69 +line range:74-105==line range:63-63 +line range:76-104==line range:73-78 +line range:83-88==line range:83-89 +private showMergeDialog(originalEntry BibEntry, fetchedEntry BibEntry, fetcher WebFetcher) : void -> private showMergeDialog(originalEntry BibEntry, fetchedEntry BibEntry, fetcher WebFetcher) : void +line range:113-113==line range:108-109 +line range:113-113==line range:109-109 +line range:115-115==line range:111-114 +public fetchAndMerge(entry BibEntry, fetcher EntryBasedFetcher) : void -> public fetchAndMerge(entry BibEntry, fetcher EntryBasedFetcher) : void +line range:170-170==line range:143-143 +line range:170-184==line range:143-146 \ No newline at end of file