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