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