package net.sf.jabref.imports; import java.awt.event.ActionEvent; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.text.Normalizer; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import net.sf.jabref.BasePanel; import net.sf.jabref.BibtexDatabase; import net.sf.jabref.BibtexEntry; import net.sf.jabref.GUIGlobals; import net.sf.jabref.Globals; import net.sf.jabref.JabRefFrame; import net.sf.jabref.MnemonicAwareAction; import net.sf.jabref.Util; import net.sf.jabref.export.AutoSaveManager; import net.sf.jabref.export.SaveSession; import net.sf.jabref.external.FileLinksUpgradeWarning; import net.sf.jabref.gui.FileDialogs; import net.sf.jabref.label.HandleDuplicateWarnings; // The action concerned with opening an existing database. public class OpenDatabaseAction extends MnemonicAwareAction { /** * */ private static final long serialVersionUID = 1L; boolean showDialog; private JabRefFrame frame; // List of actions that may need to be called after opening the file. Such as // upgrade actions etc. that may depend on the JabRef version that wrote the file: private static ArrayList<PostOpenAction> postOpenActions = new ArrayList<PostOpenAction>(); static { // Add the action for checking for new custom entry types loaded from // the bib file: postOpenActions.add(new CheckForNewEntryTypesAction()); // Add the action for the new external file handling system in version 2.3: postOpenActions.add(new FileLinksUpgradeWarning()); // Add the action for warning about and handling duplicate BibTeX keys: postOpenActions.add(new HandleDuplicateWarnings()); } public OpenDatabaseAction(JabRefFrame frame, boolean showDialog) { super(GUIGlobals.getImage("open")); this.frame = frame; this.showDialog = showDialog; putValue(NAME, "Open database"); putValue(ACCELERATOR_KEY, Globals.prefs.getKey("Open database")); putValue(SHORT_DESCRIPTION, Globals.lang("Open BibTeX database")); } public void actionPerformed(ActionEvent e) { List<File> filesToOpen = new ArrayList<File>(); //File fileToOpen = null; if (showDialog) { String[] chosen = FileDialogs.getMultipleFiles(frame.getFrame(), new File(Globals.prefs.get("workingDirectory")), ".bib", true); if (chosen != null) for (int i=0; i<chosen.length; i++) { if (chosen[i] != null) filesToOpen.add(new File(chosen[i])); } /* String chosenFile = Globals.getNewFile(frame, new File(Globals.prefs.get("workingDirectory")), ".bib", JFileChooser.OPEN_DIALOG, true); if (chosenFile != null) { fileToOpen = new File(chosenFile); }*/ } else { Util.pr(NAME); Util.pr(e.getActionCommand()); filesToOpen.add(new File(Util.checkName(e.getActionCommand()))); } BasePanel toRaise = null; int initialCount = filesToOpen.size(), removed = 0; // Check if any of the files are already open: for (Iterator<File> iterator = filesToOpen.iterator(); iterator.hasNext();) { File file = iterator.next(); for (int i=0; i<frame.getTabbedPane().getTabCount(); i++) { BasePanel bp = frame.baseAt(i); if ((bp.getFile() != null) && bp.getFile().equals(file)) { iterator.remove(); removed++; // See if we removed the final one. If so, we must perhaps // raise the BasePanel in question: if (removed == initialCount) { toRaise = bp; } break; } } } // Run the actual open in a thread to prevent the program // locking until the file is loaded. if (filesToOpen.size() > 0) { final List<File> theFiles = Collections.unmodifiableList(filesToOpen); (new Thread() { public void run() { for (Iterator<File> i=theFiles.iterator(); i.hasNext();) openIt(i.next(), true); } }).start(); for (Iterator<File> i=theFiles.iterator(); i.hasNext();) frame.getFileHistory().newFile(i.next().getPath()); } // If no files are remaining to open, this could mean that a file was // already open. If so, we may have to raise the correct tab: else if (toRaise != null) { frame.output(Globals.lang("File '%0' is already open.", toRaise.getFile().getPath())); frame.getTabbedPane().setSelectedComponent(toRaise); } } class OpenItSwingHelper implements Runnable { BasePanel bp; boolean raisePanel; File file; OpenItSwingHelper(BasePanel bp, File file, boolean raisePanel) { this.bp = bp; this.raisePanel = raisePanel; this.file = file; } public void run() { frame.addTab(bp, file, raisePanel); } } public void openIt(File file, boolean raisePanel) { if ((file != null) && (file.exists())) { File fileToLoad = file; frame.output(Globals.lang("Opening") + ": '" + file.getPath() + "'"); boolean tryingAutosave = false; boolean autoSaveFound = AutoSaveManager.newerAutoSaveExists(file); if (autoSaveFound && !Globals.prefs.getBoolean("promptBeforeUsingAutosave")) { // We have found a newer autosave, and the preferences say we should load // it without prompting, so we replace the fileToLoad: fileToLoad = AutoSaveManager.getAutoSaveFile(file); tryingAutosave = true; } else if (autoSaveFound) { // We have found a newer autosave, but we are not allowed to use it without // prompting. int answer = JOptionPane.showConfirmDialog(null,"<html>"+ Globals.lang("An autosave file was found for this database. This could indicate ") +Globals.lang("that JabRef didn't shut down cleanly last time the file was used.")+"<br>" +Globals.lang("Do you want to recover the database from the autosave file?")+"</html>", Globals.lang("Recover from autosave"), JOptionPane.YES_NO_OPTION); if (answer == JOptionPane.YES_OPTION) { fileToLoad = AutoSaveManager.getAutoSaveFile(file); tryingAutosave = true; } } boolean done = false; while (!done) { String fileName = file.getPath(); Globals.prefs.put("workingDirectory", file.getPath()); // Should this be done _after_ we know it was successfully opened? String encoding = Globals.prefs.get("defaultEncoding"); if (Util.hasLockFile(file)) { long modTime = Util.getLockFileTimeStamp(file); if ((modTime != -1) && (System.currentTimeMillis() - modTime > SaveSession.LOCKFILE_CRITICAL_AGE)) { // The lock file is fairly old, so we can offer to "steal" the file: int answer = JOptionPane.showConfirmDialog(null, "<html>"+Globals.lang("Error opening file") +" '"+fileName+"'. "+Globals.lang("File is locked by another JabRef instance.") +"<p>"+Globals.lang("Do you want to override the file lock?"), Globals.lang("File locked"), JOptionPane.YES_NO_OPTION); if (answer == JOptionPane.YES_OPTION) { Util.deleteLockFile(file); } else return; } else if (!Util.waitForFileLock(file, 10)) { JOptionPane.showMessageDialog(null, Globals.lang("Error opening file") +" '"+fileName+"'. "+Globals.lang("File is locked by another JabRef instance."), Globals.lang("Error"), JOptionPane.ERROR_MESSAGE); return; } } ParserResult pr; String errorMessage = null; try { pr = loadDatabase(fileToLoad, encoding); } catch (Exception ex) { //ex.printStackTrace(); errorMessage = ex.getMessage(); pr = null; } if ((pr == null) || (pr == ParserResult.INVALID_FORMAT)) { JOptionPane.showMessageDialog(null, Globals.lang("Error opening file") + " '" + fileName + "'", Globals.lang("Error"), JOptionPane.ERROR_MESSAGE); String message = "<html>"+errorMessage+"<p>"+ (tryingAutosave ? Globals.lang("Error opening autosave of '%0'. Trying to load '%0' instead.", file.getName()) : ""/*Globals.lang("Error opening file '%0'.", file.getName())*/)+"</html>"; JOptionPane.showMessageDialog(null, message, Globals.lang("Error opening file"), JOptionPane.ERROR_MESSAGE); if (tryingAutosave) { tryingAutosave = false; fileToLoad = file; } else done = true; continue; } else done = true; final BasePanel panel = addNewDatabase(pr, file, raisePanel); if (tryingAutosave) panel.markNonUndoableBaseChanged(); // After adding the database, go through our list and see if // any post open actions need to be done. For instance, checking // if we found new entry types that can be imported, or checking // if the database contents should be modified due to new features // in this version of JabRef: final ParserResult prf = pr; SwingUtilities.invokeLater(new Runnable() { public void run() { performPostOpenActions(panel, prf, true); } }); } } } /** * Go through the list of post open actions, and perform those that need * to be performed. * @param panel The BasePanel where the database is shown. * @param pr The result of the bib file parse operation. */ public static void performPostOpenActions(BasePanel panel, ParserResult pr, boolean mustRaisePanel) { for (Iterator<PostOpenAction> iterator = postOpenActions.iterator(); iterator.hasNext();) { PostOpenAction action = iterator.next(); if (action.isActionNecessary(pr)) { if (mustRaisePanel) panel.frame().getTabbedPane().setSelectedComponent(panel); action.performAction(panel, pr); } } } public BasePanel addNewDatabase(ParserResult pr, File file, boolean raisePanel) { String fileName = file.getPath(); BibtexDatabase db = pr.getDatabase(); HashMap<String, String> meta = pr.getMetaData(); if (pr.hasWarnings()) { final String[] wrns = pr.warnings(); (new Thread() { public void run() { StringBuffer wrn = new StringBuffer(); for (int i = 0; i < wrns.length; i++) wrn.append(i + 1).append(". ").append(wrns[i]).append("\n"); if (wrn.length() > 0) wrn.deleteCharAt(wrn.length() - 1); // Note to self or to someone else: The following line causes an // ArrayIndexOutOfBoundsException in situations with a large number of // warnings; approx. 5000 for the database I opened when I observed the problem // (duplicate key warnings). I don't think this is a big problem for normal situations, // and it may possibly be a bug in the Swing code. JOptionPane.showMessageDialog(frame, wrn.toString(), Globals.lang("Warnings"), JOptionPane.WARNING_MESSAGE); } }).start(); } BasePanel bp = new BasePanel(frame, db, file, meta, pr.getEncoding()); // file is set to null inside the EventDispatcherThread SwingUtilities.invokeLater(new OpenItSwingHelper(bp, file, raisePanel)); frame.output(Globals.lang("Opened database") + " '" + fileName + "' " + Globals.lang("with") + " " + db.getEntryCount() + " " + Globals.lang("entries") + "."); return bp; } public static ParserResult loadDataBase(File fileToOpen, String encoding, String source) throws IOException{ if(encoding != null && encoding.contains(".")){ encoding = encoding.substring(encoding.indexOf(".") + 1); } ParserResult pr = loadDatabase(fileToOpen, encoding); if(source != null && source.length() > 0 && source.equalsIgnoreCase("docear_bibtex_source.mendeley")){ for(BibtexEntry entry : pr.getDatabase().getEntries()){ for(String field : entry.getAllFields()){ String value = entry.getField(field); value = parseSpecialChars(value); value = removeMendeleyBackSlash(value); if(field.equalsIgnoreCase("file") && !System.getProperty("os.name").startsWith("Win") && !value.startsWith(":/")){ value = value.replaceFirst(":", ":/"); } entry.setField(field, value); } } } try{ for(BibtexEntry entry : pr.getDatabase().getEntries()){ for(String field : entry.getAllFields()){ String value = entry.getField(field); value = convertHTMLunicode(value); entry.setField(field, value); } } } catch(Exception e){ System.out.println(e); } return pr; } private static String removeMendeleyBackSlash(String path) { path = path.replace("$backslash$", "\\"); if(System.getProperty("os.name").startsWith("Win")){ path = path.replace("/", "\\\\"); } return path; } public static String convertHTMLunicode(String s){ if(s == null) return s; Pattern p = Pattern.compile("&#([0-9][0-9]?[0-9]?[0-9]?);"); Matcher m = p.matcher(s); StringBuffer buffer = new StringBuffer(); while (m.find()){ m.appendReplacement(buffer, new String(Character.toChars(Integer.parseInt(m.group(1))))); } m.appendTail(buffer); return buffer.toString(); } public static String parseSpecialChars(String s){ s = s.replaceAll("\\\\\"[{]([a-zA-Z])[}]", "$1" + "\u0308"); // replace ��� s = s.replaceAll("\\\\`[{]([a-zA-Z])[}]", "$1" + "\u0300"); // replace ` s = s.replaceAll("\\\\'[{]([a-zA-Z])[}]", "$1" + "\u0301"); // replace ´ s = s.replaceAll("\\\\\\^[{]([a-zA-Z])[}]", "$1" + "\u0302"); // replace ^ s = s.replaceAll("\\\\~[{]([a-zA-Z])[}]", "$1" + "\u0303"); // replace ~ s = s.replaceAll("\\\\=[{]([a-zA-Z])[}]", "$1" + "\u0304"); // replace - above s = s.replaceAll("\\\\\\.[{]([a-zA-Z])[}]", "$1" + "\u0307"); // replace . above s = s.replaceAll("\\\\u[{]([a-zA-Z])[}]", "$1" + "\u030c"); // replace v above s = s.replaceAll("\\\\v[{]([a-zA-Z])[}]", "$1" + "\u0306"); // replace combining breve s = s.replaceAll("\\\\H[{]([a-zA-Z])[}]", "$1" + "\u030b"); // replace double acute accent s = s.replaceAll("\\\\t[{]([a-zA-Z])([a-zA-Z])[}]", "$1" + "\u0361" + "$2"); // replace double inverted breve s = s.replaceAll("\\\\c[{]([a-zA-Z])[}]", "$1" + "\u0355"); // replace right arrowhead below s = s.replaceAll("\\\\d[{]([a-zA-Z])[}]", "$1" + "\u0323"); // replace . below s = s.replaceAll("\\\\b[{]([a-zA-Z])[}]", "$1" + "\u0331"); // replace - below if(s.contains("\\ss")){ s = s.replace("\\ss", "\u00df"); } if(s.contains("\\AE")){ s = s.replace("\\AE", "\u00c6"); } if(s.contains("\\ae")){ s = s.replace("\\ae", "\u00e6"); } if(s.contains("\\OE")){ s = s.replace("\\OE", "\u0152"); } if(s.contains("\\oe")){ s = s.replace("\\oe", "\u0153"); } if(s.contains("\\O")){ s = s.replace("\\O", "\u00d8"); } if(s.contains("\\o")){ s = s.replace("\\o", "\u00f8"); } if(s.contains("\\L")){ s = s.replace("\\L", "\u0141"); } if(s.contains("\\l")){ s = s.replace("\\l", "\u0142"); } if(s.contains("\\AA")){ s = s.replace("\\AA", "\u00c5"); } if(s.contains("\\aa")){ s = s.replace("\\aa", "\u00e5"); } if(s.contains("\\")){ s = s.replace("\\", ""); } s = Normalizer.normalize(s, Normalizer.Form.NFKC); return s; } public static ParserResult loadDatabase(File fileToOpen, String encoding) throws IOException { // First we make a quick check to see if this looks like a BibTeX file: Reader reader;// = ImportFormatReader.getReader(fileToOpen, encoding); //if (!BibtexParser.isRecognizedFormat(reader)) // return null; // The file looks promising. Reinitialize the reader and go on: //reader = getReader(fileToOpen, encoding); // We want to check if there is a JabRef signature in the file, because that would tell us // which character encoding is used. However, to read the signature we must be using a compatible // encoding in the first place. Since the signature doesn't contain any fancy characters, we can // read it regardless of encoding, with either UTF8 or UTF-16. That's the hypothesis, at any rate. // 8 bit is most likely, so we try that first: Reader utf8Reader = ImportFormatReader.getReader(fileToOpen, "UTF8"); String suppliedEncoding = checkForEncoding(utf8Reader); utf8Reader.close(); // Now if that didn't get us anywhere, we check with the 16 bit encoding: if (suppliedEncoding == null) { Reader utf16Reader = ImportFormatReader.getReader(fileToOpen, "UTF-16"); suppliedEncoding = checkForEncoding(utf16Reader); utf16Reader.close(); //System.out.println("Result of UTF-16 test: "+suppliedEncoding); } //System.out.println(suppliedEncoding != null ? "Encoding: '"+suppliedEncoding+"' Len: "+suppliedEncoding.length() : "no supplied encoding"); if ((suppliedEncoding != null)) { try { reader = ImportFormatReader.getReader(fileToOpen, suppliedEncoding); encoding = suppliedEncoding; // Just so we put the right info into the ParserResult. } catch (Exception ex) { ex.printStackTrace(); reader = ImportFormatReader.getReader(fileToOpen, encoding); // The supplied encoding didn't work out, so we use the default. } } else { // We couldn't find a header with info about encoding. Use default: reader = ImportFormatReader.getReader(fileToOpen, encoding); } BibtexParser bp = new BibtexParser(reader); ParserResult pr = bp.parse(); pr.setEncoding(encoding); pr.setFile(fileToOpen); return pr; } private static String checkForEncoding(Reader reader) { String suppliedEncoding = null; StringBuffer headerText = new StringBuffer(); try { boolean keepon = true; int piv = 0, offset = 0; int c; while (keepon) { c = reader.read(); if ((piv == 0) && ((c == '%') || (Character.isWhitespace((char)c)))) offset++; else { headerText.append((char) c); if (c == GUIGlobals.SIGNATURE.charAt(piv)) piv++; else //if (((char)c) == '@') keepon = false; } //System.out.println(headerText.toString()); found: if (piv == GUIGlobals.SIGNATURE.length()) { keepon = false; //if (headerText.length() > GUIGlobals.SIGNATURE.length()) // System.out.println("'"+headerText.toString().substring(0, headerText.length()-GUIGlobals.SIGNATURE.length())+"'"); // Found the signature. The rest of the line is unknown, so we skip // it: while (reader.read() != '\n'){ // keep reading } // If the next line starts with something like "% ", handle this: while (((c =reader.read()) == '%') || (Character.isWhitespace((char)c))){ // keep reading } // Then we must skip the "Encoding: ". We may already have read the first // character: if ((char)c != GUIGlobals.encPrefix.charAt(0)) break found; for (int i = 1; i < GUIGlobals.encPrefix.length(); i++) { if (reader.read() != GUIGlobals.encPrefix.charAt(i)) break found; // No, // it // doesn't // seem // to // match. } // If ok, then read the rest of the line, which should contain the // name // of the encoding: StringBuffer sb = new StringBuffer(); while ((c = reader.read()) != '\n') { sb.append((char) c); } suppliedEncoding = sb.toString(); } } } catch (IOException ex) { } return suppliedEncoding != null ? suppliedEncoding.trim() : null; } }