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