package org.jabref.gui;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.swing.SwingUtilities;
import org.jabref.JabRefExecutorService;
import org.jabref.JabRefGUI;
import org.jabref.gui.DuplicateResolverDialog.DuplicateResolverResult;
import org.jabref.gui.DuplicateResolverDialog.DuplicateResolverType;
import org.jabref.gui.undo.NamedCompound;
import org.jabref.gui.undo.UndoableInsertEntry;
import org.jabref.gui.undo.UndoableRemoveEntry;
import org.jabref.gui.worker.CallBack;
import org.jabref.logic.bibtex.DuplicateCheck;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.entry.BibEntry;
import spin.Spin;
public class DuplicateSearch implements Runnable {
private final BasePanel panel;
private List<BibEntry> bes;
private final List<List<BibEntry>> duplicates = new ArrayList<>();
public DuplicateSearch(BasePanel bp) {
panel = bp;
}
@Override
public void run() {
panel.output(Localization.lang("Searching for duplicates..."));
bes = panel.getDatabase().getEntries();
if (bes.size() < 2) {
return;
}
SearcherRunnable st = new SearcherRunnable();
JabRefExecutorService.INSTANCE.executeInterruptableTask(st, "DuplicateSearcher");
int current = 0;
final List<BibEntry> toRemove = new ArrayList<>();
final List<BibEntry> toAdd = new ArrayList<>();
int duplicateCounter = 0;
boolean autoRemoveExactDuplicates = false;
synchronized (duplicates) {
while (!st.finished() || (current < duplicates.size())) {
if (current >= duplicates.size()) {
// wait until the search thread puts something into duplicates vector
// or finish its work
try {
duplicates.wait();
} catch (InterruptedException ignored) {
// Ignore
}
} else { // duplicates found
List<BibEntry> be = duplicates.get(current);
current++;
if (!toRemove.contains(be.get(0)) && !toRemove.contains(be.get(1))) {
// Check if they are exact duplicates:
boolean askAboutExact = false;
if (DuplicateCheck.compareEntriesStrictly(be.get(0), be.get(1)) > 1) {
if (autoRemoveExactDuplicates) {
toRemove.add(be.get(1));
duplicateCounter++;
continue;
}
askAboutExact = true;
}
DuplicateCallBack cb = new DuplicateCallBack(JabRefGUI.getMainFrame(), be.get(0), be.get(1),
askAboutExact ? DuplicateResolverType.DUPLICATE_SEARCH_WITH_EXACT : DuplicateResolverType.DUPLICATE_SEARCH);
((CallBack) Spin.over(cb)).update();
duplicateCounter++;
DuplicateResolverResult answer = cb.getSelected();
if ((answer == DuplicateResolverResult.KEEP_LEFT)
|| (answer == DuplicateResolverResult.AUTOREMOVE_EXACT)) {
toRemove.add(be.get(1));
if (answer == DuplicateResolverResult.AUTOREMOVE_EXACT) {
autoRemoveExactDuplicates = true; // Remember choice
}
} else if (answer == DuplicateResolverResult.KEEP_RIGHT) {
toRemove.add(be.get(0));
} else if (answer == DuplicateResolverResult.BREAK) {
st.setFinished(); // thread killing
current = Integer.MAX_VALUE;
duplicateCounter--; // correct counter
} else if (answer == DuplicateResolverResult.KEEP_MERGE) {
toRemove.addAll(be);
toAdd.add(cb.getMergedEntry());
}
}
}
}
}
final NamedCompound ce = new NamedCompound(Localization.lang("duplicate removal"));
final int dupliC = duplicateCounter;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
// Now, do the actual removal:
if (!toRemove.isEmpty()) {
for (BibEntry entry : toRemove) {
panel.getDatabase().removeEntry(entry);
ce.addEdit(new UndoableRemoveEntry(panel.getDatabase(), entry, panel));
}
panel.markBaseChanged();
}
// and adding merged entries:
if (!toAdd.isEmpty()) {
for (BibEntry entry : toAdd) {
panel.getDatabase().insertEntry(entry);
ce.addEdit(new UndoableInsertEntry(panel.getDatabase(), entry, panel));
}
panel.markBaseChanged();
}
synchronized (duplicates) {
panel.output(Localization.lang("Duplicates found") + ": " + duplicates.size() + ' '
+ Localization.lang("pairs processed") + ": " + dupliC);
}
ce.end();
panel.getUndoManager().addEdit(ce);
}
});
}
class SearcherRunnable implements Runnable {
private volatile boolean finished;
@Override
public void run() {
for (int i = 0; (i < (bes.size() - 1)) && !finished; i++) {
for (int j = i + 1; (j < bes.size()) && !finished; j++) {
BibEntry first = bes.get(i);
BibEntry second = bes.get(j);
boolean eq = DuplicateCheck.isDuplicate(first, second, panel.getBibDatabaseContext().getMode());
// If (suspected) duplicates, add them to the duplicates vector.
if (eq) {
synchronized (duplicates) {
duplicates.add(Arrays.asList(first, second));
duplicates.notifyAll(); // send wake up all
}
}
}
}
finished = true;
// if no duplicates found, the graphical thread will never wake up
synchronized (duplicates) {
duplicates.notifyAll();
}
}
public boolean finished() {
return finished;
}
// Thread cancel option
// no synchronized used because no "really" critical situations expected
public void setFinished() {
finished = true;
}
}
static class DuplicateCallBack implements CallBack {
private DuplicateResolverResult reply = DuplicateResolverResult.NOT_CHOSEN;
private final JabRefFrame frame;
private final BibEntry one;
private final BibEntry two;
private final DuplicateResolverType dialogType;
private BibEntry merged;
public DuplicateCallBack(JabRefFrame frame, BibEntry one, BibEntry two, DuplicateResolverType dialogType) {
this.frame = frame;
this.one = one;
this.two = two;
this.dialogType = dialogType;
}
public DuplicateResolverResult getSelected() {
return reply;
}
public BibEntry getMergedEntry() {
return merged;
}
@Override
public void update() {
DuplicateResolverDialog diag = new DuplicateResolverDialog(frame, one, two, dialogType);
diag.setVisible(true);
diag.dispose();
reply = diag.getSelected();
merged = diag.getMergedEntry();
}
}
}