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