Java tutorial
/* Copyright (C) 2003-2015 JabRef contributors. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package net.sf.jabref.collab; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.Set; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import javax.swing.tree.DefaultMutableTreeNode; import net.sf.jabref.BibDatabaseContext; import net.sf.jabref.Defaults; import net.sf.jabref.Globals; import net.sf.jabref.JabRefExecutorService; import net.sf.jabref.MetaData; import net.sf.jabref.gui.BasePanel; import net.sf.jabref.gui.JabRefFrame; import net.sf.jabref.importer.OpenDatabaseAction; import net.sf.jabref.importer.ParserResult; import net.sf.jabref.logic.bibtex.comparator.EntryComparator; import net.sf.jabref.logic.exporter.BibDatabaseWriter; import net.sf.jabref.logic.exporter.BibtexDatabaseWriter; import net.sf.jabref.logic.exporter.FileSaveSession; import net.sf.jabref.logic.exporter.SaveException; import net.sf.jabref.logic.exporter.SavePreferences; import net.sf.jabref.logic.exporter.SaveSession; import net.sf.jabref.logic.groups.GroupTreeNode; import net.sf.jabref.logic.l10n.Localization; import net.sf.jabref.model.DuplicateCheck; import net.sf.jabref.model.database.BibDatabase; import net.sf.jabref.model.database.BibDatabaseMode; import net.sf.jabref.model.database.EntrySorter; import net.sf.jabref.model.entry.BibEntry; import net.sf.jabref.model.entry.BibtexString; import net.sf.jabref.model.entry.FieldName; import net.sf.jabref.preferences.JabRefPreferences; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class ChangeScanner implements Runnable { private static final String[] SORT_BY = new String[] { FieldName.YEAR, FieldName.AUTHOR, FieldName.TITLE }; private final File f; private final BibDatabase inMem; private final MetaData mdInMem; private final BasePanel panel; private final JabRefFrame frame; private BibDatabase inTemp; private MetaData mdInTemp; private static final Log LOGGER = LogFactory.getLog(ChangeScanner.class); private static final double MATCH_THRESHOLD = 0.4; /** * We create an ArrayList to hold the changes we find. These will be added in the form * of UndoEdit objects. We instantiate these so that the changes found in the file on disk * can be reproduced in memory by calling redo() on them. REDO, not UNDO! */ private final DefaultMutableTreeNode changes = new DefaultMutableTreeNode( Localization.lang("External changes")); // NamedCompound edit = new NamedCompound("Merged external changes") public ChangeScanner(JabRefFrame frame, BasePanel bp, File file) { this.panel = bp; this.frame = frame; this.inMem = bp.getDatabase(); this.mdInMem = bp.getBibDatabaseContext().getMetaData(); this.f = file; } @Override public void run() { try { // Parse the temporary file. Path tempFile = Globals.getFileUpdateMonitor().getTempFile(panel.fileMonitorHandle()); ParserResult pr = OpenDatabaseAction.loadDatabase(tempFile.toFile(), Globals.prefs.getDefaultEncoding()); inTemp = pr.getDatabase(); mdInTemp = pr.getMetaData(); // Parse the modified file. pr = OpenDatabaseAction.loadDatabase(f, Globals.prefs.getDefaultEncoding()); BibDatabase onDisk = pr.getDatabase(); MetaData mdOnDisk = pr.getMetaData(); // Sort both databases according to a common sort key. EntryComparator comp = new EntryComparator(false, true, SORT_BY[2]); comp = new EntryComparator(false, true, SORT_BY[1], comp); comp = new EntryComparator(false, true, SORT_BY[0], comp); EntrySorter sInTemp = inTemp.getSorter(comp); comp = new EntryComparator(false, true, SORT_BY[2]); comp = new EntryComparator(false, true, SORT_BY[1], comp); comp = new EntryComparator(false, true, SORT_BY[0], comp); EntrySorter sOnDisk = onDisk.getSorter(comp); comp = new EntryComparator(false, true, SORT_BY[2]); comp = new EntryComparator(false, true, SORT_BY[1], comp); comp = new EntryComparator(false, true, SORT_BY[0], comp); EntrySorter sInMem = inMem.getSorter(comp); // Start looking at changes. scanMetaData(mdInMem, mdInTemp, mdOnDisk); scanPreamble(inMem, inTemp, onDisk); scanStrings(inMem, inTemp, onDisk); scanEntries(sInMem, sInTemp, sOnDisk); scanGroups(mdInTemp, mdOnDisk); } catch (IOException ex) { LOGGER.warn("Problem running", ex); } } public boolean changesFound() { return changes.getChildCount() > 0; } public void displayResult(final DisplayResultCallback fup) { if (changes.getChildCount() > 0) { SwingUtilities.invokeLater(() -> { ChangeDisplayDialog dial = new ChangeDisplayDialog(frame, panel, inTemp, changes); dial.setLocationRelativeTo(frame); dial.setVisible(true); fup.scanResultsResolved(dial.isOkPressed()); if (dial.isOkPressed()) { // Overwrite the temp database: storeTempDatabase(); } }); } else { JOptionPane.showMessageDialog(frame, Localization.lang("No actual changes found."), Localization.lang("External changes"), JOptionPane.INFORMATION_MESSAGE); fup.scanResultsResolved(true); } } private void storeTempDatabase() { JabRefExecutorService.INSTANCE.execute(() -> { try { SavePreferences prefs = SavePreferences.loadForSaveFromPreferences(Globals.prefs) .withMakeBackup(false) .withEncoding(panel.getBibDatabaseContext().getMetaData().getEncoding()); Defaults defaults = new Defaults(BibDatabaseMode .fromPreference(Globals.prefs.getBoolean(JabRefPreferences.BIBLATEX_DEFAULT_MODE))); BibDatabaseWriter databaseWriter = new BibtexDatabaseWriter(FileSaveSession::new); SaveSession ss = databaseWriter.saveDatabase(new BibDatabaseContext(inTemp, mdInTemp, defaults), prefs); ss.commit(Globals.getFileUpdateMonitor().getTempFile(panel.fileMonitorHandle())); } catch (SaveException ex) { LOGGER.warn("Problem updating tmp file after accepting external changes", ex); } }); } private void scanMetaData(MetaData inMem1, MetaData inTemp1, MetaData onDisk) { MetaDataChange mdc = new MetaDataChange(inMem1, inTemp1); List<String> handledOnDisk = new ArrayList<>(); // Loop through the metadata entries of the "tmp" database, looking for // matches for (String key : inTemp1) { // See if the key is missing in the disk database: List<String> vod = onDisk.getData(key); if (vod == null) { mdc.insertMetaDataRemoval(key); } else { // Both exist. Check if they are different: List<String> vit = inTemp1.getData(key); if (!vod.equals(vit)) { mdc.insertMetaDataChange(key, vod); } // Remember that we've handled this one: handledOnDisk.add(key); } } // See if there are unhandled keys in the disk database: for (String key : onDisk) { if (!handledOnDisk.contains(key)) { mdc.insertMetaDataAddition(key, onDisk.getData(key)); } } if (mdc.getChangeCount() > 0) { changes.add(mdc); } } private void scanEntries(EntrySorter mem, EntrySorter tmp, EntrySorter disk) { // Create pointers that are incremented as the entries of each base are used in // successive order from the beginning. Entries "further down" in the "disk" base // can also be matched. int piv1; int piv2 = 0; // Create a HashSet where we can put references to entry numbers in the "disk" // database that we have matched. This is to avoid matching them twice. Set<String> used = new HashSet<>(disk.getEntryCount()); Set<Integer> notMatched = new HashSet<>(tmp.getEntryCount()); // Loop through the entries of the "tmp" database, looking for exact matches in the "disk" one. // We must finish scanning for exact matches before looking for near matches, to avoid an exact // match being "stolen" from another entry. mainLoop: for (piv1 = 0; piv1 < tmp.getEntryCount(); piv1++) { // First check if the similarly placed entry in the other base matches exactly. double comp = -1; // (if there are not any entries left in the "disk" database, comp will stay at -1, // and this entry will be marked as nonmatched). if (!used.contains(String.valueOf(piv2)) && (piv2 < disk.getEntryCount())) { comp = DuplicateCheck.compareEntriesStrictly(tmp.getEntryAt(piv1), disk.getEntryAt(piv2)); } if (comp > 1) { used.add(String.valueOf(piv2)); piv2++; continue; } // No? Then check if another entry matches exactly. if (piv2 < (disk.getEntryCount() - 1)) { for (int i = piv2 + 1; i < disk.getEntryCount(); i++) { if (used.contains(String.valueOf(i))) { comp = -1; } else { comp = DuplicateCheck.compareEntriesStrictly(tmp.getEntryAt(piv1), disk.getEntryAt(i)); } if (comp > 1) { used.add(String.valueOf(i)); continue mainLoop; } } } // No? Add this entry to the list of nonmatched entries. notMatched.add(piv1); } // Now we've found all exact matches, look through the remaining entries, looking // for close matches. if (!notMatched.isEmpty()) { for (Iterator<Integer> it = notMatched.iterator(); it.hasNext();) { piv1 = it.next(); // These two variables will keep track of which entry most closely matches the // one we're looking at, in case none matches completely. int bestMatchI = -1; double bestMatch = 0; double comp; if (piv2 < (disk.getEntryCount() - 1)) { for (int i = piv2; i < disk.getEntryCount(); i++) { if (used.contains(String.valueOf(i))) { comp = -1; } else { comp = DuplicateCheck.compareEntriesStrictly(tmp.getEntryAt(piv1), disk.getEntryAt(i)); } if (comp > bestMatch) { bestMatch = comp; bestMatchI = i; } } } if (bestMatch > MATCH_THRESHOLD) { used.add(String.valueOf(bestMatchI)); it.remove(); EntryChange ec = new EntryChange(bestFit(tmp, mem, piv1), tmp.getEntryAt(piv1), disk.getEntryAt(bestMatchI)); changes.add(ec); } else { EntryDeleteChange ec = new EntryDeleteChange(bestFit(tmp, mem, piv1), tmp.getEntryAt(piv1)); changes.add(ec); } } } // Finally, look if there are still untouched entries in the disk database. These // may have been added. if (used.size() < disk.getEntryCount()) { for (int i = 0; i < disk.getEntryCount(); i++) { if (!used.contains(String.valueOf(i))) { // See if there is an identical dupe in the mem database: boolean hasAlready = false; for (int j = 0; j < mem.getEntryCount(); j++) { if (DuplicateCheck.compareEntriesStrictly(mem.getEntryAt(j), disk.getEntryAt(i)) >= 1) { hasAlready = true; break; } } if (!hasAlready) { EntryAddChange ec = new EntryAddChange(disk.getEntryAt(i)); changes.add(ec); } } } } } /** * Finds the entry in neu best fitting the specified entry in old. If no entries get a score * above zero, an entry is still returned. * * @param old EntrySorter * @param neu EntrySorter * @param index int * @return BibEntry */ private static BibEntry bestFit(EntrySorter old, EntrySorter neu, int index) { double comp = -1; int found = 0; for (int i = 0; i < neu.getEntryCount(); i++) { double res = DuplicateCheck.compareEntriesStrictly(old.getEntryAt(index), neu.getEntryAt(i)); if (res > comp) { comp = res; found = i; } if (comp > 1) { break; } } return neu.getEntryAt(found); } private void scanPreamble(BibDatabase inMem1, BibDatabase onTmp, BibDatabase onDisk) { String mem = inMem1.getPreamble(); String tmp = onTmp.getPreamble(); String disk = onDisk.getPreamble(); if (tmp == null) { if ((disk != null) && !disk.isEmpty()) { changes.add(new PreambleChange(mem, disk)); } } else { if ((disk == null) || !tmp.equals(disk)) { changes.add(new PreambleChange(mem, disk)); } } } private void scanStrings(BibDatabase inMem1, BibDatabase onTmp, BibDatabase onDisk) { if (onTmp.hasNoStrings() && onDisk.hasNoStrings()) { return; } Set<Object> used = new HashSet<>(); Set<Object> usedInMem = new HashSet<>(); Set<String> notMatched = new HashSet<>(onTmp.getStringCount()); // First try to match by string names. mainLoop: for (String key : onTmp.getStringKeySet()) { BibtexString tmp = onTmp.getString(key); for (String diskId : onDisk.getStringKeySet()) { if (!used.contains(diskId)) { BibtexString disk = onDisk.getString(diskId); if (disk.getName().equals(tmp.getName())) { // We have found a string with a matching name. if ((tmp.getContent() != null) && !tmp.getContent().equals(disk.getContent())) { // But they have nonmatching contents, so we've found a change. Optional<BibtexString> mem = findString(inMem1, tmp.getName(), usedInMem); if (mem.isPresent()) { changes.add(new StringChange(mem.get(), tmp, tmp.getName(), mem.get().getContent(), disk.getContent())); } else { changes.add(new StringChange(null, tmp, tmp.getName(), null, disk.getContent())); } } used.add(diskId); continue mainLoop; } } } // If we get here, there was no match for this string. notMatched.add(tmp.getId()); } // See if we can detect a name change for those entries that we couldn't match. if (!notMatched.isEmpty()) { for (Iterator<String> i = notMatched.iterator(); i.hasNext();) { BibtexString tmp = onTmp.getString(i.next()); // If we get to this point, we found no string with matching name. See if we // can find one with matching content. for (String diskId : onDisk.getStringKeySet()) { if (!used.contains(diskId)) { BibtexString disk = onDisk.getString(diskId); if (disk.getContent().equals(tmp.getContent())) { // We have found a string with the same content. It cannot have the same // name, or we would have found it above. // Try to find the matching one in memory: BibtexString bsMem = null; for (String memId : inMem1.getStringKeySet()) { BibtexString bsMemCandidate = inMem1.getString(memId); if (bsMemCandidate.getContent().equals(disk.getContent()) && !usedInMem.contains(memId)) { usedInMem.add(memId); bsMem = bsMemCandidate; break; } } if (bsMem != null) { changes.add(new StringNameChange(bsMem, tmp, bsMem.getName(), tmp.getName(), disk.getName(), tmp.getContent())); i.remove(); used.add(diskId); } } } } } } if (!notMatched.isEmpty()) { // Still one or more non-matched strings. So they must have been removed. for (String nmId : notMatched) { BibtexString tmp = onTmp.getString(nmId); // The removed string is not removed from the mem version. findString(inMem1, tmp.getName(), usedInMem) .ifPresent(x -> changes.add(new StringRemoveChange(tmp, tmp, x))); } } // Finally, see if there are remaining strings in the disk database. They // must have been added. for (String diskId : onDisk.getStringKeySet()) { if (!used.contains(diskId)) { BibtexString disk = onDisk.getString(diskId); used.add(diskId); changes.add(new StringAddChange(disk)); } } } private static Optional<BibtexString> findString(BibDatabase base, String name, Set<Object> used) { if (!base.hasStringLabel(name)) { return Optional.empty(); } for (String key : base.getStringKeySet()) { BibtexString bs = base.getString(key); if (bs.getName().equals(name) && !used.contains(key)) { used.add(key); return Optional.of(bs); } } return Optional.empty(); } /** * This method only detects whether a change took place or not. It does not determine the type of change. This would * be possible, but difficult to do properly, so I rather only report the change. */ private void scanGroups(MetaData onTmp, MetaData onDisk) { final GroupTreeNode groupsTmp = onTmp.getGroups(); final GroupTreeNode groupsDisk = onDisk.getGroups(); if ((groupsTmp == null) && (groupsDisk == null)) { return; } if (((groupsTmp != null) && (groupsDisk == null)) || (groupsTmp == null)) { changes.add(new GroupChange(groupsDisk, groupsTmp)); return; } if (!groupsTmp.equals(groupsDisk)) { changes.add(new GroupChange(groupsDisk, groupsTmp)); } } @FunctionalInterface public interface DisplayResultCallback { void scanResultsResolved(boolean resolved); } }