5f7e58278c31532f12c923e0a83fe7c30c771491
[debian/jabref.git] / src / java / net / sf / jabref / Util.java
1 /*
2  Copyright (C) 2003 Morten O. Alver
3
4  All programs in this directory and
5  subdirectories are published under the GNU General Public License as
6  described below.
7
8  This program is free software; you can redistribute it and/or modify
9  it under the terms of the GNU General Public License as published by
10  the Free Software Foundation; either version 2 of the License, or (at
11  your option) any later version.
12
13  This program is distributed in the hope that it will be useful, but
14  WITHOUT ANY WARRANTY; without even the implied warranty of
15  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16  General Public License for more details.
17
18  You should have received a copy of the GNU General Public License
19  along with this program; if not, write to the Free Software
20  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
21  USA
22
23  Further information about the GNU GPL is available at:
24  http://www.gnu.org/copyleft/gpl.ja.html
25
26  */
27 // created by : Morten O. Alver 2003
28 //
29 // function : utility functions
30 //
31 // todo     :
32 //
33 // modified :  - r.nagel 20.04.2006
34 //               make the DateFormatter abstract and splitt the easyDate methode
35 //               (now we cannot change the dateformat dynamicly, sorry)
36
37 package net.sf.jabref;
38
39 import java.awt.BorderLayout;
40 import java.awt.CardLayout;
41 import java.awt.Color;
42 import java.awt.Component;
43 import java.awt.Dimension;
44 import java.awt.Font;
45 import java.awt.Point;
46 import java.awt.event.ActionEvent;
47 import java.awt.event.ActionListener;
48 import java.io.BufferedInputStream;
49 import java.io.BufferedOutputStream;
50 import java.io.File;
51 import java.io.FileInputStream;
52 import java.io.FileOutputStream;
53 import java.io.FilenameFilter;
54 import java.io.IOException;
55 import java.io.PrintWriter;
56 import java.io.StringWriter;
57 import java.net.URI;
58 import java.net.URISyntaxException;
59 import java.nio.charset.Charset;
60 import java.nio.charset.CharsetEncoder;
61 import java.text.NumberFormat;
62 import java.text.SimpleDateFormat;
63 import java.util.ArrayList;
64 import java.util.Arrays;
65 import java.util.Calendar;
66 import java.util.Collection;
67 import java.util.Date;
68 import java.util.HashSet;
69 import java.util.Iterator;
70 import java.util.LinkedList;
71 import java.util.List;
72 import java.util.Map;
73 import java.util.SortedSet;
74 import java.util.StringTokenizer;
75 import java.util.TreeSet;
76 import java.util.Vector;
77 import java.util.regex.Matcher;
78 import java.util.regex.Pattern;
79
80 import javax.swing.Box;
81 import javax.swing.JButton;
82 import javax.swing.JFrame;
83 import javax.swing.JLabel;
84 import javax.swing.JOptionPane;
85 import javax.swing.JPanel;
86 import javax.swing.JScrollPane;
87 import javax.swing.JTextArea;
88 import javax.swing.undo.UndoableEdit;
89
90 import net.sf.jabref.export.layout.LayoutEntry;
91 import net.sf.jabref.export.layout.LayoutFormatter;
92 import net.sf.jabref.external.ExternalFileType;
93 import net.sf.jabref.groups.AbstractGroup;
94 import net.sf.jabref.groups.KeywordGroup;
95 import net.sf.jabref.imports.CiteSeerFetcher;
96 import net.sf.jabref.undo.NamedCompound;
97 import net.sf.jabref.undo.UndoableFieldChange;
98
99 import com.jgoodies.forms.builder.DefaultFormBuilder;
100 import com.jgoodies.forms.layout.FormLayout;
101
102 /**
103  * Describe class <code>Util</code> here.
104  * 
105  * @author <a href="mailto:"> </a>
106  * @version 1.0
107  */
108 public class Util {
109         // A static Object for date formatting. Please do not create the object
110         // here,
111         // because there are some references from the Globals class.....
112         private static SimpleDateFormat dateFormatter = null;
113
114         // Colors are defined here.
115         public static Color fieldsCol = new Color(180, 180, 200);
116
117         // Integer values for indicating result of duplicate check (for entries):
118         final static int TYPE_MISMATCH = -1, NOT_EQUAL = 0, EQUAL = 1, EMPTY_IN_ONE = 2,
119                 EMPTY_IN_TWO = 3;
120
121         final static NumberFormat idFormat;
122
123         static {
124                 idFormat = NumberFormat.getInstance();
125                 idFormat.setMinimumIntegerDigits(8);
126                 idFormat.setGroupingUsed(false);
127         }
128
129         public static void bool(boolean b) {
130                 if (b)
131                         System.out.println("true");
132                 else
133                         System.out.println("false");
134         }
135
136         public static void pr(String s) {
137                 System.out.println(s);
138         }
139
140         public static void pr_(String s) {
141                 System.out.print(s);
142         }
143
144         public static String nCase(String s) {
145                 // Make first character of String uppercase, and the
146                 // rest lowercase.
147                 if (s.length() > 1)
148                         return s.substring(0, 1).toUpperCase() + s.substring(1, s.length()).toLowerCase();
149                 else
150                         return s.toUpperCase();
151
152         }
153
154         public static String checkName(String s) {
155                 // Append '.bib' to the string unless it ends with that.
156                 if (s.length() < 4 || !s.substring(s.length() - 4).equalsIgnoreCase(".bib")) {
157                         return s + ".bib";
158                 }
159                 return s;
160         }
161
162         private static int idCounter = 0;
163
164         public synchronized static String createNeutralId() {
165                 return idFormat.format(idCounter++);
166                 // return String.valueOf(idCounter++);
167         }
168
169         /**
170          * This method sets the location of a Dialog such that it is centered with
171          * regard to another window, but not outside the screen on the left and the
172          * top.
173          */
174         public static void placeDialog(java.awt.Dialog diag, java.awt.Container win) {
175                 Dimension ds = diag.getSize(), df = win.getSize();
176                 Point pf = win.getLocation();
177                 diag.setLocation(new Point(Math.max(0, pf.x + (df.width - ds.width) / 2), Math.max(0, pf.y
178                         + (df.height - ds.height) / 2)));
179
180         }
181
182         /**
183          * This method translates a field or string from Bibtex notation, with
184          * possibly text contained in " " or { }, and string references,
185          * concatenated by '#' characters, into Bibkeeper notation, where string
186          * references are enclosed in a pair of '#' characters.
187          */
188         public static String parseField(String content) {
189                 if (content.length() == 0)
190                         return content;
191                 String toSet = "";
192                 boolean string;
193                 // Keeps track of whether the next item is
194                 // a reference to a string, or normal content. First we must
195                 // check which we begin with. We simply check if we can find
196                 // a '#' before either '"' or '{'.
197                 int hash = content.indexOf('#'), wr1 = content.indexOf('"'), wr2 = content.indexOf('{'), end = content
198                         .length();
199                 if (hash == -1)
200                         hash = end;
201                 if (wr1 == -1)
202                         wr1 = end;
203                 if (wr2 == -1)
204                         wr2 = end;
205                 string = ((wr1 == end) && (wr2 == end)) || (hash < Math.min(wr1, wr2));
206
207                 // System.out.println("FileLoader: "+content+" "+string+" "+hash+"
208                 // "+wr1+" "+wr2);
209                 StringTokenizer tok = new StringTokenizer(content, "#", true);
210                 // 'tok' splits at the '#' sign, and keeps delimiters
211
212                 while (tok.hasMoreTokens()) {
213                         String str = tok.nextToken();
214                         if (str.equals("#"))
215                                 string = !string;
216                         else {
217                                 if (string) {
218                                         // This part should normally be a string, but if it's
219                                         // a pure number, it is not.
220                                         String s = shaveString(str);
221                                         try {
222                                                 Integer.parseInt(s);
223                                                 // If there's no exception, it's a number.
224                                                 toSet = toSet + s;
225                                         } catch (NumberFormatException ex) {
226                                                 toSet = toSet + "#" + shaveString(str) + "#";
227                                         }
228
229                                 } else
230                                         toSet = toSet + shaveString(str);
231                         }
232                 }
233                 return toSet;
234         }
235
236         public static String shaveString(String s) {
237                 // returns the string, after shaving off whitespace at the beginning
238                 // and end, and removing (at most) one pair of braces or " surrounding
239                 // it.
240                 if (s == null)
241                         return null;
242                 char ch, ch2;
243                 int beg = 0, end = s.length();
244                 // We start out assuming nothing will be removed.
245                 boolean begok = false, endok = false;
246                 while (!begok) {
247                         if (beg < s.length()) {
248                                 ch = s.charAt(beg);
249                                 if (Character.isWhitespace(ch))
250                                         beg++;
251                                 else
252                                         begok = true;
253                         } else
254                                 begok = true;
255
256                 }
257                 while (!endok) {
258                         if (end > beg + 1) {
259                                 ch = s.charAt(end - 1);
260                                 if (Character.isWhitespace(ch))
261                                         end--;
262                                 else
263                                         endok = true;
264                         } else
265                                 endok = true;
266                 }
267
268                 if (end > beg + 1) {
269                         ch = s.charAt(beg);
270                         ch2 = s.charAt(end - 1);
271                         if (((ch == '{') && (ch2 == '}')) || ((ch == '"') && (ch2 == '"'))) {
272                                 beg++;
273                                 end--;
274                         }
275                 }
276                 s = s.substring(beg, end);
277                 // Util.pr(s);
278                 return s;
279         }
280
281         /**
282          * This method returns a String similar to the one passed in, except that it
283          * is molded into a form that is acceptable for bibtex.
284          * 
285          * Watch-out that the returned string might be of length 0 afterwards.
286          * 
287          * @param key
288          *            mayBeNull
289          */
290         public static String checkLegalKey(String key) {
291                 if (key == null)
292                         return null;
293                 StringBuffer newKey = new StringBuffer();
294                 for (int i = 0; i < key.length(); i++) {
295                         char c = key.charAt(i);
296                         if (!Character.isWhitespace(c) && (c != '#') && (c != '{') && (c != '\\') && (c != '"')
297                                 && (c != '}') && (c != '~') && (c != ',') && (c != '^'))
298                                 newKey.append(c);
299                 }
300
301                 // Replace non-english characters like umlauts etc. with a sensible
302                 // letter or letter combination that bibtex can accept.
303                 String newKeyS = replaceSpecialCharacters(newKey.toString());
304
305                 return newKeyS;
306         }
307
308         /**
309          * Replace non-english characters like umlauts etc. with a sensible letter
310          * or letter combination that bibtex can accept. The basis for replacement
311          * is the HashMap GLobals.UNICODE_CHARS.
312          */
313         public static String replaceSpecialCharacters(String s) {
314                 for (Iterator i = Globals.UNICODE_CHARS.keySet().iterator(); i.hasNext();) {
315                         String chr = (String) i.next(), replacer = (String) Globals.UNICODE_CHARS.get(chr);
316                         // pr(chr+" "+replacer);
317                         s = s.replaceAll(chr, replacer);
318                 }
319                 return s;
320         }
321
322         static public String _wrap2(String in, int wrapAmount) {
323                 // The following line cuts out all whitespace and replaces them with
324                 // single
325                 // spaces:
326                 // in = in.replaceAll("[ ]+"," ").replaceAll("[\\t]+"," ");
327                 // StringBuffer out = new StringBuffer(in);
328                 StringBuffer out = new StringBuffer(in.replaceAll("[ \\t\\r]+", " "));
329
330                 int p = in.length() - wrapAmount;
331                 int lastInserted = -1;
332                 while (p > 0) {
333                         p = out.lastIndexOf(" ", p);
334                         if (p <= 0 || p <= 20)
335                                 break;
336                         int lbreak = out.indexOf("\n", p);
337                         System.out.println(lbreak + " " + lastInserted);
338                         if ((lbreak > p) && ((lastInserted >= 0) && (lbreak < lastInserted))) {
339                                 p = lbreak - wrapAmount;
340                         } else {
341                                 out.insert(p, "\n\t");
342                                 lastInserted = p;
343                                 p -= wrapAmount;
344                         }
345                 }
346                 return out.toString();
347         }
348
349         static public String wrap2(String in, int wrapAmount) {
350                 return net.sf.jabref.imports.FieldContentParser.wrap(in, wrapAmount);
351         }
352
353         static public String __wrap2(String in, int wrapAmount) {
354                 // The following line cuts out all whitespace except line breaks, and
355                 // replaces
356                 // with single spaces. Line breaks are padded with a tab character:
357                 StringBuffer out = new StringBuffer(in.replaceAll("[ \\t\\r]+", " "));
358
359                 int p = 0;
360                 // int lastInserted = -1;
361                 while (p < out.length()) {
362                         int q = out.indexOf(" ", p + wrapAmount);
363                         if ((q < 0) || (q >= out.length()))
364                                 break;
365                         int lbreak = out.indexOf("\n", p);
366                         // System.out.println(lbreak);
367                         if ((lbreak > p) && (lbreak < q)) {
368                                 p = lbreak + 1;
369                                 int piv = lbreak + 1;
370                                 if ((out.length() > piv) && !(out.charAt(piv) == '\t'))
371                                         out.insert(piv, "\n\t");
372
373                         } else {
374                                 // System.out.println(q+" "+out.length());
375                                 out.deleteCharAt(q);
376                                 out.insert(q, "\n\t");
377                                 p = q + 1;
378                         }
379                 }
380                 return out.toString();// .replaceAll("\n", "\n\t");
381         }
382
383         public static HashSet findDeliminatedWordsInField(BibtexDatabase db, String field,
384                 String deliminator) {
385                 HashSet res = new HashSet();
386                 Iterator i = db.getKeySet().iterator();
387                 while (i.hasNext()) {
388                         BibtexEntry be = db.getEntryById(i.next().toString());
389                         Object o = be.getField(field);
390                         if (o != null) {
391                                 String fieldValue = o.toString().trim();
392                                 StringTokenizer tok = new StringTokenizer(fieldValue, deliminator);
393                                 while (tok.hasMoreTokens())
394                                         res.add(tok.nextToken().trim());
395                         }
396                 }
397                 return res;
398         }
399
400         /**
401          * Returns a HashMap containing all words used in the database in the given
402          * field type. Characters in
403          * 
404          * @param remove
405          *            are not included.
406          * @param db
407          *            a <code>BibtexDatabase</code> value
408          * @param field
409          *            a <code>String</code> value
410          * @param remove
411          *            a <code>String</code> value
412          * @return a <code>HashSet</code> value
413          */
414         public static HashSet findAllWordsInField(BibtexDatabase db, String field, String remove) {
415                 HashSet res = new HashSet();
416                 StringTokenizer tok;
417                 Iterator i = db.getKeySet().iterator();
418                 while (i.hasNext()) {
419                         BibtexEntry be = db.getEntryById(i.next().toString());
420                         Object o = be.getField(field);
421                         if (o != null) {
422                                 tok = new StringTokenizer(o.toString(), remove, false);
423                                 while (tok.hasMoreTokens())
424                                         res.add(tok.nextToken());
425                         }
426                 }
427                 return res;
428         }
429
430         /**
431          * Takes a String array and returns a string with the array's elements
432          * delimited by a certain String.
433          * 
434          * @param strs
435          *            String array to convert.
436          * @param delimiter
437          *            String to use as delimiter.
438          * @return Delimited String.
439          */
440         public static String stringArrayToDelimited(String[] strs, String delimiter) {
441                 if ((strs == null) || (strs.length == 0))
442                         return "";
443                 if (strs.length == 1)
444                         return strs[0];
445                 StringBuffer sb = new StringBuffer();
446                 for (int i = 0; i < strs.length - 1; i++) {
447                         sb.append(strs[i]);
448                         sb.append(delimiter);
449                 }
450                 sb.append(strs[strs.length - 1]);
451                 return sb.toString();
452         }
453
454         /**
455          * Takes a delimited string, splits it and returns
456          * 
457          * @param names
458          *            a <code>String</code> value
459          * @return a <code>String[]</code> value
460          */
461         public static String[] delimToStringArray(String names, String delimiter) {
462                 if (names == null)
463                         return null;
464                 return names.split(delimiter);
465         }
466
467         /**
468          * Open a http/pdf/ps viewer for the given link string.
469          */
470         public static void openExternalViewer(MetaData metaData, String link, String fieldName)
471                 throws IOException {
472
473                 if (fieldName.equals("ps") || fieldName.equals("pdf")) {
474
475                         // Find the default directory for this field type:
476                         String dir = metaData.getFileDirectory(fieldName);
477                         
478                         File file = expandFilename(link, new String[]{dir, "."});
479
480                         // Check that the file exists:
481                         if ((file == null) || !file.exists()) {
482                                 throw new IOException(Globals.lang("File not found") + " (" + fieldName + "): '"
483                                         + link + "'.");
484                         }
485                         link = file.getCanonicalPath();
486
487                         // Use the correct viewer even if pdf and ps are mixed up:
488                         String[] split = file.getName().split("\\.");
489                         if (split.length >= 2) {
490                                 if (split[split.length - 1].equalsIgnoreCase("pdf"))
491                                         fieldName = "pdf";
492                                 else if (split[split.length - 1].equalsIgnoreCase("ps")
493                                         || (split.length >= 3 && split[split.length - 2].equalsIgnoreCase("ps")))
494                                         fieldName = "ps";
495                         }
496                 } else if (fieldName.equals("doi")) {
497                         fieldName = "url";
498                         // Check to see if link field already contains a well formated URL
499                         if (!link.startsWith("http://")) {
500                                 link = Globals.DOI_LOOKUP_PREFIX + link;
501                         }
502                 } else if (fieldName.equals("citeseerurl")) {
503                         fieldName = "url";
504
505                         String canonicalLink = CiteSeerFetcher.generateCanonicalURL(link);
506                         if (canonicalLink != null)
507                                 link = canonicalLink;
508                 }
509
510                 String cmdArray[] = new String[2];
511                 if (fieldName.equals("url")) { // html
512                         try {
513
514                                 // First check if the url is enclosed in \\url{}. If so, remove
515                                 // the wrapper.
516                                 if (link.startsWith("\\url{") && link.endsWith("}"))
517                                         link = link.substring(5, link.length() - 1);
518
519                                 link = sanitizeUrl(link);
520
521                                 if (Globals.ON_MAC) {
522                                         String[] cmd = { "/usr/bin/open", "-a", Globals.prefs.get("htmlviewer"), link };
523                                         Process child = Runtime.getRuntime().exec(cmd);
524                                 } else if (Globals.ON_WIN) {
525                                         openFileOnWindows(link, false);
526                                         /*
527                                          * cmdArray[0] = Globals.prefs.get("htmlviewer");
528                                          * cmdArray[1] = link; Process child =
529                                          * Runtime.getRuntime().exec( cmdArray[0] + " " +
530                                          * cmdArray[1]);
531                                          */
532                                 } else {
533                                         cmdArray[0] = Globals.prefs.get("htmlviewer");
534                                         cmdArray[1] = link;
535                                         Process child = Runtime.getRuntime().exec(cmdArray);
536                                 }
537
538                         } catch (IOException e) {
539                                 System.err.println("An error occured on the command: "
540                                         + Globals.prefs.get("htmlviewer") + " " + link);
541                         } catch (URISyntaxException e2) {
542                                 e2.printStackTrace();
543                         }
544                 } else if (fieldName.equals("ps")) {
545                         try {
546                                 if (Globals.ON_MAC) {
547                                         String[] cmd = { "/usr/bin/open", "-a", Globals.prefs.get("psviewer"), link };
548                                         Process child = Runtime.getRuntime().exec(cmd);
549                                 } else if (Globals.ON_WIN) {
550                                         openFileOnWindows(link, true);
551                                         /*
552                                          * cmdArray[0] = Globals.prefs.get("psviewer"); cmdArray[1] =
553                                          * link; Process child = Runtime.getRuntime().exec(
554                                          * cmdArray[0] + " " + cmdArray[1]);
555                                          */
556                                 } else {
557                                         cmdArray[0] = Globals.prefs.get("psviewer");
558                                         cmdArray[1] = link;
559                                         Process child = Runtime.getRuntime().exec(cmdArray);
560                                 }
561                         } catch (IOException e) {
562                                 System.err.println("An error occured on the command: "
563                                         + Globals.prefs.get("psviewer") + " " + link);
564                         }
565                 } else if (fieldName.equals("pdf")) {
566                         try {
567                                 if (Globals.ON_MAC) {
568                                         String[] cmd = { "/usr/bin/open", "-a", Globals.prefs.get("pdfviewer"), link };
569                                         Process child = Runtime.getRuntime().exec(cmd);
570                                 } else if (Globals.ON_WIN) {
571                                         openFileOnWindows(link, true);
572                                         /*
573                                          * String[] spl = link.split("\\\\"); StringBuffer sb = new
574                                          * StringBuffer(); for (int i = 0; i < spl.length; i++) { if
575                                          * (i > 0) sb.append("\\"); if (spl[i].indexOf(" ") >= 0)
576                                          * spl[i] = "\"" + spl[i] + "\""; sb.append(spl[i]);
577                                          *  } //pr(sb.toString()); link = sb.toString();
578                                          * 
579                                          * String cmd = "cmd.exe /c start " + link;
580                                          * 
581                                          * Process child = Runtime.getRuntime().exec(cmd);
582                                          */
583                                 } else {
584                                         cmdArray[0] = Globals.prefs.get("pdfviewer");
585                                         cmdArray[1] = link;
586                                         // Process child = Runtime.getRuntime().exec(cmdArray[0]+"
587                                         // "+cmdArray[1]);
588                                         Process child = Runtime.getRuntime().exec(cmdArray);
589                                 }
590                         } catch (IOException e) {
591                                 e.printStackTrace();
592                                 System.err.println("An error occured on the command: "
593                                         + Globals.prefs.get("pdfviewer") + " #" + link);
594                                 System.err.println(e.getMessage());
595                         }
596                 } else {
597                         System.err
598                                 .println("Message: currently only PDF, PS and HTML files can be opened by double clicking");
599                         // ignore
600                 }
601         }
602
603         /**
604          * Opens a file on a Windows system, using its default viewer.
605          * 
606          * @param link
607          *            The file name.
608          * @param localFile
609          *            true if it is a local file, not an URL.
610          * @throws IOException
611          */
612         public static void openFileOnWindows(String link, boolean localFile) throws IOException {
613                 /*
614                  * if (localFile) { String[] spl = link.split("\\\\"); StringBuffer sb =
615                  * new StringBuffer(); for (int i = 0; i < spl.length; i++) { if (i > 0)
616                  * sb.append("\\"); if (spl[i].indexOf(" ") >= 0) spl[i] = "\"" + spl[i] +
617                  * "\""; sb.append(spl[i]); } link = sb.toString(); }
618                  */
619                 link = link.replaceAll("&", "\"&\"").replaceAll(" ", "\" \"");
620
621                 // Bug fix for:
622                 // http://sourceforge.net/tracker/index.php?func=detail&aid=1489454&group_id=92314&atid=600306
623                 String cmd;
624                 if (Globals.osName.startsWith("Windows 9")) {
625                         cmd = "command.com /c start " + link;
626                 } else {
627                         cmd = "cmd.exe /c start " + link;
628                 }
629                 Process child = Runtime.getRuntime().exec(cmd);
630         }
631
632         /**
633          * Open an external file, attempting to use the correct viewer for it.
634          * 
635          * @param metaData
636          *            The MetaData for the database this file belongs to.
637          * @param link
638          *            The file name.
639          */
640         public static void openExternalFileAnyFormat(MetaData metaData, String link) throws IOException {
641
642                 // For other platforms we'll try to find the file type:
643                 File file = new File(link);
644
645                 // We try to check the extension for the file:
646                 String name = file.getName();
647                 int pos = name.indexOf('.');
648                 String extension = ((pos >= 0) && (pos < name.length() - 1)) ? name.substring(pos + 1)
649                         .trim().toLowerCase() : null;
650
651                 /*
652                  * if ((extension == null) || (extension.length() == 0)) { // No
653                  * extension. What to do? throw new IOException(Globals.lang("No file
654                  * extension. Could not find viewer for file.")); }
655                  */
656
657                 // Now we know the extension, check if it is one we know about:
658                 ExternalFileType fileType = Globals.prefs.getExternalFileType(extension);
659
660                 // Find the default directory for this field type, if any:
661                 String dir = metaData.getFileDirectory(extension);
662                 if (dir != null) {
663                         File tmp = expandFilename(link, new String[]{dir, "."});
664                         if (tmp != null)
665                                 file = tmp;
666                 }
667
668                 // Check if we have arrived at an existing file:
669                 if (file.exists() && (fileType != null)) {
670                         // Open the file:
671                         try {
672                                 if (Globals.ON_MAC) {
673                                         String[] cmd = { "/usr/bin/open", "-a", fileType.getOpenWith(), file.getPath() };
674                                         Runtime.getRuntime().exec(cmd);
675                                 } else if (Globals.ON_WIN) {
676                                         openFileOnWindows(file.getPath(), true);
677                                 } else {
678                                         String[] cmdArray = new String[] { fileType.getOpenWith(), file.getPath() };
679                                         Runtime.getRuntime().exec(cmdArray);
680                                 }
681                         } catch (IOException e) {
682                                 e.printStackTrace();
683                                 System.err.println("An error occured on the command: " + fileType.getOpenWith()
684                                         + " #" + link);
685                                 System.err.println(e.getMessage());
686                         }
687
688                 } else {
689                         // No file matched the name, or we didn't know the file type.
690                         // Perhaps it is an URL thing.
691
692                         // First check if it is enclosed in \\url{}. If so, remove
693                         // the wrapper.
694                         if (link.startsWith("\\url{") && link.endsWith("}"))
695                                 link = link.substring(5, link.length() - 1);
696
697                         if (link.startsWith("doi:"))
698                                 link = Globals.DOI_LOOKUP_PREFIX + link;
699
700                         try {
701                                 link = sanitizeUrl(link);
702                         } catch (URISyntaxException ex) {
703                                 ex.printStackTrace();
704                         }
705
706                         if (Globals.ON_MAC) {
707                                 String[] cmd = { "/usr/bin/open", "-a", Globals.prefs.get("htmlviewer"), link };
708                                 Runtime.getRuntime().exec(cmd);
709                         } else if (Globals.ON_WIN) {
710                                 openFileOnWindows(link, false);
711                         } else {
712                                 String[] cmdArray = new String[] { Globals.prefs.get("htmlviewer"), link };
713                                 Runtime.getRuntime().exec(cmdArray);
714                         }
715
716                 }
717         }
718
719         /**
720          * Make sure an URL is "portable", in that it doesn't contain bad characters
721          * that break the open command in some OSes.
722          * 
723          * @param link
724          *            The URL to sanitize.
725          * @return Sanitized URL
726          */
727         public static String sanitizeUrl(String link) throws URISyntaxException {
728                 String scheme = "http";
729                 String ssp;
730                 if (link.indexOf("//") > 0)
731                         ssp = "//" + link.substring(2 + link.indexOf("//"));
732                 else
733                         ssp = "//" + link;
734
735         // The following is an ugly hack to fix the problem where a %20 in the
736         // original string (correct encoding of a space) gets returned as %2520
737         // because the URI constructor doesn't recognize that the % is the start of
738         // a sequence. The problem is that other such sequences will meet the same fate.
739         // If we need to take care of all these up front, we might as well not use URI
740         // at all.
741         // Another option is to do two times, hiding the %xx sequences in one of them,
742         // and see if one of the URLs looks good...
743         ssp = ssp.replaceAll("%20", " ");
744
745         URI uri = new URI(scheme, ssp, null);
746                 return uri.toASCIIString();
747         }
748
749         /**
750          * Searches the given directory and subdirectories for a pdf file with name
751          * as given + ".pdf"
752          */
753         public static String findPdf(String key, String extension, String directory, OpenFileFilter off) {
754                 // String filename = key + "."+extension;
755
756                 /*
757                  * Simon Fischer's patch for replacing a regexp in keys before
758                  * converting to filename:
759                  * 
760                  * String regex = Globals.prefs.get("basenamePatternRegex"); if ((regex !=
761                  * null) && (regex.trim().length() > 0)) { String replacement =
762                  * Globals.prefs.get("basenamePatternReplacement"); key =
763                  * key.replaceAll(regex, replacement); }
764                  */
765                 if (!directory.endsWith(System.getProperty("file.separator")))
766                         directory += System.getProperty("file.separator");
767                 String found = findInDir(key, directory, off);
768                 if (found != null)
769                         return found.substring(directory.length());
770                 else
771                         return null;
772         }
773
774         /**
775          * New version of findPdf that uses findFiles.
776          * 
777          * The search pattern will be read from the preferences.
778          * 
779          * The [extension]-tags in this pattern will be replace by the given extension parameter.
780          * 
781          */
782         public static String findPdf(BibtexEntry entry, String extension, String directory) {
783                 return findPdf(entry, extension, new String[]{directory});
784         }
785
786         /**
787          * Convenience method for findPDF. Can search multiple PDF directories.
788          */
789         public static String findPdf(BibtexEntry entry, String extension, String[] directories) {
790
791                 String regularExpression;
792                 if (Globals.prefs.getBoolean(JabRefPreferences.USE_REG_EXP_SEARCH_KEY)){
793                         regularExpression = Globals.prefs.get(JabRefPreferences.REG_EXP_SEARCH_EXPRESSION_KEY);
794                 } else {
795                         regularExpression = Globals.prefs.get(JabRefPreferences.DEFAULT_REG_EXP_SEARCH_EXPRESSION_KEY);
796                 }
797                 regularExpression = regularExpression.replaceAll("\\[extension\\]", extension);
798                 
799                 return findFile(entry, null, directories, regularExpression, true);
800         }
801         
802         
803         /**
804          * Searches the given directory and file name pattern for a file for the
805          * bibtexentry.
806          * 
807          * Used to fix:
808          * 
809          * http://sourceforge.net/tracker/index.php?func=detail&aid=1503410&group_id=92314&atid=600309
810          * 
811          * Requirements:
812          * 
813          *  - Be able to find the associated PDF in a set of given directories.
814          *  
815          *  - Be able to return a relative path or absolute path. 
816          *
817          *  - Be fast.
818          *  
819          *  - Allow for flexible naming schemes in the PDFs.
820          * 
821          * Syntax scheme for file:
822          * <ul>
823          *   <li>* Any subDir</li>
824          *   <li>** Any subDir (recursiv)</li>
825          *   <li>[key] Key from bibtex file and database</li> 
826          *   <li>.* Anything else is taken to be a Regular expression.</li>
827          * </ul>
828          * 
829          * @param entry
830          *            non-null
831          * @param database
832          *            non-null
833          * @param directory
834          *                              A set of root directories to start the search from.
835          *                              Paths are returned relative to these directories if
836          *                              relative is set to true. These directories will not 
837          *                              be expanded or anything. Use the file attribute for 
838          *                              this.
839          * @param file
840          *            non-null
841          *            
842          * @param relative
843          *            whether to return relative file paths or absolute ones
844          * 
845          * @return Will return the first file found to match the given criteria or
846          *         null if none was found.
847          */
848         public static String findFile(BibtexEntry entry, BibtexDatabase database, String[] directory,
849                 String file, boolean relative) {
850
851                 for (int i = 0; i < directory.length; i++){
852                 
853                         String result = findFile(entry, database, directory[i], file, relative);
854                         if (result != null){
855                                 return result;
856                         }
857                 }
858                 return null;
859         }
860
861         /**
862          * Removes optional square brackets from the string s
863          * 
864          * @param s
865          * @return
866          */
867         public static String stripBrackets(String s) {
868                 int beginIndex = (s.startsWith("[") ? 1 : 0);
869                 int endIndex = (s.endsWith("]") ? s.length() - 1 : s.length());
870                 return s.substring(beginIndex, endIndex);
871         }
872
873         public static ArrayList parseMethodsCalls(String calls) throws RuntimeException {
874                 
875                 ArrayList result = new ArrayList();
876                 
877                 char[] c = calls.toCharArray();
878                 
879                 int i = 0;
880                 
881                 while (i < c.length){
882                 
883                         int start = i;
884                         if (Character.isJavaIdentifierStart(c[i])){
885                                 i++;
886                                 while (i < c.length && (Character.isJavaIdentifierPart(c[i]) || c[i] == '.')){
887                                         i++;
888                                 }
889                                 if (i < c.length && c[i] == '('){
890                                         
891                                         String method = calls.substring(start, i);
892                                         
893                                         i++; i++; 
894                                         
895                                         int startParam = i;
896                                         i++;
897                                         
898                                         while (i + 1 < c.length && !(c[i] == '"' && c[i+1] == ')')){
899                                                 i++;
900                                         }
901                                         
902                                         String param = calls.substring(startParam, i);
903                                         
904                                         result.add(new String[]{method, param});
905                                 } else {
906                                         String method = calls.substring(start, i);
907                                         result.add(new String[]{method});
908                                 }
909                         }
910                         i++;
911                 }
912                 
913                 return result;
914         }
915         
916         
917         /**
918          * Accepts a string like [author:toLowerCase("escapedstring"),toUpperCase], whereas the first
919          * string signifies the bibtex-field to get while the others are the names
920          * of layouters that will be applied.
921          * 
922          * @param fieldAndFormat
923          * @param entry
924          * @param database
925          * @return
926          */
927         public static String getFieldAndFormat(String fieldAndFormat, BibtexEntry entry,
928                 BibtexDatabase database) {
929
930                 fieldAndFormat = stripBrackets(fieldAndFormat);
931
932                 int colon = fieldAndFormat.indexOf(':');
933                 
934                 String beforeColon, afterColon;
935                 if (colon == -1){
936                         beforeColon = fieldAndFormat;
937                         afterColon = null;
938                 } else {
939                         beforeColon = fieldAndFormat.substring(0, colon);
940                         afterColon = fieldAndFormat.substring(colon+1);
941                 }
942                 beforeColon = beforeColon.trim();
943                 
944                 if (beforeColon.length() == 0){
945                         return null;
946                 }
947                 
948                 String fieldValue = getField(beforeColon, entry, database);
949
950                 if (fieldValue == null)
951                         return null;
952
953                 if (afterColon == null || afterColon.length() == 0)
954                         return fieldValue;
955
956                 try {
957                         LayoutFormatter[] formatters = LayoutEntry.getOptionalLayout(afterColon, "");
958                         for (int i = 0; i < formatters.length; i++) {
959                                 fieldValue = formatters[i].format(fieldValue);
960                         }
961                 } catch (Exception e) {
962                         throw new RuntimeException(e);
963                 }
964
965                 return fieldValue;
966         }
967
968         public static String getField(String field, BibtexEntry bibtex, BibtexDatabase database) {
969
970                 if (field.equals("bibtextype"))
971                         return bibtex.getType().getName();
972
973                 String res = (String) bibtex.getField(field);
974
975                 if ((res != null) && (database != null))
976                         res = database.resolveForStrings(res);
977
978                 return res;
979         }
980
981         /**
982          * Convenience function for absolute search.
983      *
984      * Uses findFile(BibtexEntry, BibtexDatabase, (String)null, String, false).
985          */
986         public static String findFile(BibtexEntry entry, BibtexDatabase database, String file){
987                 return findFile(entry, database, (String)null, file, false);
988         }       
989         
990         /**
991          * Internal Version of findFile, which also accepts a current directory to
992          * base the search on.
993          * 
994          */
995         public static String findFile(BibtexEntry entry,
996                 BibtexDatabase database, String directory, String file, boolean relative) {
997
998                 File root;
999                 if (directory == null){
1000                         root = new File(".");
1001                 } else {
1002                         root = new File(directory);
1003                 }
1004                 if (!root.exists())
1005                         return null;
1006         
1007                 String found = findFile(entry, database, root, file);
1008                 
1009                 if (directory == null || !relative){
1010                         return found;
1011                 }
1012                 
1013                 if (found != null){
1014                         try {
1015                                 /**
1016                                  * [ 1601651 ] PDF subdirectory - missing first character
1017                                  * 
1018                                  * http://sourceforge.net/tracker/index.php?func=detail&aid=1601651&group_id=92314&atid=600306
1019                                  */
1020                                 return found.substring(root.getCanonicalPath().length());
1021                         } catch (IOException e) {
1022                                 return null;
1023                         }
1024                 }
1025                 return null;
1026         }
1027         
1028         /**
1029          * The actual work-horse. Will find absolute filepaths starting from the given directory using the given regular expression string for search.
1030          */
1031         protected static String findFile(BibtexEntry entry,
1032                 BibtexDatabase database, File directory, String file) {
1033
1034                 if (file.startsWith("/")){
1035                         directory = new File(".");
1036                         file = file.substring(1);
1037                 }
1038                 
1039                 // Escape handling...
1040                 Matcher m = Pattern.compile("([^\\\\])\\\\([^\\\\])").matcher(file);
1041                 StringBuffer s = new StringBuffer();
1042                 while (m.find()) {
1043                         m.appendReplacement(s, m.group(1) + "/" + m.group(2));
1044                 }
1045                 m.appendTail(s);
1046                 file = s.toString();
1047                 String[] fileParts = file.split("/");
1048                 
1049                 if (fileParts.length == 0)
1050                         return null;
1051                 
1052                 if (fileParts.length > 1){
1053
1054                         for (int i = 0; i < fileParts.length - 1; i++) {
1055
1056                                 String dirToProcess = fileParts[i];     
1057                                 
1058                                 dirToProcess = expandBrackets(dirToProcess, entry, database);
1059
1060                                 if (dirToProcess.matches("^.:$")) { // Windows Drive Letter
1061                                         directory = new File(dirToProcess + "/");
1062                                         continue;
1063                                 }
1064                                 if (dirToProcess.equals(".")) { // Stay in current directory
1065                                         continue;
1066                                 }
1067                                 if (dirToProcess.equals("..")) {
1068                                         directory = new File(directory.getParent());
1069                                         continue;
1070                                 }
1071                                 if (dirToProcess.equals("*")) { // Do for all direct subdirs
1072
1073                                         File[] subDirs = directory.listFiles();
1074                                         if (subDirs == null)
1075                                                 return null; // No permission?
1076
1077                                         String restOfFileString = join(fileParts, "/", i + 1, fileParts.length);
1078
1079                                         for (int sub = 0; sub < subDirs.length; sub++) {
1080                                                 if (subDirs[sub].isDirectory()) {
1081                                                         String result = findFile(entry, database,
1082                                                                 subDirs[sub], restOfFileString);
1083                                                         if (result != null)
1084                                                                 return result;
1085                                                 }
1086                                         }
1087                                         return null;
1088                                 }
1089                                 // Do for all direct and indirect subdirs
1090                                 if (dirToProcess.equals("**")) {
1091                                         List toDo = new LinkedList();
1092                                         toDo.add(directory);
1093
1094                                         String restOfFileString = join(fileParts, "/", i + 1, fileParts.length);
1095
1096                                         // Before checking the subdirs, we first check the current
1097                                         // dir
1098                                         String result = findFile(entry, database, directory, restOfFileString);
1099                                         if (result != null)
1100                                                 return result;
1101
1102                                         while (!toDo.isEmpty()) {
1103
1104                                                 // Get all subdirs of each of the elements found in toDo
1105                                                 File[] subDirs = ((File) toDo.remove(0)).listFiles();
1106                                                 if (subDirs == null) // No permission?
1107                                                         continue;
1108
1109                                                 toDo.addAll(Arrays.asList(subDirs));
1110
1111                                                 for (int sub = 0; sub < subDirs.length; sub++) {
1112                                                         if (!subDirs[sub].isDirectory())
1113                                                                 continue;
1114                                                         result = findFile(entry, database, subDirs[sub], restOfFileString);
1115                                                         if (result != null)
1116                                                                 return result;
1117                                                 }
1118                                         }
1119                                         // We already did the currentDirectory
1120                                         return null;
1121                                 }
1122
1123                                 final Pattern toMatch = Pattern.compile(dirToProcess.replaceAll("\\\\\\\\", "\\\\"));
1124
1125                                 File[] matches = directory.listFiles(new FilenameFilter() {
1126                                         public boolean accept(File arg0, String arg1) {
1127                                                 return toMatch.matcher(arg1).matches();
1128                                         }
1129                                 });
1130                                 if (matches == null || matches.length == 0)
1131                                         return null;
1132
1133                                 directory = matches[0];
1134
1135                                 if (!directory.exists())
1136                                         return null;
1137
1138                         } // End process directory information
1139                 }
1140                 // Last step check if the given file can be found in this directory
1141                 String filenameToLookFor = expandBrackets(fileParts[fileParts.length - 1], entry, database);
1142
1143                 final Pattern toMatch = Pattern.compile("^" + filenameToLookFor.replaceAll("\\\\\\\\", "\\\\") + "$");
1144
1145                 File[] matches = directory.listFiles(new FilenameFilter() {
1146                         public boolean accept(File arg0, String arg1) {
1147                                 return toMatch.matcher(arg1).matches();
1148                         }
1149                 });
1150                 if (matches == null || matches.length == 0)
1151                         return null;
1152
1153                 try {
1154                         return matches[0].getCanonicalPath();
1155                 } catch (IOException e) {
1156                         return null;
1157                 }
1158         }
1159
1160         static Pattern squareBracketsPattern = Pattern.compile("\\[.*?\\]");
1161
1162         /**
1163          * Takes a string that contains bracketed expression and expands each of
1164          * these using getFieldAndFormat.
1165          * 
1166          * Unknown Bracket expressions are silently dropped.
1167          * 
1168          * @param bracketString
1169          * @param entry
1170          * @param database
1171          * @return
1172          */
1173         public static String expandBrackets(String bracketString, BibtexEntry entry,
1174                 BibtexDatabase database) {
1175                 Matcher m = squareBracketsPattern.matcher(bracketString);
1176                 StringBuffer s = new StringBuffer();
1177                 while (m.find()) {
1178                         String replacement = getFieldAndFormat(m.group(), entry, database);
1179                         if (replacement == null)
1180                                 replacement = "";
1181                         m.appendReplacement(s, replacement);
1182                 }
1183                 m.appendTail(s);
1184
1185                 return s.toString();
1186         }
1187
1188         /**
1189          * Concatenate all strings in the array from index 'from' to 'to' (excluding
1190          * to) with the given separator.
1191          * 
1192          * Example:
1193          * 
1194          * String[] s = "ab/cd/ed".split("/"); join(s, "\\", 0, s.length) ->
1195          * "ab\\cd\\ed"
1196          * 
1197          * @param strings
1198          * @param separator
1199          * @param from
1200          * @param to
1201          *            Excluding strings[to]
1202          * @return
1203          */
1204         public static String join(String[] strings, String separator, int from, int to) {
1205                 if (strings.length == 0 || from >= to)
1206                         return "";
1207
1208                 StringBuffer sb = new StringBuffer();
1209                 for (int i = from; i < to - 1; i++) {
1210                         sb.append(strings[i]).append(separator);
1211                 }
1212                 return sb.append(strings[to - 1]).toString();
1213         }
1214
1215         /**
1216          * Converts a relative filename to an absolute one, if necessary. Returns
1217          * null if the file does not exist.
1218          * 
1219          * Will look in each of the given dirs starting from the beginning and returning the first found file to match if any.
1220          */
1221         public static File expandFilename(String name, String[] dir) {
1222                 
1223                 for (int i = 0; i < dir.length; i++) {
1224                         File result = expandFilename(name, dir[i]);
1225                         if (result != null){
1226                                 return result;
1227                         }
1228                 }
1229         
1230                 return null;
1231         }
1232         
1233         /**
1234          * Converts a relative filename to an absolute one, if necessary. Returns
1235          * null if the file does not exist.
1236          */
1237         public static File expandFilename(String name, String dir) {
1238                 // System.out.println("expandFilename: name="+name+"\t dir="+dir);
1239                 File file = null;
1240                 if (name == null || name.length() == 0)
1241                         return null;
1242                 else {
1243                         file = new File(name);
1244                 }
1245
1246                 if (file != null) {
1247                         if (!file.exists() && (dir != null)) {
1248                                 if (dir.endsWith(System.getProperty("file.separator")))
1249                                         name = dir + name;
1250                                 else
1251                                         name = dir + System.getProperty("file.separator") + name;
1252
1253                                 // System.out.println("expanded to: "+name);
1254                                 // if (name.startsWith("ftp"))
1255
1256                                 file = new File(name);
1257                                 if (file.exists())
1258                                         return file;
1259                                 // Ok, try to fix / and \ problems:
1260                                 if (Globals.ON_WIN) {
1261                                         // workaround for catching Java bug in regexp replacer
1262                                         // and, why, why, why ... I don't get it - wegner 2006/01/22
1263                                         try {
1264                                                 name = name.replaceAll("/", "\\\\");
1265                                         } catch (java.lang.StringIndexOutOfBoundsException exc) {
1266                                                 System.err.println("An internal Java error was caused by the entry " + "\""
1267                                                         + name + "\"");
1268                                         }
1269                                 } else
1270                                         name = name.replaceAll("\\\\", "/");
1271                                 // System.out.println("expandFilename: "+name);
1272                                 file = new File(name);
1273                                 if (!file.exists())
1274                                         file = null;
1275                         }
1276                 }
1277                 return file;
1278         }
1279
1280         private static String findInDir(String key, String dir, OpenFileFilter off) {
1281                 File f = new File(dir);
1282                 File[] all = f.listFiles();
1283                 if (all == null)
1284                         return null; // An error occured. We may not have
1285                 // permission to list the files.
1286
1287                 int numFiles = all.length;
1288
1289                 for (int i = 0; i < numFiles; i++) {
1290                         File curFile = all[i];
1291
1292                         if (curFile.isFile()) {
1293                                 String name = curFile.getName();
1294                                 if (name.startsWith(key + ".") && off.accept(name))
1295                                         return curFile.getPath();
1296
1297                         } else if (curFile.isDirectory()) {
1298                                 String found = findInDir(key, curFile.getPath(), off);
1299                                 if (found != null)
1300                                         return found;
1301                         }
1302                 }
1303                 return null;
1304         }
1305
1306         /**
1307          * Checks if the two entries represent the same publication.
1308          * 
1309          * @param one
1310          *            BibtexEntry
1311          * @param two
1312          *            BibtexEntry
1313          * @return boolean
1314          */
1315         public static boolean isDuplicate(BibtexEntry one, BibtexEntry two, float threshold) {
1316
1317                 // First check if they are of the same type - a necessary condition:
1318                 if (one.getType() != two.getType())
1319                         return false;
1320
1321                 // The check if they have the same required fields:
1322                 String[] fields = one.getType().getRequiredFields();
1323
1324                 if (fields == null)
1325                         return false;
1326
1327                 float req = compareFieldSet(fields, one, two);
1328                 fields = one.getType().getOptionalFields();
1329
1330                 if (fields != null) {
1331                         float opt = compareFieldSet(fields, one, two);
1332                         return (2 * req + opt) / 3 >= threshold;
1333                 } else {
1334                         return (req >= threshold);
1335                 }
1336         }
1337
1338         /**
1339          * Goes through all entries in the given database, and if at least one of
1340          * them is a duplicate of the given entry, as per
1341          * Util.isDuplicate(BibtexEntry, BibtexEntry), the duplicate is returned.
1342          * The search is terminated when the first duplicate is found.
1343          * 
1344          * @param database
1345          *            The database to search.
1346          * @param entry
1347          *            The entry of which we are looking for duplicates.
1348          * @return The first duplicate entry found. null if no duplicates are found.
1349          */
1350         public static BibtexEntry containsDuplicate(BibtexDatabase database, BibtexEntry entry) {
1351                 Collection entries = database.getEntries();
1352                 for (Iterator i = entries.iterator(); i.hasNext();) {
1353                         BibtexEntry other = (BibtexEntry) i.next();
1354                         if (isDuplicate(entry, other, Globals.duplicateThreshold))
1355                                 return other; // Duplicate found.
1356                 }
1357                 return null; // No duplicate found.
1358         }
1359
1360         private static float compareFieldSet(String[] fields, BibtexEntry one, BibtexEntry two) {
1361                 int res = 0;
1362                 for (int i = 0; i < fields.length; i++) {
1363                         // Util.pr(":"+compareSingleField(fields[i], one, two));
1364                         if (compareSingleField(fields[i], one, two) == EQUAL) {
1365                                 res++;
1366                                 // Util.pr(fields[i]);
1367                         }
1368                 }
1369                 return ((float) res) / ((float) fields.length);
1370         }
1371
1372         private static int compareSingleField(String field, BibtexEntry one, BibtexEntry two) {
1373                 String s1 = (String) one.getField(field), s2 = (String) two.getField(field);
1374                 if (s1 == null) {
1375                         if (s2 == null)
1376                                 return EQUAL;
1377                         else
1378                                 return EMPTY_IN_ONE;
1379                 } else if (s2 == null)
1380                         return EMPTY_IN_TWO;
1381                 s1 = s1.toLowerCase();
1382                 s2 = s2.toLowerCase();
1383                 // Util.pr(field+": '"+s1+"' vs '"+s2+"'");
1384                 if (field.equals("author") || field.equals("editor")) {
1385                         // Specific for name fields.
1386                         // Harmonise case:
1387                         String[] aus1 = AuthorList.fixAuthor_lastNameFirst(s1).split(" and "), aus2 = AuthorList
1388                                 .fixAuthor_lastNameFirst(s2).split(" and "), au1 = aus1[0].split(","), au2 = aus2[0]
1389                                 .split(",");
1390
1391                         // Can check number of authors, all authors or only the first.
1392                         if ((aus1.length > 0) && (aus1.length == aus2.length)
1393                                 && au1[0].trim().equals(au2[0].trim()))
1394                                 return EQUAL;
1395                         else
1396                                 return NOT_EQUAL;
1397                 } else {
1398                         if (s1.trim().equals(s2.trim()))
1399                                 return EQUAL;
1400                         else
1401                                 return NOT_EQUAL;
1402                 }
1403
1404         }
1405
1406         public static double compareEntriesStrictly(BibtexEntry one, BibtexEntry two) {
1407                 HashSet allFields = new HashSet();// one.getAllFields());
1408                 Object[] o = one.getAllFields();
1409                 for (int i = 0; i < o.length; i++)
1410                         allFields.add(o[i]);
1411                 o = two.getAllFields();
1412                 for (int i = 0; i < o.length; i++)
1413                         allFields.add(o[i]);
1414                 int score = 0;
1415                 for (Iterator fld = allFields.iterator(); fld.hasNext();) {
1416                         String field = (String) fld.next();
1417                         Object en = one.getField(field), to = two.getField(field);
1418                         if ((en != null) && (to != null) && (en.equals(to)))
1419                                 score++;
1420                         else if ((en == null) && (to == null))
1421                                 score++;
1422                 }
1423                 if (score == allFields.size())
1424                         return 1.01; // Just to make sure we can
1425                 // use score>1 without
1426                 // trouble.
1427                 else
1428                         return ((double) score) / allFields.size();
1429         }
1430
1431         /**
1432          * This methods assures all words in the given entry are recorded in their
1433          * respective Completers, if any.
1434          */
1435         /*
1436          * public static void updateCompletersForEntry(Hashtable autoCompleters,
1437          * BibtexEntry be) {
1438          * 
1439          * for (Iterator j=autoCompleters.keySet().iterator(); j.hasNext();) {
1440          * String field = (String)j.next(); Completer comp =
1441          * (Completer)autoCompleters.get(field); comp.addAll(be.getField(field)); } }
1442          */
1443
1444         /**
1445          * Sets empty or non-existing owner fields of bibtex entries inside a List
1446          * to a specified default value. Timestamp field is also set. Preferences
1447          * are checked to see if these options are enabled.
1448          * 
1449          * @param bibs
1450          *            List of bibtex entries
1451          */
1452         public static void setAutomaticFields(List bibs) {
1453                 String defaultOwner = Globals.prefs.get("defaultOwner");
1454                 String timestamp = easyDateFormat();
1455                 boolean setOwner = Globals.prefs.getBoolean("useOwner"), setTimeStamp = Globals.prefs
1456                         .getBoolean("useTimeStamp");
1457                 String timeStampField = Globals.prefs.get("timeStampField");
1458                 // Iterate through all entries
1459                 for (int i = 0; i < bibs.size(); i++) {
1460                         // Get current entry
1461                         BibtexEntry curEntry = (BibtexEntry) bibs.get(i);
1462                         setAutomaticFields(curEntry, setOwner, defaultOwner, setTimeStamp, timeStampField,
1463                                 timestamp);
1464
1465                 }
1466
1467         }
1468
1469         /**
1470          * Sets empty or non-existing owner fields of a bibtex entry to a specified
1471          * default value. Timestamp field is also set. Preferences are checked to
1472          * see if these options are enabled.
1473          * 
1474          * @param entry
1475          *            The entry to set fields for.
1476          */
1477         public static void setAutomaticFields(BibtexEntry entry) {
1478                 String defaultOwner = Globals.prefs.get("defaultOwner");
1479                 String timestamp = easyDateFormat();
1480                 boolean setOwner = Globals.prefs.getBoolean("useOwner"), setTimeStamp = Globals.prefs
1481                         .getBoolean("useTimeStamp");
1482                 String timeStampField = Globals.prefs.get("timeStampField");
1483                 setAutomaticFields(entry, setOwner, defaultOwner, setTimeStamp, timeStampField, timestamp);
1484         }
1485
1486         private static void setAutomaticFields(BibtexEntry entry, boolean setOwner, String owner,
1487                 boolean setTimeStamp, String timeStampField, String timeStamp) {
1488
1489                 // Set owner field if this option is enabled:
1490                 if (setOwner) {
1491                         // No or empty owner field?
1492                         // if (entry.getField(Globals.OWNER) == null
1493                         // || ((String) entry.getField(Globals.OWNER)).length() == 0) {
1494                         // Set owner field to default value
1495                         entry.setField(BibtexFields.OWNER, owner);
1496                         // }
1497                 }
1498
1499                 if (setTimeStamp)
1500                         entry.setField(timeStampField, timeStamp);
1501         }
1502
1503         /**
1504          * Copies a file.
1505          * 
1506          * @param source
1507          *            File Source file
1508          * @param dest
1509          *            File Destination file
1510          * @param deleteIfExists
1511          *            boolean Determines whether the copy goes on even if the file
1512          *            exists.
1513          * @throws IOException
1514          * @return boolean Whether the copy succeeded, or was stopped due to the
1515          *         file already existing.
1516          */
1517         public static boolean copyFile(File source, File dest, boolean deleteIfExists)
1518                 throws IOException {
1519
1520                 BufferedInputStream in = null;
1521                 BufferedOutputStream out = null;
1522                 try {
1523                         // Check if the file already exists.
1524                         if (dest.exists()) {
1525                                 if (!deleteIfExists)
1526                                         return false;
1527                                 // else dest.delete();
1528                         }
1529
1530                         in = new BufferedInputStream(new FileInputStream(source));
1531                         out = new BufferedOutputStream(new FileOutputStream(dest));
1532                         int el;
1533                         // int tell = 0;
1534                         while ((el = in.read()) >= 0) {
1535                                 out.write(el);
1536                         }
1537                 } catch (IOException ex) {
1538                         throw ex;
1539                 } finally {
1540                         if (out != null) {
1541                                 out.flush();
1542                                 out.close();
1543                         }
1544                         if (in != null)
1545                                 in.close();
1546                 }
1547                 return true;
1548         }
1549
1550         /**
1551          * This method is called at startup, and makes necessary adaptations to
1552          * preferences for users from an earlier version of Jabref.
1553          */
1554         public static void performCompatibilityUpdate() {
1555
1556                 // Make sure "abstract" is not in General fields, because
1557                 // Jabref 1.55 moves the abstract to its own tab.
1558                 String genFields = Globals.prefs.get("generalFields");
1559                 // pr(genFields+"\t"+genFields.indexOf("abstract"));
1560                 if (genFields.indexOf("abstract") >= 0) {
1561                         // pr(genFields+"\t"+genFields.indexOf("abstract"));
1562                         String newGen;
1563                         if (genFields.equals("abstract"))
1564                                 newGen = "";
1565                         else if (genFields.indexOf(";abstract;") >= 0) {
1566                                 newGen = genFields.replaceAll(";abstract;", ";");
1567                         } else if (genFields.indexOf("abstract;") == 0) {
1568                                 newGen = genFields.replaceAll("abstract;", "");
1569                         } else if (genFields.indexOf(";abstract") == genFields.length() - 9) {
1570                                 newGen = genFields.replaceAll(";abstract", "");
1571                         } else
1572                                 newGen = genFields;
1573                         // pr(newGen);
1574                         Globals.prefs.put("generalFields", newGen);
1575                 }
1576
1577         }
1578
1579         // -------------------------------------------------------------------------------
1580
1581         /**
1582          * extends the filename with a default Extension, if no Extension '.x' could
1583          * be found
1584          */
1585         public static String getCorrectFileName(String orgName, String defaultExtension) {
1586                 if (orgName == null)
1587                         return "";
1588
1589                 String back = orgName;
1590                 int t = orgName.indexOf(".", 1); // hidden files Linux/Unix (?)
1591                 if (t < 1)
1592                         back = back + "." + defaultExtension;
1593
1594                 return back;
1595         }
1596
1597         /**
1598          * Quotes each and every character, e.g. '!' as &#33;. Used for verbatim
1599          * display of arbitrary strings that may contain HTML entities.
1600          */
1601         public static String quoteForHTML(String s) {
1602                 StringBuffer sb = new StringBuffer();
1603                 for (int i = 0; i < s.length(); ++i) {
1604                         sb.append("&#" + (int) s.charAt(i) + ";");
1605                 }
1606                 return sb.toString();
1607         }
1608
1609         public static String quote(String s, String specials, char quoteChar) {
1610                 return quote(s, specials, quoteChar, 0);
1611         }
1612
1613         /**
1614          * Quote special characters.
1615          * 
1616          * @param s
1617          *            The String which may contain special characters.
1618          * @param specials
1619          *            A String containing all special characters except the quoting
1620          *            character itself, which is automatically quoted.
1621          * @param quoteChar
1622          *            The quoting character.
1623          * @param linewrap
1624          *            The number of characters after which a linebreak is inserted
1625          *            (this linebreak is undone by unquote()). Set to 0 to disable.
1626          * @return A String with every special character (including the quoting
1627          *         character itself) quoted.
1628          */
1629         public static String quote(String s, String specials, char quoteChar, int linewrap) {
1630                 StringBuffer sb = new StringBuffer();
1631                 char c;
1632                 int linelength = 0;
1633                 boolean isSpecial;
1634                 for (int i = 0; i < s.length(); ++i) {
1635                         c = s.charAt(i);
1636                         isSpecial = specials.indexOf(c) >= 0 || c == quoteChar;
1637                         // linebreak?
1638                         if (linewrap > 0
1639                                 && (++linelength >= linewrap || (isSpecial && linelength >= linewrap - 1))) {
1640                                 sb.append(quoteChar);
1641                                 sb.append('\n');
1642                                 linelength = 0;
1643                         }
1644                         if (isSpecial) {
1645                                 sb.append(quoteChar);
1646                                 ++linelength;
1647                         }
1648                         sb.append(c);
1649                 }
1650                 return sb.toString();
1651         }
1652
1653         /**
1654          * Unquote special characters.
1655          * 
1656          * @param s
1657          *            The String which may contain quoted special characters.
1658          * @param quoteChar
1659          *            The quoting character.
1660          * @return A String with all quoted characters unquoted.
1661          */
1662         public static String unquote(String s, char quoteChar) {
1663                 StringBuffer sb = new StringBuffer();
1664                 char c;
1665                 boolean quoted = false;
1666                 for (int i = 0; i < s.length(); ++i) {
1667                         c = s.charAt(i);
1668                         if (quoted) { // append literally...
1669                                 if (c != '\n') // ...unless newline
1670                                         sb.append(c);
1671                                 quoted = false;
1672                         } else if (c != quoteChar) {
1673                                 sb.append(c);
1674                         } else { // quote char
1675                                 quoted = true;
1676                         }
1677                 }
1678                 return sb.toString();
1679         }
1680
1681         /**
1682          * Quote all regular expression meta characters in s, in order to search for
1683          * s literally.
1684          */
1685         public static String quoteMeta(String s) {
1686                 // work around a bug: trailing backslashes have to be quoted
1687                 // individually
1688                 int i = s.length() - 1;
1689                 StringBuffer bs = new StringBuffer("");
1690                 while ((i >= 0) && (s.charAt(i) == '\\')) {
1691                         --i;
1692                         bs.append("\\\\");
1693                 }
1694                 s = s.substring(0, i + 1);
1695                 return "\\Q" + s.replaceAll("\\\\E", "\\\\E\\\\\\\\E\\\\Q") + "\\E" + bs.toString();
1696         }
1697
1698         /*
1699          * This method "tidies" up e.g. a keyword string, by alphabetizing the words
1700          * and removing all duplicates.
1701          */
1702         public static String sortWordsAndRemoveDuplicates(String text) {
1703
1704                 String[] words = text.split(", ");
1705                 SortedSet set = new TreeSet();
1706                 for (int i = 0; i < words.length; i++)
1707                         set.add(words[i]);
1708                 StringBuffer sb = new StringBuffer();
1709                 for (Iterator i = set.iterator(); i.hasNext();) {
1710                         sb.append(i.next());
1711                         sb.append(", ");
1712                 }
1713                 if (sb.length() > 2)
1714                         sb.delete(sb.length() - 2, sb.length());
1715                 String result = sb.toString();
1716                 return result.length() > 2 ? result : "";
1717         }
1718
1719         /**
1720          * Warns the user of undesired side effects of an explicit
1721          * assignment/removal of entries to/from this group. Currently there are
1722          * four types of groups: AllEntriesGroup, SearchGroup - do not support
1723          * explicit assignment. ExplicitGroup - never modifies entries. KeywordGroup -
1724          * only this modifies entries upon assignment/removal. Modifications are
1725          * acceptable unless they affect a standard field (such as "author") besides
1726          * the "keywords" field.
1727          * 
1728          * @param parent
1729          *            The Component used as a parent when displaying a confirmation
1730          *            dialog.
1731          * @return true if the assignment has no undesired side effects, or the user
1732          *         chose to perform it anyway. false otherwise (this indicates that
1733          *         the user has aborted the assignment).
1734          */
1735         public static boolean warnAssignmentSideEffects(AbstractGroup[] groups, BibtexEntry[] entries,
1736                 BibtexDatabase db, Component parent) {
1737                 Vector affectedFields = new Vector();
1738                 for (int k = 0; k < groups.length; ++k) {
1739                         if (groups[k] instanceof KeywordGroup) {
1740                                 KeywordGroup kg = (KeywordGroup) groups[k];
1741                                 String field = kg.getSearchField().toLowerCase();
1742                                 if (field.equals("keywords"))
1743                                         continue; // this is not undesired
1744                                 for (int i = 0, len = BibtexFields.numberOfPublicFields(); i < len; ++i) {
1745                                         if (field.equals(BibtexFields.getFieldName(i))) {
1746                                                 affectedFields.add(field);
1747                                                 break;
1748                                         }
1749                                 }
1750                         }
1751                 }
1752                 if (affectedFields.size() == 0)
1753                         return true; // no side effects
1754
1755                 // show a warning, then return
1756                 StringBuffer message = // JZTODO lyrics...
1757                 new StringBuffer("This action will modify the following field(s)\n"
1758                         + "in at least one entry each:\n");
1759                 for (int i = 0; i < affectedFields.size(); ++i)
1760                         message.append(affectedFields.elementAt(i)).append("\n");
1761                 message.append("This could cause undesired changes to "
1762                         + "your entries, so it is\nrecommended that you change the grouping field "
1763                         + "in your group\ndefinition to \"keywords\" or a non-standard name."
1764                         + "\n\nDo you still want to continue?");
1765                 int choice = JOptionPane.showConfirmDialog(parent, message, Globals.lang("Warning"),
1766                         JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
1767                 return choice != JOptionPane.NO_OPTION;
1768
1769                 // if (groups instanceof KeywordGroup) {
1770                 // KeywordGroup kg = (KeywordGroup) groups;
1771                 // String field = kg.getSearchField().toLowerCase();
1772                 // if (field.equals("keywords"))
1773                 // return true; // this is not undesired
1774                 // for (int i = 0; i < GUIGlobals.ALL_FIELDS.length; ++i) {
1775                 // if (field.equals(GUIGlobals.ALL_FIELDS[i])) {
1776                 // // show a warning, then return
1777                 // String message = Globals // JZTODO lyrics...
1778                 // .lang(
1779                 // "This action will modify the \"%0\" field "
1780                 // + "of your entries.\nThis could cause undesired changes to "
1781                 // + "your entries, so it is\nrecommended that you change the grouping
1782                 // field "
1783                 // + "in your group\ndefinition to \"keywords\" or a non-standard name."
1784                 // + "\n\nDo you still want to continue?",
1785                 // field);
1786                 // int choice = JOptionPane.showConfirmDialog(parent, message,
1787                 // Globals.lang("Warning"), JOptionPane.YES_NO_OPTION,
1788                 // JOptionPane.WARNING_MESSAGE);
1789                 // return choice != JOptionPane.NO_OPTION;
1790                 // }
1791                 // }
1792                 // }
1793                 // return true; // found no side effects
1794         }
1795
1796         // ========================================================
1797         // lot of abreviations in medline
1798         // PKC etc convert to {PKC} ...
1799         // ========================================================
1800         static Pattern titleCapitalPattern = Pattern.compile("[A-Z]+");
1801
1802         /**
1803          * Wrap all uppercase letters, or sequences of uppercase letters, in curly
1804          * braces. Ignore letters within a pair of # character, as these are part of
1805          * a string label that should not be modified.
1806          * 
1807          * @param s
1808          *            The string to modify.
1809          * @return The resulting string after wrapping capitals.
1810          */
1811         public static String putBracesAroundCapitals(String s) {
1812
1813                 boolean inString = false, isBracing = false, escaped = false;
1814                 int inBrace = 0;
1815                 StringBuffer buf = new StringBuffer();
1816                 for (int i = 0; i < s.length(); i++) {
1817                         // Update variables based on special characters:
1818                         int c = s.charAt(i);
1819                         if (c == '{')
1820                                 inBrace++;
1821                         else if (c == '}')
1822                                 inBrace--;
1823                         else if (!escaped && (c == '#'))
1824                                 inString = !inString;
1825
1826                         // See if we should start bracing:
1827                         if ((inBrace == 0) && !isBracing && !inString && Character.isLetter((char) c)
1828                                 && Character.isUpperCase((char) c)) {
1829
1830                                 buf.append('{');
1831                                 isBracing = true;
1832                         }
1833
1834                         // See if we should close a brace set:
1835                         if (isBracing && !(Character.isLetter((char) c) && Character.isUpperCase((char) c))) {
1836
1837                                 buf.append('}');
1838                                 isBracing = false;
1839                         }
1840
1841                         // Add the current character:
1842                         buf.append((char) c);
1843
1844                         // Check if we are entering an escape sequence:
1845                         if ((c == '\\') && !escaped)
1846                                 escaped = true;
1847                         else
1848                                 escaped = false;
1849
1850                 }
1851                 // Check if we have an unclosed brace:
1852                 if (isBracing)
1853                         buf.append('}');
1854
1855                 return buf.toString();
1856
1857                 /*
1858                  * if (s.length() == 0) return s; // Protect against ArrayIndexOutOf....
1859                  * StringBuffer buf = new StringBuffer();
1860                  * 
1861                  * Matcher mcr = titleCapitalPattern.matcher(s.substring(1)); while
1862                  * (mcr.find()) { String replaceStr = mcr.group();
1863                  * mcr.appendReplacement(buf, "{" + replaceStr + "}"); }
1864                  * mcr.appendTail(buf); return s.substring(0, 1) + buf.toString();
1865                  */
1866         }
1867
1868         static Pattern bracedTitleCapitalPattern = Pattern.compile("\\{[A-Z]+\\}");
1869
1870         /**
1871          * This method looks for occurences of capital letters enclosed in an
1872          * arbitrary number of pairs of braces, e.g. "{AB}" or "{{T}}". All of these
1873          * pairs of braces are removed.
1874          * 
1875          * @param s
1876          *            The String to analyze.
1877          * @return A new String with braces removed.
1878          */
1879         public static String removeBracesAroundCapitals(String s) {
1880                 String previous = s;
1881                 while ((s = removeSingleBracesAroundCapitals(s)).length() < previous.length()) {
1882                         previous = s;
1883                 }
1884                 return s;
1885         }
1886
1887         /**
1888          * This method looks for occurences of capital letters enclosed in one pair
1889          * of braces, e.g. "{AB}". All these are replaced by only the capitals in
1890          * between the braces.
1891          * 
1892          * @param s
1893          *            The String to analyze.
1894          * @return A new String with braces removed.
1895          */
1896         public static String removeSingleBracesAroundCapitals(String s) {
1897                 Matcher mcr = bracedTitleCapitalPattern.matcher(s);
1898                 StringBuffer buf = new StringBuffer();
1899                 while (mcr.find()) {
1900                         String replaceStr = mcr.group();
1901                         mcr.appendReplacement(buf, replaceStr.substring(1, replaceStr.length() - 1));
1902                 }
1903                 mcr.appendTail(buf);
1904                 return buf.toString();
1905         }
1906
1907         /**
1908          * This method looks up what kind of external binding is used for the given
1909          * field, and constructs on OpenFileFilter suitable for browsing for an
1910          * external file.
1911          * 
1912          * @param fieldName
1913          *            The BibTeX field in question.
1914          * @return The file filter.
1915          */
1916         public static OpenFileFilter getFileFilterForField(String fieldName) {
1917                 String s = BibtexFields.getFieldExtras(fieldName);
1918                 final String ext = "." + fieldName.toLowerCase();
1919                 final OpenFileFilter off;
1920                 if (s.equals("browseDocZip"))
1921                         off = new OpenFileFilter(new String[] { ext, ext + ".gz", ext + ".bz2" });
1922                 else
1923                         off = new OpenFileFilter(new String[] { ext });
1924                 return off;
1925         }
1926
1927         /**
1928          * This method can be used to display a "rich" error dialog which offers the
1929          * entire stack trace for an exception.
1930          * 
1931          * @param parent
1932          * @param e
1933          */
1934         public static void showQuickErrorDialog(JFrame parent, String title, Exception e) {
1935                 // create and configure a text area - fill it with exception text.
1936                 final JPanel pan = new JPanel(), details = new JPanel();
1937                 final CardLayout crd = new CardLayout();
1938                 pan.setLayout(crd);
1939                 final JTextArea textArea = new JTextArea();
1940                 textArea.setFont(new Font("Sans-Serif", Font.PLAIN, 10));
1941                 textArea.setEditable(false);
1942                 StringWriter writer = new StringWriter();
1943                 e.printStackTrace(new PrintWriter(writer));
1944                 textArea.setText(writer.toString());
1945                 JLabel lab = new JLabel(e.getMessage());
1946                 JButton flip = new JButton(Globals.lang("Details"));
1947
1948                 FormLayout layout = new FormLayout("left:pref", "");
1949                 DefaultFormBuilder builder = new DefaultFormBuilder(layout);
1950                 builder.append(lab);
1951                 builder.nextLine();
1952                 builder.append(Box.createVerticalGlue());
1953                 builder.nextLine();
1954                 builder.append(flip);
1955                 final JPanel simple = builder.getPanel();
1956
1957                 // stuff it in a scrollpane with a controlled size.
1958                 JScrollPane scrollPane = new JScrollPane(textArea);
1959                 scrollPane.setPreferredSize(new Dimension(350, 150));
1960                 details.setLayout(new BorderLayout());
1961                 details.add(scrollPane, BorderLayout.CENTER);
1962
1963                 flip.addActionListener(new ActionListener() {
1964                         public void actionPerformed(ActionEvent event) {
1965                                 crd.show(pan, "details");
1966                         }
1967                 });
1968                 pan.add(simple, "simple");
1969                 pan.add(details, "details");
1970                 // pass the scrollpane to the joptionpane.
1971                 JOptionPane.showMessageDialog(parent, pan, title, JOptionPane.ERROR_MESSAGE);
1972         }
1973
1974         public static String wrapHTML(String s, final int lineWidth) {
1975                 StringBuffer sb = new StringBuffer();
1976                 StringTokenizer tok = new StringTokenizer(s);
1977                 int charsLeft = lineWidth;
1978                 while (tok.hasMoreTokens()) {
1979                         String word = tok.nextToken();
1980                         if (charsLeft == lineWidth) { // fresh line
1981                                 sb.append(word);
1982                                 charsLeft -= word.length();
1983                                 if (charsLeft <= 0) {
1984                                         sb.append("<br>\n");
1985                                         charsLeft = lineWidth;
1986                                 }
1987                         } else { // continue previous line
1988                                 if (charsLeft < word.length() + 1) {
1989                                         sb.append("<br>\n");
1990                                         sb.append(word);
1991                                         if (word.length() >= lineWidth - 1) {
1992                                                 sb.append("<br>\n");
1993                                                 charsLeft = lineWidth;
1994                                         } else {
1995                                                 sb.append(" ");
1996                                                 charsLeft = lineWidth - word.length() - 1;
1997                                         }
1998                                 } else {
1999                                         sb.append(' ').append(word);
2000                                         charsLeft -= word.length() + 1;
2001                                 }
2002                         }
2003                 }
2004                 return sb.toString();
2005         }
2006
2007         /**
2008          * Creates a String containing the current date (and possibly time),
2009          * formatted according to the format set in preferences under the key
2010          * "timeStampFormat".
2011          * 
2012          * @return The date string.
2013          */
2014         public static String easyDateFormat() {
2015                 // Date today = new Date();
2016                 return easyDateFormat(new Date());
2017         }
2018
2019         /**
2020          * Creates a readable Date string from the parameter date. The format is set
2021          * in preferences under the key "timeStampFormat".
2022          * 
2023          * @return The formatted date string.
2024          */
2025         public static String easyDateFormat(Date date) {
2026                 // first use, create an instance
2027                 if (dateFormatter == null) {
2028                         String format = Globals.prefs.get("timeStampFormat");
2029                         dateFormatter = new SimpleDateFormat(format);
2030                 }
2031                 return dateFormatter.format(date);
2032         }
2033
2034         public static void markEntry(BibtexEntry be, NamedCompound ce) {
2035                 Object o = be.getField(BibtexFields.MARKED);
2036                 if ((o != null) && (o.toString().indexOf(Globals.prefs.WRAPPED_USERNAME) >= 0))
2037                         return;
2038                 String newValue;
2039                 if (o == null) {
2040                         newValue = Globals.prefs.WRAPPED_USERNAME;
2041                 } else {
2042                         StringBuffer sb = new StringBuffer(o.toString());
2043                         // sb.append(' ');
2044                         sb.append(Globals.prefs.WRAPPED_USERNAME);
2045                         newValue = sb.toString();
2046                 }
2047                 ce.addEdit(new UndoableFieldChange(be, BibtexFields.MARKED, be
2048                         .getField(BibtexFields.MARKED), newValue));
2049                 be.setField(BibtexFields.MARKED, newValue);
2050         }
2051
2052         public static void unmarkEntry(BibtexEntry be, BibtexDatabase database, NamedCompound ce) {
2053                 Object o = be.getField(BibtexFields.MARKED);
2054                 if (o != null) {
2055                         String s = o.toString();
2056                         if (s.equals("0")) {
2057                                 unmarkOldStyle(be, database, ce);
2058                                 return;
2059                         }
2060
2061                         int piv = 0, hit;
2062                         StringBuffer sb = new StringBuffer();
2063                         while ((hit = s.indexOf(Globals.prefs.WRAPPED_USERNAME, piv)) >= 0) {
2064                                 if (hit > 0)
2065                                         sb.append(s.substring(piv, hit));
2066                                 piv = hit + Globals.prefs.WRAPPED_USERNAME.length();
2067                         }
2068                         if (piv < s.length() - 1) {
2069                                 sb.append(s.substring(piv));
2070                         }
2071                         String newVal = sb.length() > 0 ? sb.toString() : null;
2072                         ce.addEdit(new UndoableFieldChange(be, BibtexFields.MARKED, be
2073                                 .getField(BibtexFields.MARKED), newVal));
2074                         be.setField(BibtexFields.MARKED, newVal);
2075                 }
2076         }
2077
2078         /**
2079          * An entry is marked with a "0", not in the new style with user names. We
2080          * want to unmark it as transparently as possible. Since this shouldn't
2081          * happen too often, we do it by scanning the "owner" fields of the entire
2082          * database, collecting all user names. We then mark the entry for all users
2083          * except the current one. Thus only the user who unmarks will see that it
2084          * is unmarked, and we get rid of the old-style marking.
2085          * 
2086          * @param be
2087          * @param ce
2088          */
2089         private static void unmarkOldStyle(BibtexEntry be, BibtexDatabase database, NamedCompound ce) {
2090                 TreeSet owners = new TreeSet();
2091                 for (Iterator i = database.getEntries().iterator(); i.hasNext();) {
2092                         BibtexEntry entry = (BibtexEntry) i.next();
2093                         Object o = entry.getField(BibtexFields.OWNER);
2094                         if (o != null)
2095                                 owners.add(o);
2096                         // System.out.println("Owner: "+entry.getField(Globals.OWNER));
2097                 }
2098                 owners.remove(Globals.prefs.get("defaultOwner"));
2099                 StringBuffer sb = new StringBuffer();
2100                 for (Iterator i = owners.iterator(); i.hasNext();) {
2101                         sb.append('[');
2102                         sb.append(i.next().toString());
2103                         sb.append(']');
2104                 }
2105                 String newVal = sb.toString();
2106                 if (newVal.length() == 0)
2107                         newVal = null;
2108                 ce.addEdit(new UndoableFieldChange(be, BibtexFields.MARKED, be
2109                         .getField(BibtexFields.MARKED), newVal));
2110                 be.setField(BibtexFields.MARKED, newVal);
2111
2112         }
2113
2114         public static boolean isMarked(BibtexEntry be) {
2115                 Object fieldVal = be.getField(BibtexFields.MARKED);
2116                 if (fieldVal == null)
2117                         return false;
2118                 String s = (String) fieldVal;
2119                 return (s.equals("0") || (s.indexOf(Globals.prefs.WRAPPED_USERNAME) >= 0));
2120         }
2121
2122     /**
2123      * Set a given field to a given value for all entries in a Collection.
2124      * This method DOES NOT update any UndoManager, but returns a relevant
2125      * CompoundEdit that should be registered by the caller.
2126      *
2127      * @param entries The entries to set the field for.
2128      * @param field The name of the field to set.
2129      * @param text The value to set. This value can be null, indicating that the
2130      *      field should be cleared.
2131      * @param overwriteValues Indicate whether the value should be set even if
2132      *      an entry already has the field set.
2133      * @return A CompoundEdit for the entire operation.
2134      */
2135     public static UndoableEdit massSetField(Collection entries,
2136                                             String field, String text,
2137                                             boolean overwriteValues) {
2138         
2139         NamedCompound ce = new NamedCompound(Globals.lang("Set field"));
2140         for (Iterator i = entries.iterator(); i.hasNext();) {
2141             BibtexEntry entry = (BibtexEntry) i.next();
2142             Object oldVal = entry.getField(field);
2143             // If we are not allowed to overwrite values, check if there is a nonempty
2144             // value already for this entry:
2145             if (!overwriteValues && (oldVal != null) && (((String)oldVal).length() > 0))
2146                 continue;
2147             if (text != null)
2148                 entry.setField(field, text);
2149             else
2150                 entry.clearField(field);
2151             ce.addEdit(new UndoableFieldChange(entry, field, oldVal, text));
2152         }
2153         ce.end();
2154         return ce;
2155     }
2156
2157     /**
2158          * Make a list of supported character encodings that can encode all
2159          * characters in the given String.
2160          * 
2161          * @param characters
2162          *            A String of characters that should be supported by the
2163          *            encodings.
2164          * @return A List of character encodings
2165          */
2166         public static List findEncodingsForString(String characters) {
2167                 List encodings = new ArrayList();
2168                 for (int i = 0; i < Globals.ENCODINGS.length; i++) {
2169                         CharsetEncoder encoder = Charset.forName(Globals.ENCODINGS[i]).newEncoder();
2170                         if (encoder.canEncode(characters))
2171                                 encodings.add(Globals.ENCODINGS[i]);
2172                 }
2173                 return encodings;
2174         }
2175         
2176
2177         /**
2178          * Will convert a two digit year using the following scheme (describe at
2179          * http://www.filemaker.com/help/02-Adding%20and%20view18.html):
2180          * 
2181          * If a two digit year is encountered they are matched against the last 69
2182          * years and future 30 years.
2183          * 
2184          * For instance if it is the year 1992 then entering 23 is taken to be 1923
2185          * but if you enter 23 in 1993 then it will evaluate to 2023.
2186          * 
2187          * @param year
2188          *            The year to convert to 4 digits.
2189          * @return
2190          */
2191         public static String toFourDigitYear(String year) {
2192                 if (thisYear == 0){
2193                         thisYear = Calendar.getInstance().get(Calendar.YEAR);
2194                 }
2195                 return toFourDigitYear(year, thisYear);
2196         }
2197         
2198         public static int thisYear;
2199         
2200         /**
2201          * Will convert a two digit year using the following scheme (describe at
2202          * http://www.filemaker.com/help/02-Adding%20and%20view18.html):
2203          * 
2204          * If a two digit year is encountered they are matched against the last 69
2205          * years and future 30 years.
2206          * 
2207          * For instance if it is the year 1992 then entering 23 is taken to be 1923
2208          * but if you enter 23 in 1993 then it will evaluate to 2023.
2209          * 
2210          * @param year
2211          *            The year to convert to 4 digits.
2212          * @return
2213          */
2214         public static String toFourDigitYear(String year, int thisYear) {
2215                 if (year.length() != 2)
2216                         return year;
2217                 try {
2218                         int thisYearTwoDigits = thisYear % 100;
2219                         int thisCentury = thisYear - thisYearTwoDigits;
2220
2221                         int yearNumber = Integer.parseInt(year);
2222
2223                         if (yearNumber == thisYearTwoDigits) {
2224                                 return String.valueOf(thisYear);
2225                         } 
2226                         // 20 , 90
2227                         // 99 > 30
2228                         if ((yearNumber + 100 - thisYearTwoDigits) % 100 > 30) {
2229                                 if (yearNumber < thisYearTwoDigits) {
2230                                         return String.valueOf(thisCentury + yearNumber);
2231                                 } else {
2232                                         return String.valueOf(thisCentury - 100 + yearNumber);
2233                                 }
2234                         } else {
2235                                 if (yearNumber < thisYearTwoDigits) {
2236                                         return String.valueOf(thisCentury + 100 + yearNumber);
2237                                 } else {
2238                                         return String.valueOf(thisCentury + yearNumber);
2239                                 }
2240                         }
2241                 } catch (NumberFormatException e) {
2242                         return year;
2243                 }
2244         }
2245         
2246         /**
2247          * Will return an integer indicating the month of the entry from 0 to 11.
2248          * 
2249          * -1 signals a unknown month string.
2250          * 
2251          * This method accepts three types of months given:
2252          * 
2253          * - Single and Double Digit months from 1 to 12 (01 to 12)
2254          * 
2255          * - 3 Digit BibTex strings (jan
2256          * 
2257          * - Full English Month identifiers.
2258          * 
2259          * @param month
2260          * @return
2261          */
2262         public static int getMonthNumber(String month){
2263                 
2264                 month = month.toLowerCase();
2265                 
2266                 for (int i = 0; i < Globals.MONTHS.length; i++){
2267                         if (month.startsWith(Globals.MONTHS[i])){
2268                                 return i;
2269                         }
2270                 }
2271                 
2272                 try {
2273                         return Integer.parseInt(month) - 1;
2274                 } catch (NumberFormatException e){
2275                 }
2276                 return -1;
2277         }
2278 }