Logo Search packages:      
Sourcecode: jabref version File versions  Download package

Util.java

/*
 Copyright (C) 2003 Morten O. Alver

 All programs in this directory and
 subdirectories are published under the GNU General Public License as
 described below.

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at
 your option) any later version.

 This program is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 USA

 Further information about the GNU GPL is available at:
 http://www.gnu.org/copyleft/gpl.ja.html

 */
// created by : Morten O. Alver 2003
//
// function : utility functions
//
// modified :  - r.nagel 20.04.2006
//               make the DateFormatter abstract and splitt the easyDate methode
//               (now we cannot change the dateformat dynamicly, sorry)
package net.sf.jabref;

import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.undo.UndoableEdit;

import net.sf.jabref.autocompleter.AbstractAutoCompleter;
import net.sf.jabref.export.SaveSession;
import net.sf.jabref.external.ExternalFileType;
import net.sf.jabref.external.ExternalFileTypeEntryEditor;
import net.sf.jabref.external.UnknownExternalFileType;
import net.sf.jabref.groups.AbstractGroup;
import net.sf.jabref.groups.KeywordGroup;
import net.sf.jabref.gui.FileListEntry;
import net.sf.jabref.gui.FileListEntryEditor;
import net.sf.jabref.gui.FileListTableModel;
import net.sf.jabref.imports.CiteSeerFetcher;
import net.sf.jabref.labelPattern.LabelPatternUtil;
import net.sf.jabref.net.URLDownload;
import net.sf.jabref.undo.NamedCompound;
import net.sf.jabref.undo.UndoableFieldChange;

import com.jgoodies.forms.builder.DefaultFormBuilder;
import com.jgoodies.forms.layout.FormLayout;

/**
 * Describe class <code>Util</code> here.
 * 
 * @author <a href="mailto:"> </a>
 * @version 1.0
 */
00119 public class Util {

      /**
       * A static Object for date formatting. Please do not create the object
       * here, because there are some references from the Globals class.....
       * 
       */
00126       private static SimpleDateFormat dateFormatter = null;

      /*
       * Colors are defined here.
       * 
       */
      public static Color fieldsCol = new Color(180, 180, 200);

      /*
       * Integer values for indicating result of duplicate check (for entries):
       * 
       */
      final static int TYPE_MISMATCH = -1, NOT_EQUAL = 0, EQUAL = 1, EMPTY_IN_ONE = 2,
            EMPTY_IN_TWO = 3, EMPTY_IN_BOTH = 4;

      final static NumberFormat idFormat;

    public static Pattern remoteLinkPattern = Pattern.compile("[a-z]+://.*");

      static {
            idFormat = NumberFormat.getInstance();
            idFormat.setMinimumIntegerDigits(8);
            idFormat.setGroupingUsed(false);
      }

      public static int getMinimumIntegerDigits(){
            return idFormat.getMinimumIntegerDigits();
      }

      public static void bool(boolean b) {
            if (b)
                  System.out.println("true");
            else
                  System.out.println("false");
      }

      public static void pr(String s) {
            System.out.println(s);
      }

      public static void pr_(String s) {
            System.out.print(s);
      }

      public static String nCase(String s) {
            // Make first character of String uppercase, and the
            // rest lowercase.
            if (s.length() > 1)
                  return s.substring(0, 1).toUpperCase() + s.substring(1, s.length()).toLowerCase();
            else
                  return s.toUpperCase();

      }

      public static String checkName(String s) {
            // Append '.bib' to the string unless it ends with that.
            if (s.length() < 4 || !s.substring(s.length() - 4).equalsIgnoreCase(".bib")) {
                  return s + ".bib";
            }
            return s;
      }

      private static int idCounter = 0;

      public synchronized static String createNeutralId() {
            return idFormat.format(idCounter++);
      }

      /**
       * This method sets the location of a Dialog such that it is centered with
       * regard to another window, but not outside the screen on the left and the
       * top.
       */
00199       public static void placeDialog(java.awt.Dialog diag, java.awt.Container win) {
        diag.setLocationRelativeTo(win);
      }

      /**
       * This method translates a field or string from Bibtex notation, with
       * possibly text contained in " " or { }, and string references,
       * concatenated by '#' characters, into Bibkeeper notation, where string
       * references are enclosed in a pair of '#' characters.
       */
00209       public static String parseField(String content) {
            
            if (content.length() == 0)
                  return content;
            
            String[] strings = content.split("#");
            StringBuffer result = new StringBuffer();
            for (int i = 0; i < strings.length; i++){
                  String s = strings[i].trim();
                  if (s.length() > 0){
                        char c = s.charAt(0);
                        // String reference or not?
                        if (c == '{' || c == '"'){
                              result.append(shaveString(strings[i]));   
                        } else {
                              // This part should normally be a string reference, but if it's
                              // a pure number, it is not.
                              String s2 = shaveString(s);
                              try {
                                    Integer.parseInt(s2);
                                    // If there's no exception, it's a number.
                                    result.append(s2);
                              } catch (NumberFormatException ex) {
                                    // otherwise append with hashes...
                                    result.append("#").append(s2).append("#");
                              }
                        }
                  }
            }
            return result.toString();
      }

      /**
       * Will return the publication date of the given bibtex entry in conformance
       * to ISO 8601, i.e. either YYYY or YYYY-MM.
       * 
       * @param entry
       * @return will return the publication date of the entry or null if no year
       *         was found.
       */
00249       public static String getPublicationDate(BibtexEntry entry) {

            Object o = entry.getField("year");
            if (o == null)
                  return null;

            String year = toFourDigitYear(o.toString());

            o = entry.getField("month");
            if (o != null) {
                  int month = Util.getMonthNumber(o.toString());
                  if (month != -1) {
                        return year + "-" + (month + 1 < 10 ? "0" : "") + (month + 1);
                  }
            }
            return year;
      }

      public static String shaveString(String s) {
            // returns the string, after shaving off whitespace at the beginning
            // and end, and removing (at most) one pair of braces or " surrounding
            // it.
            if (s == null)
                  return null;
            char ch, ch2;
            int beg = 0, end = s.length();
            // We start out assuming nothing will be removed.
            boolean begok = false, endok = false;
            while (!begok) {
                  if (beg < s.length()) {
                        ch = s.charAt(beg);
                        if (Character.isWhitespace(ch))
                              beg++;
                        else
                              begok = true;
                  } else
                        begok = true;

            }
            while (!endok) {
                  if (end > beg + 1) {
                        ch = s.charAt(end - 1);
                        if (Character.isWhitespace(ch))
                              end--;
                        else
                              endok = true;
                  } else
                        endok = true;
            }

            if (end > beg + 1) {
                  ch = s.charAt(beg);
                  ch2 = s.charAt(end - 1);
                  if (((ch == '{') && (ch2 == '}')) || ((ch == '"') && (ch2 == '"'))) {
                        beg++;
                        end--;
                  }
            }
            s = s.substring(beg, end);
            return s;
      }

      /**
       * This method returns a String similar to the one passed in, except that it
       * is molded into a form that is acceptable for bibtex.
       * 
       * Watch-out that the returned string might be of length 0 afterwards.
       * 
       * @param key
       *            mayBeNull
       */
00320       public static String checkLegalKey(String key) {
            if (key == null)
                  return null;
        if (!Globals.prefs.getBoolean("enforceLegalBibtexKey")) {
            // User doesn't want us to enforce legal characters. We must still look
            // for whitespace and some characters such as commas, since these would
            // interfere with parsing:
            StringBuilder newKey = new StringBuilder();
            for (int i = 0; i < key.length(); i++) {
                char c = key.charAt(i);
                if (!Character.isWhitespace(c) && (c != '{') && (c != '\\') && (c != '"')
                    && (c != '}') && (c != ','))
                    newKey.append(c);
            }
            return newKey.toString();

        }
            StringBuilder newKey = new StringBuilder();
            for (int i = 0; i < key.length(); i++) {
                  char c = key.charAt(i);
                  if (!Character.isWhitespace(c) && (c != '#') && (c != '{') && (c != '\\') && (c != '"')
                        && (c != '}') && (c != '~') && (c != ',') && (c != '^'))
                        newKey.append(c);
            }

            // Replace non-english characters like umlauts etc. with a sensible
            // letter or letter combination that bibtex can accept.
            String newKeyS = replaceSpecialCharacters(newKey.toString());

            return newKeyS;
      }

      /**
       * Replace non-english characters like umlauts etc. with a sensible letter
       * or letter combination that bibtex can accept. The basis for replacement
       * is the HashMap GLobals.UNICODE_CHARS.
       */
00357       public static String replaceSpecialCharacters(String s) {
            for (Map.Entry<String, String> chrAndReplace : Globals.UNICODE_CHARS.entrySet()){
                  s = s.replaceAll(chrAndReplace.getKey(), chrAndReplace.getValue());
            }
            return s;
      }

      static public String _wrap2(String in, int wrapAmount) {
            // The following line cuts out all whitespace and replaces them with
            // single
            // spaces:
            // in = in.replaceAll("[ ]+"," ").replaceAll("[\\t]+"," ");
            // StringBuffer out = new StringBuffer(in);
            StringBuffer out = new StringBuffer(in.replaceAll("[ \\t\\r]+", " "));

            int p = in.length() - wrapAmount;
            int lastInserted = -1;
            while (p > 0) {
                  p = out.lastIndexOf(" ", p);
                  if (p <= 0 || p <= 20)
                        break;
                  int lbreak = out.indexOf("\n", p);
                  System.out.println(lbreak + " " + lastInserted);
                  if ((lbreak > p) && ((lastInserted >= 0) && (lbreak < lastInserted))) {
                        p = lbreak - wrapAmount;
                  } else {
                        out.insert(p, "\n\t");
                        lastInserted = p;
                        p -= wrapAmount;
                  }
            }
            return out.toString();
      }

      static public String wrap2(String in, int wrapAmount) {
            return net.sf.jabref.imports.FieldContentParser.wrap(in, wrapAmount);
      }

      static public String __wrap2(String in, int wrapAmount) {
            // The following line cuts out all whitespace except line breaks, and
            // replaces
            // with single spaces. Line breaks are padded with a tab character:
            StringBuffer out = new StringBuffer(in.replaceAll("[ \\t\\r]+", " "));

            int p = 0;
            // int lastInserted = -1;
            while (p < out.length()) {
                  int q = out.indexOf(" ", p + wrapAmount);
                  if ((q < 0) || (q >= out.length()))
                        break;
                  int lbreak = out.indexOf("\n", p);
                  // System.out.println(lbreak);
                  if ((lbreak > p) && (lbreak < q)) {
                        p = lbreak + 1;
                        int piv = lbreak + 1;
                        if ((out.length() > piv) && !(out.charAt(piv) == '\t'))
                              out.insert(piv, "\n\t");

                  } else {
                        // System.out.println(q+" "+out.length());
                        out.deleteCharAt(q);
                        out.insert(q, "\n\t");
                        p = q + 1;
                  }
            }
            return out.toString();// .replaceAll("\n", "\n\t");
      }

      public static TreeSet<String> findDeliminatedWordsInField(BibtexDatabase db, String field,
            String deliminator) {
            TreeSet<String> res = new TreeSet<String>();
            
            for (String s : db.getKeySet()){
                  BibtexEntry be = db.getEntryById(s);
                  Object o = be.getField(field);
                  if (o != null) {
                        String fieldValue = o.toString().trim();
                        StringTokenizer tok = new StringTokenizer(fieldValue, deliminator);
                        while (tok.hasMoreTokens())
                              res.add(nCase(tok.nextToken().trim()));
                  }
            }
            return res;
      }

      /**
       * Returns a HashMap containing all words used in the database in the given
       * field type. Characters in
       * 
       * @param remove
       *            are not included.
       * @param db
       *            a <code>BibtexDatabase</code> value
       * @param field
       *            a <code>String</code> value
       * @param remove
       *            a <code>String</code> value
       * @return a <code>HashSet</code> value
       */
00456       public static TreeSet<String> findAllWordsInField(BibtexDatabase db, String field, String remove) {
            TreeSet<String> res = new TreeSet<String>();
            StringTokenizer tok;
            for (String s : db.getKeySet()){
                  BibtexEntry be = db.getEntryById(s);
                  Object o = be.getField(field);
                  if (o != null) {
                        tok = new StringTokenizer(o.toString(), remove, false);
                        while (tok.hasMoreTokens())
                              res.add(nCase(tok.nextToken().trim()));
                  }
            }
            return res;
      }


    /**
     * Finds all authors' last names in all the given fields for the given database.
     * @param db The database.
     * @param fields The fields to look in.
     * @return a set containing the names.
     */
00478     public static Set<String> findAuthorLastNames(BibtexDatabase db, List<String> fields) {
            Set<String> res = new TreeSet<String>();
            for (String s : db.getKeySet()){
                  BibtexEntry be = db.getEntryById(s);
            for (String field : fields) {
                String val = be.getField(field);
                if ((val != null) && (val.length() > 0)) {
                    AuthorList al = AuthorList.getAuthorList(val);
                    for (int i=0; i<al.size(); i++) {
                        AuthorList.Author a = al.getAuthor(i);
                        String lastName = a.getLast();
                        if ((lastName != null) && (lastName.length() > 0))
                            res.add(lastName);
                    }
                }

            }
            }

            return res;
      }
    

      /**
       * Takes a String array and returns a string with the array's elements
       * delimited by a certain String.
       * 
       * @param strs
       *            String array to convert.
       * @param delimiter
       *            String to use as delimiter.
       * @return Delimited String.
       */
00511       public static String stringArrayToDelimited(String[] strs, String delimiter) {
            if ((strs == null) || (strs.length == 0))
                  return "";
            if (strs.length == 1)
                  return strs[0];
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < strs.length - 1; i++) {
                  sb.append(strs[i]);
                  sb.append(delimiter);
            }
            sb.append(strs[strs.length - 1]);
            return sb.toString();
      }

      /**
       * Takes a delimited string, splits it and returns
       * 
       * @param names
       *            a <code>String</code> value
       * @return a <code>String[]</code> value
       */
00532       public static String[] delimToStringArray(String names, String delimiter) {
            if (names == null)
                  return null;
            return names.split(delimiter);
      }

      /**
       * Open a http/pdf/ps viewer for the given link string.
       */
00541       public static void openExternalViewer(MetaData metaData, String link, String fieldName)
            throws IOException {

        if (fieldName.equals("ps") || fieldName.equals("pdf")) {

            // Find the default directory for this field type:
                  String dir = metaData.getFileDirectory(fieldName);

                  File file = expandFilename(link, new String[] { dir, "." });

                  // Check that the file exists:
                  if ((file == null) || !file.exists()) {
                        throw new IOException(Globals.lang("File not found") + " (" + fieldName + "): '"
                              + link + "'.");
                  }
                  link = file.getCanonicalPath();

                  // Use the correct viewer even if pdf and ps are mixed up:
                  String[] split = file.getName().split("\\.");
                  if (split.length >= 2) {
                        if (split[split.length - 1].equalsIgnoreCase("pdf"))
                              fieldName = "pdf";
                        else if (split[split.length - 1].equalsIgnoreCase("ps")
                              || (split.length >= 3 && split[split.length - 2].equalsIgnoreCase("ps")))
                              fieldName = "ps";
                  }

        } else if (fieldName.equals("doi")) {
                  fieldName = "url";
                  
                  link = sanitizeUrl(link);
                  
                  // Check to see if link field already contains a well formated URL
                  if (!link.startsWith("http://")) {
                      // Remove possible 'doi:'
                      if (link.matches("^doi:/*.*")){
                      link = link.replaceFirst("^doi:/*", "");
                  }
                      link = Globals.DOI_LOOKUP_PREFIX + link;
                  }
            } else if (fieldName.equals("citeseerurl")) {
                  fieldName = "url";

                  String canonicalLink = CiteSeerFetcher.generateCanonicalURL(link);
                  if (canonicalLink != null)
                        link = canonicalLink;
            }

            if (fieldName.equals("url")) { // html
                  try {
                        link = sanitizeUrl(link);
                ExternalFileType fileType = Globals.prefs.getExternalFileTypeByExt("html");
                        if (Globals.ON_MAC) {

                    String[] cmd = ((fileType.getOpenWith() != null) && (fileType.getOpenWith().length() > 0)) ?
                            new String[] { "/usr/bin/open", "-a", fileType.getOpenWith(), link } :
                            new String[] { "/usr/bin/open", link };
                              Runtime.getRuntime().exec(cmd);
                        } else if (Globals.ON_WIN) {

                    if ((fileType.getOpenWith() != null) && (fileType.getOpenWith().length() > 0)) {
                        // Application is specified. Use it:
                        openFileWithApplicationOnWindows(link, fileType.getOpenWith());
                    } else
                        openFileOnWindows(link, true);
                        } else {
                    // Use the given app if specified, and the universal "xdg-open" otherwise:
                    String[] openWith;
                    if ((fileType.getOpenWith() != null) && (fileType.getOpenWith().length() > 0))
                        openWith = fileType.getOpenWith().split(" ");
                    else
                        openWith = new String[] {"xdg-open"};

                    String[] cmd = new String[openWith.length+1];
                    System.arraycopy(openWith, 0, cmd, 0, openWith.length);
                    cmd[cmd.length-1] = link;
                    Runtime.getRuntime().exec(cmd);
                        }

                  } catch (IOException e) {
                System.err.println(Globals.lang("Error_opening_file_'%0'.", link));
                e.printStackTrace();
                  }
            } else if (fieldName.equals("ps")) {
                  try {
                        if (Globals.ON_MAC) {
                    ExternalFileType type = Globals.prefs.getExternalFileTypeByExt("ps");
                    String viewer = type != null ? type.getOpenWith() : Globals.prefs.get("psviewer");
                    String[] cmd = { "/usr/bin/open", "-a", viewer, link };
                              Runtime.getRuntime().exec(cmd);
                        } else if (Globals.ON_WIN) {
                              openFileOnWindows(link, true);
                              /*
                               * cmdArray[0] = Globals.prefs.get("psviewer"); cmdArray[1] =
                               * link; Process child = Runtime.getRuntime().exec(
                               * cmdArray[0] + " " + cmdArray[1]);
                               */
                        } else {
                    ExternalFileType type = Globals.prefs.getExternalFileTypeByExt("ps");
                    String viewer = type != null ? type.getOpenWith() : "xdg-open";
                    String[] cmdArray = new String[2];
                    cmdArray[0] = viewer;
                              cmdArray[1] = link;
                              Runtime.getRuntime().exec(cmdArray);
                        }
                  } catch (IOException e) {
                        System.err.println("An error occured on the command: "
                              + Globals.prefs.get("psviewer") + " " + link);
                  }
            } else if (fieldName.equals("pdf")) {
                  try {
                        if (Globals.ON_MAC) {
                    ExternalFileType type = Globals.prefs.getExternalFileTypeByExt("pdf");
                    String viewer = type != null ? type.getOpenWith() : Globals.prefs.get("psviewer");
                    String[] cmd = { "/usr/bin/open", "-a", viewer, link };
                              Runtime.getRuntime().exec(cmd);
                        } else if (Globals.ON_WIN) {
                              openFileOnWindows(link, true);
                              /*
                               * String[] spl = link.split("\\\\"); StringBuffer sb = new
                               * StringBuffer(); for (int i = 0; i < spl.length; i++) { if
                               * (i > 0) sb.append("\\"); if (spl[i].indexOf(" ") >= 0)
                               * spl[i] = "\"" + spl[i] + "\""; sb.append(spl[i]); }
                               * //pr(sb.toString()); link = sb.toString();
                               * 
                               * String cmd = "cmd.exe /c start " + link;
                               * 
                               * Process child = Runtime.getRuntime().exec(cmd);
                               */
                        } else {
                    ExternalFileType type = Globals.prefs.getExternalFileTypeByExt("pdf");
                    String viewer = type != null ? type.getOpenWith() : Globals.prefs.get("psviewer");
                    String[] cmdArray = new String[2];
                    cmdArray[0] = viewer;
                              cmdArray[1] = link;
                              // Process child = Runtime.getRuntime().exec(cmdArray[0]+"
                              // "+cmdArray[1]);
                              Runtime.getRuntime().exec(cmdArray);
                        }
                  } catch (IOException e) {
                        e.printStackTrace();
                        System.err.println("An error occured on the command: "
                              + Globals.prefs.get("pdfviewer") + " #" + link);
                        System.err.println(e.getMessage());
                  }
            } else {
                  System.err
                        .println("Message: currently only PDF, PS and HTML files can be opened by double clicking");
            }
      }

      /**
       * Opens a file on a Windows system, using its default viewer.
       * 
       * @param link
       *            The file name.
       * @param localFile
       *            true if it is a local file, not an URL.
       * @throws IOException
       */
00701       public static void openFileOnWindows(String link, boolean localFile) throws IOException {
            /*
             * if (localFile) { String[] spl = link.split("\\\\"); StringBuffer sb =
             * new StringBuffer(); for (int i = 0; i < spl.length; i++) { if (i > 0)
             * sb.append("\\"); if (spl[i].indexOf(" ") >= 0) spl[i] = "\"" + spl[i] +
             * "\""; sb.append(spl[i]); } link = sb.toString(); }
             */
            link = link.replaceAll("&", "\"&\"").replaceAll(" ", "\" \"");

            // Bug fix for:
            // http://sourceforge.net/tracker/index.php?func=detail&aid=1489454&group_id=92314&atid=600306
            String cmd;
            if (Globals.osName.startsWith("Windows 9")) {
                  cmd = "command.com /c start " + link;
            } else {
                  cmd = "cmd.exe /c start " + link;
            }

        Runtime.getRuntime().exec(cmd);
      }

    /**
     * Opens a file on a Windows system, using the given application.
     *
     * @param link The file name.
     * @param application Link to the app that opens the file.
     * @throws IOException
     */
00729     public static void openFileWithApplicationOnWindows(String link, String application)
        throws IOException {

        link = link.replaceAll("&", "\"&\"").replaceAll(" ", "\" \"");

            Runtime.getRuntime().exec(application + " " + link);
    }

    /**
       * Open an external file, attempting to use the correct viewer for it.
       * 
       * @param metaData
       *            The MetaData for the database this file belongs to.
       * @param link
       *            The file name.
     * @return false if the link couldn't be resolved, true otherwise.
       */
00746       public static boolean openExternalFileAnyFormat(final MetaData metaData, String link,
                                                 final ExternalFileType fileType) throws IOException {

        boolean httpLink = false;

        if (remoteLinkPattern.matcher(link.toLowerCase()).matches()) {
            httpLink = true;
        }
        /*if (link.toLowerCase().startsWith("file://")) {
            link = link.substring(7);
        }
        final String ln = link;
        if (remoteLinkPattern.matcher(link.toLowerCase()).matches()) {
            (new Thread(new Runnable() {
                public void run() {
                    openRemoteExternalFile(metaData, ln, fileType);
                }
            })).start();

            return true;
        }*/

        //boolean httpLink = link.toLowerCase().startsWith("http:")
        //        || link.toLowerCase().startsWith("ftp:");

        
        // For other platforms we'll try to find the file type:
            File file = new File(link);

            // We try to check the extension for the file:
            String name = file.getName();
            int pos = name.lastIndexOf('.');
            String extension = ((pos >= 0) && (pos < name.length() - 1)) ? name.substring(pos + 1)
                  .trim().toLowerCase() : null;
            // Find the default directory for this field type, if any:
            String dir = metaData.getFileDirectory(extension);
            // Include the standard "file" directory:
        String fileDir = metaData.getFileDirectory(GUIGlobals.FILE_FIELD);
        // Include the directory of the bib file:
        String[] dirs;
        if (metaData.getFile() != null) {
            String databaseDir = metaData.getFile().getParent();
            dirs = new String[] { dir, fileDir, databaseDir };
        }
        else
            dirs = new String[] { dir, fileDir };

        if (!httpLink) {
            File tmp = expandFilename(link, dirs);
            if (tmp != null)
                file = tmp;
        }

        // Check if we have arrived at a file type, and either an http link or an existing file:
            if ((httpLink || file.exists()) && (fileType != null)) {
            // Open the file:
                  try {
                String filePath = httpLink ? link : file.getPath();
                if (Globals.ON_MAC) {
                    // Use "-a <application>" if the app is specified, and just "open <filename>" otherwise:
                    String[] cmd = ((fileType.getOpenWith() != null) && (fileType.getOpenWith().length() > 0)) ?
                            new String[] { "/usr/bin/open", "-a", fileType.getOpenWith(), filePath } :
                            new String[] { "/usr/bin/open", filePath };
                              Runtime.getRuntime().exec(cmd);
                        } else if (Globals.ON_WIN) {
                    if ((fileType.getOpenWith() != null) && (fileType.getOpenWith().length() > 0)) {
                        // Application is specified. Use it:
                        openFileWithApplicationOnWindows(filePath, fileType.getOpenWith());
                    } else
                        openFileOnWindows(filePath, true);
                        } else {
                    // Use the given app if specified, and the universal "xdg-open" otherwise:
                    String[] openWith;
                    if ((fileType.getOpenWith() != null) && (fileType.getOpenWith().length() > 0))
                        openWith = fileType.getOpenWith().split(" ");
                    else
                        openWith = new String[] {"xdg-open"};
                    
                    String[] cmdArray = new String[openWith.length+1];
                    System.arraycopy(openWith, 0, cmdArray, 0, openWith.length);
                    cmdArray[cmdArray.length-1] = filePath;
                    Runtime.getRuntime().exec(cmdArray);
                        }
                return true;
            } catch (IOException e) {
                throw e;
                /*e.printStackTrace();
                        System.err.println("An error occured on the command: " + fileType.getOpenWith()
                              + " #" + link);
                        System.err.println(e.getMessage());*/
                  }

            } else {

            return false;
            // No file matched the name, or we didn't know the file type.

            }


    }


    public static void openRemoteExternalFile(final MetaData metaData,
                                              final String link, final ExternalFileType fileType) {
        File temp = null;
        try {
            temp = File.createTempFile("jabref-link", "."+fileType.getExtension());
            temp.deleteOnExit();
            System.out.println("Downloading to '"+temp.getPath()+"'");
            URLDownload ud = new URLDownload(null, new URL(link), temp);
            ud.download();
            System.out.println("Done");
        } catch (MalformedURLException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        final String ln = temp.getPath();
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                try {
                    openExternalFileAnyFormat(metaData, ln, fileType);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        });
    }

public static boolean openExternalFileUnknown(JabRefFrame frame, BibtexEntry entry, MetaData metaData,
                                           String link, UnknownExternalFileType fileType) throws IOException {

    String cancelMessage = Globals.lang("Unable to open file.");
    String[] options = new String[] {Globals.lang("Define '%0'", fileType.getName()),
            Globals.lang("Change file type"), Globals.lang("Cancel")};
    String defOption = options[0];
    int answer = JOptionPane.showOptionDialog(frame, Globals.lang("This external link is of the type '%0', which is undefined. What do you want to do?",
            fileType.getName()),
            Globals.lang("Undefined file type"), JOptionPane.YES_NO_CANCEL_OPTION,
            JOptionPane.QUESTION_MESSAGE, null, options, defOption);
    if (answer == JOptionPane.CANCEL_OPTION) {
        frame.output(cancelMessage);
        return false;
    }
    else if (answer == JOptionPane.YES_OPTION) {
        // User wants to define the new file type. Show the dialog:
        ExternalFileType newType = new ExternalFileType(fileType.getName(), "", "", "", "new");
        ExternalFileTypeEntryEditor editor = new ExternalFileTypeEntryEditor(frame, newType);
        editor.setVisible(true);
        if (editor.okPressed()) {
            // Get the old list of types, add this one, and update the list in prefs:
            List<ExternalFileType> fileTypes = new ArrayList<ExternalFileType>();
            ExternalFileType[] oldTypes = Globals.prefs.getExternalFileTypeSelection();
            for (int i = 0; i < oldTypes.length; i++) {
                fileTypes.add(oldTypes[i]);
            }
            fileTypes.add(newType);
            Collections.sort(fileTypes);
            Globals.prefs.setExternalFileTypes(fileTypes);
            // Finally, open the file:
            return openExternalFileAnyFormat(metaData, link, newType);
        } else {
            // Cancelled:
            frame.output(cancelMessage);
            return false;
        }
    }
    else {
        // User wants to change the type of this link.
        // First get a model of all file links for this entry:
        FileListTableModel tModel = new FileListTableModel();
        String oldValue = entry.getField(GUIGlobals.FILE_FIELD);
        tModel.setContent(oldValue);
        FileListEntry flEntry = null;
        // Then find which one we are looking at:
        for (int i=0; i<tModel.getRowCount(); i++) {
            FileListEntry iEntry = tModel.getEntry(i);
            if (iEntry.getLink().equals(link)) {
                flEntry = iEntry;
                break;
            }
        }
        if (flEntry == null) {
            // This shouldn't happen, so I'm not sure what to put in here:
            throw new RuntimeException("Could not find the file list entry "+link+" in "+entry.toString());
        }

        FileListEntryEditor editor = new FileListEntryEditor(frame, flEntry, false, true, metaData);
        editor.setVisible(true, false);
        if (editor.okPressed()) {
            // Store the changes and add an undo edit:
            String newValue = tModel.getStringRepresentation();
            UndoableFieldChange ce = new UndoableFieldChange(entry, GUIGlobals.FILE_FIELD,
                    oldValue, newValue);
            entry.setField(GUIGlobals.FILE_FIELD, newValue);
            frame.basePanel().undoManager.addEdit(ce);
            frame.basePanel().markBaseChanged();
            // Finally, open the link:
            return openExternalFileAnyFormat(metaData, flEntry.getLink(), flEntry.getType());
        } else {
            // Cancelled:
            frame.output(cancelMessage);
            return false;
        }
    }
}
    /**
       * Make sure an URL is "portable", in that it doesn't contain bad characters
       * that break the open command in some OSes.
       * 
       * A call to this method will also remove \\url{} enclosings and clean doi links.
       * 
       * Old Version can be found in CVS version 114 of Util.java.
       * 
       * @param link
       *            The URL to sanitize.
       * @return Sanitized URL
       */
00965       public static String sanitizeUrl(String link) {

          // First check if it is enclosed in \\url{}. If so, remove
        // the wrapper.
        if (link.startsWith("\\url{") && link.endsWith("}"))
            link = link.substring(5, link.length() - 1);

        if (link.matches("^doi:/*.*")){
            // Remove 'doi:'
            link = link.replaceFirst("^doi:/*", "");
            link = Globals.DOI_LOOKUP_PREFIX + link;
        }
        
        /*
         * Poor man's DOI detection
         * 
         * Fixes
         * https://sourceforge.net/tracker/index.php?func=detail&aid=1709449&group_id=92314&atid=600306
         */
        if (link.startsWith("10.")) {
            link = Globals.DOI_LOOKUP_PREFIX + link;
        }
          
            link = link.replaceAll("\\+", "%2B");

            try {
                  link = URLDecoder.decode(link, "UTF-8");
            } catch (UnsupportedEncodingException e) {
            }

            /**
             * Fix for: [ 1574773 ] sanitizeUrl() breaks ftp:// and file:///
             * 
             * http://sourceforge.net/tracker/index.php?func=detail&aid=1574773&group_id=92314&atid=600306
             */
            try {
                  return new URI(null, link, null).toASCIIString();
            } catch (URISyntaxException e) {
                  return link;
            }
      }

      /**
       * Searches the given directory and subdirectories for a pdf file with name
       * as given + ".pdf"
       */
01011       public static String findPdf(String key, String extension, String directory, OpenFileFilter off) {
            // String filename = key + "."+extension;

            /*
             * Simon Fischer's patch for replacing a regexp in keys before
             * converting to filename:
             * 
             * String regex = Globals.prefs.get("basenamePatternRegex"); if ((regex !=
             * null) && (regex.trim().length() > 0)) { String replacement =
             * Globals.prefs.get("basenamePatternReplacement"); key =
             * key.replaceAll(regex, replacement); }
             */
            if (!directory.endsWith(System.getProperty("file.separator")))
                  directory += System.getProperty("file.separator");
            String found = findInDir(key, directory, off, 0);
            if (found != null)
                  return found.substring(directory.length());
            else
                  return null;
      }

      public static Map<BibtexEntry, List<File>> findAssociatedFiles(Collection<BibtexEntry> entries, Collection<String> extensions, Collection<File> directories){
            HashMap<BibtexEntry, List<File>> result = new HashMap<BibtexEntry, List<File>>();
      
            // First scan directories
            Set<File> filesWithExtension = findFiles(extensions, directories);
            
            // Initialize Result-Set
            for (BibtexEntry entry : entries){
                  result.put(entry, new ArrayList<File>());
            }

        boolean exactOnly = Globals.prefs.getBoolean("autolinkExactKeyOnly");
        // Now look for keys
            nextFile:
            for (File file : filesWithExtension){
                  
                  String name = file.getName();
            int dot = name.lastIndexOf('.');
            // First, look for exact matches:
            for (BibtexEntry entry : entries){
                String citeKey = entry.getCiteKey();
                if ((citeKey != null) && (citeKey.length() > 0)) {
                    if (dot > 0) {
                        if (name.substring(0, dot).equals(citeKey)) {
                            result.get(entry).add(file);
                            continue nextFile;
                        }
                    }
                }
            }
            // If we get here, we didn't find any exact matches. If non-exact
            // matches are allowed, try to find one:
            if (!exactOnly) {
                for (BibtexEntry entry : entries){
                    String citeKey = entry.getCiteKey();
                    if ((citeKey != null) && (citeKey.length() > 0)) {
                        if (name.startsWith(citeKey)){
                            result.get(entry).add(file);
                            continue nextFile;
                        }
                    }
                }
            }
            }
            
            return result;
      }
      
      public static Set<File> findFiles(Collection<String> extensions, Collection<File> directories) {
            Set<File> result = new HashSet<File>();
            
            for (File directory : directories){
                  result.addAll(findFiles(extensions, directory));
            }
            
            return result;
      }

      private static Collection<? extends File> findFiles(Collection<String> extensions, File directory) {
            Set<File> result = new HashSet<File>();
            
            File[] children = directory.listFiles();
            if (children == null)
                  return result; // No permission?

            for (File child : children){
                  if (child.isDirectory()) {
                        result.addAll(findFiles(extensions, child));
                  } else {
                        
                        String extension = getFileExtension(child);
                              
                        if (extension != null){
                              if (extensions.contains(extension)){
                                    result.add(child);
                              }
                        }
                  }
            }
            
            return result;
      }

      /**
       * Returns the extension of a file or null if the file does not have one (no . in name).
       * 
       * @param file
       * 
       * @return The extension, trimmed and in lowercase.
       */
01122       public static String getFileExtension(File file) {
            String name = file.getName();
            int pos = name.lastIndexOf('.');
            String extension = ((pos >= 0) && (pos < name.length() - 1)) ? name.substring(pos + 1)
                  .trim().toLowerCase() : null;
            return extension;
      }

      /**
       * New version of findPdf that uses findFiles.
       * 
       * The search pattern will be read from the preferences.
       * 
       * The [extension]-tags in this pattern will be replace by the given
       * extension parameter.
       * 
       */
01139       public static String findPdf(BibtexEntry entry, String extension, String directory) {
            return findPdf(entry, extension, new String[] { directory });
      }

      /**
       * Convenience method for findPDF. Can search multiple PDF directories.
       */
01146       public static String findPdf(BibtexEntry entry, String extension, String[] directories) {

            String regularExpression;
            if (Globals.prefs.getBoolean(JabRefPreferences.USE_REG_EXP_SEARCH_KEY)) {
                  regularExpression = Globals.prefs.get(JabRefPreferences.REG_EXP_SEARCH_EXPRESSION_KEY);
            } else {
                  regularExpression = Globals.prefs
                        .get(JabRefPreferences.DEFAULT_REG_EXP_SEARCH_EXPRESSION_KEY);
            }
            regularExpression = regularExpression.replaceAll("\\[extension\\]", extension);

            return findFile(entry, null, directories, regularExpression, true);
      }

    /**
     * Convenience menthod for findPDF. Searches for a file of the given type.
     * @param entry The BibtexEntry to search for a link for.
     * @param fileType The file type to search for.
     * @return The link to the file found, or null if not found.
     */
01166     public static String findFile(BibtexEntry entry, ExternalFileType fileType, List<String> extraDirs) {

        List<String> dirs = new ArrayList<String>();
        dirs.addAll(extraDirs);
        if (Globals.prefs.hasKey(fileType.getExtension()+"Directory")) {
            dirs.add(Globals.prefs.get(fileType.getExtension()+"Directory"));
        }
        String [] directories = dirs.toArray(new String[dirs.size()]);
        return findPdf(entry, fileType.getExtension(), directories);
    }

    /**
       * Searches the given directory and file name pattern for a file for the
       * bibtexentry.
       *
       * Used to fix:
       *
       * http://sourceforge.net/tracker/index.php?func=detail&aid=1503410&group_id=92314&atid=600309
       *
       * Requirements:
       *  - Be able to find the associated PDF in a set of given directories.
       *  - Be able to return a relative path or absolute path.
       *  - Be fast.
       *  - Allow for flexible naming schemes in the PDFs.
       *
       * Syntax scheme for file:
       * <ul>
       * <li>* Any subDir</li>
       * <li>** Any subDir (recursiv)</li>
       * <li>[key] Key from bibtex file and database</li>
       * <li>.* Anything else is taken to be a Regular expression.</li>
       * </ul>
       *
       * @param entry
       *            non-null
       * @param database
       *            non-null
       * @param directory
       *            A set of root directories to start the search from. Paths are
       *            returned relative to these directories if relative is set to
       *            true. These directories will not be expanded or anything. Use
       *            the file attribute for this.
       * @param file
       *            non-null
       *
       * @param relative
       *            whether to return relative file paths or absolute ones
       *
       * @return Will return the first file found to match the given criteria or
       *         null if none was found.
       */
01217       public static String findFile(BibtexEntry entry, BibtexDatabase database, String[] directory,
            String file, boolean relative) {

            for (int i = 0; i < directory.length; i++) {
                  String result = findFile(entry, database, directory[i], file, relative);
                  if (result != null) {
                        return result;
                  }
            }
            return null;
      }

      /**
       * Removes optional square brackets from the string s
       *
       * @param s
       * @return
       */
01235       public static String stripBrackets(String s) {
            int beginIndex = (s.startsWith("[") ? 1 : 0);
            int endIndex = (s.endsWith("]") ? s.length() - 1 : s.length());
            return s.substring(beginIndex, endIndex);
      }

      public static ArrayList<String[]> parseMethodsCalls(String calls) throws RuntimeException {

            ArrayList<String[]> result = new ArrayList<String[]>();

            char[] c = calls.toCharArray();

            int i = 0;

            while (i < c.length) {

                  int start = i;
                  if (Character.isJavaIdentifierStart(c[i])) {
                        i++;
                        while (i < c.length && (Character.isJavaIdentifierPart(c[i]) || c[i] == '.')) {
                              i++;
                        }
                        if (i < c.length && c[i] == '(') {

                              String method = calls.substring(start, i);

                              // Skip the brace
                              i++;

                              if (i < c.length){
                                    if (c[i] == '"'){
                                          // Parameter is in format "xxx"

                                          // Skip "
                                          i++;

                                          int startParam = i;
                                          i++;
                                boolean escaped = false;
                                          while (i + 1 < c.length &&
                                    !(!escaped && c[i] == '"' && c[i + 1] == ')')) {
                                if (c[i] == '\\') {
                                    escaped = !escaped;
                                }
                                else
                                    escaped = false;
                                i++;

                            }

                                          String param = calls.substring(startParam, i);
            
                                          result.add(new String[] { method, param });
                                    } else {
                                          // Parameter is in format xxx

                                          int startParam = i;

                                          while (i < c.length && c[i] != ')') {
                                                i++;
                                          }

                                          String param = calls.substring(startParam, i);

                                          result.add(new String[] { method, param });


                                    }
                              } else {
                                    // Incorrecly terminated open brace
                                    result.add(new String[] { method });
                              }
                        } else {
                              String method = calls.substring(start, i);
                              result.add(new String[] { method });
                        }
                  }
                  i++;
            }

            return result;
      }

      /**
       * Accepts a string like [author:lower] or [title:abbr] or [auth],
       * whereas the first part signifies the bibtex-field to get, or the key generator
     * field marker to use, while the others are the modifiers that will be applied.
       *
       * @param fieldAndFormat
       * @param entry
       * @param database
       * @return
       */
01328       public static String getFieldAndFormat(String fieldAndFormat, BibtexEntry entry,
            BibtexDatabase database) {

            fieldAndFormat = stripBrackets(fieldAndFormat);

            int colon = fieldAndFormat.indexOf(':');

            String beforeColon, afterColon;
            if (colon == -1) {
                  beforeColon = fieldAndFormat;
                  afterColon = null;
            } else {
                  beforeColon = fieldAndFormat.substring(0, colon);
                  afterColon = fieldAndFormat.substring(colon + 1);
            }
            beforeColon = beforeColon.trim();

            if (beforeColon.length() == 0) {
                  return null;
            }

            String fieldValue = BibtexDatabase.getResolvedField(beforeColon, entry, database);

        // If no field value was found, try to interpret it as a key generator field marker:
        if (fieldValue == null)
            fieldValue =  LabelPatternUtil.makeLabel(entry, beforeColon);

            if (fieldValue == null)
                  return null;

            if (afterColon == null || afterColon.length() == 0)
                  return fieldValue;

        String[] parts = afterColon.split(":");
        fieldValue = LabelPatternUtil.applyModifiers(fieldValue, parts, 0);
        
            return fieldValue;
      }

      /**
       * Convenience function for absolute search.
       *
       * Uses findFile(BibtexEntry, BibtexDatabase, (String)null, String, false).
       */
01372       public static String findFile(BibtexEntry entry, BibtexDatabase database, String file) {
            return findFile(entry, database, (String) null, file, false);
      }

      /**
       * Internal Version of findFile, which also accepts a current directory to
       * base the search on.
       *
       */
01381       public static String findFile(BibtexEntry entry, BibtexDatabase database, String directory,
            String file, boolean relative) {

            File root;
            if (directory == null) {
                  root = new File(".");
            } else {
                  root = new File(directory);
            }
            if (!root.exists())
                  return null;

            String found = findFile(entry, database, root, file);

            if (directory == null || !relative) {
                  return found;
            }

            if (found != null) {
                  try {
                        /**
                         * [ 1601651 ] PDF subdirectory - missing first character
                         *
                         * http://sourceforge.net/tracker/index.php?func=detail&aid=1601651&group_id=92314&atid=600306
                         */
                // Changed by M. Alver 2007.01.04:
                // Remove first character if it is a directory separator character:
                String tmp = found.substring(root.getCanonicalPath().length());
                if ((tmp.length() > 1) && (tmp.charAt(0) == File.separatorChar))
                    tmp = tmp.substring(1);
                return tmp;
                //return found.substring(root.getCanonicalPath().length());
                  } catch (IOException e) {
                        return null;
                  }
            }
            return null;
      }

      /**
       * The actual work-horse. Will find absolute filepaths starting from the
       * given directory using the given regular expression string for search.
       */
01424       protected static String findFile(BibtexEntry entry, BibtexDatabase database, File directory,
            String file) {

            if (file.startsWith("/")) {
                  directory = new File(".");
                  file = file.substring(1);
            }

            // Escape handling...
            Matcher m = Pattern.compile("([^\\\\])\\\\([^\\\\])").matcher(file);
            StringBuffer s = new StringBuffer();
            while (m.find()) {
                  m.appendReplacement(s, m.group(1) + "/" + m.group(2));
            }
            m.appendTail(s);
            file = s.toString();
            String[] fileParts = file.split("/");

            if (fileParts.length == 0)
                  return null;

            if (fileParts.length > 1) {

                  for (int i = 0; i < fileParts.length - 1; i++) {

                        String dirToProcess = fileParts[i];

                        dirToProcess = expandBrackets(dirToProcess, entry, database);

                        if (dirToProcess.matches("^.:$")) { // Windows Drive Letter
                              directory = new File(dirToProcess + "/");
                              continue;
                        }
                        if (dirToProcess.equals(".")) { // Stay in current directory
                              continue;
                        }
                        if (dirToProcess.equals("..")) {
                              directory = new File(directory.getParent());
                              continue;
                        }
                        if (dirToProcess.equals("*")) { // Do for all direct subdirs

                              File[] subDirs = directory.listFiles();
                              if (subDirs == null)
                                    return null; // No permission?

                              String restOfFileString = join(fileParts, "/", i + 1, fileParts.length);

                              for (int sub = 0; sub < subDirs.length; sub++) {
                                    if (subDirs[sub].isDirectory()) {
                                          String result = findFile(entry, database, subDirs[sub],
                                                restOfFileString);
                                          if (result != null)
                                                return result;
                                    }
                              }
                              return null;
                        }
                        // Do for all direct and indirect subdirs
                        if (dirToProcess.equals("**")) {
                              List<File> toDo = new LinkedList<File>();
                              toDo.add(directory);

                              String restOfFileString = join(fileParts, "/", i + 1, fileParts.length);

                              // Before checking the subdirs, we first check the current
                              // dir
                              String result = findFile(entry, database, directory, restOfFileString);
                              if (result != null)
                                    return result;

                              while (!toDo.isEmpty()) {

                                    // Get all subdirs of each of the elements found in toDo
                                    File[] subDirs = toDo.remove(0).listFiles();
                                    if (subDirs == null) // No permission?
                                          continue;

                                    toDo.addAll(Arrays.asList(subDirs));

                                    for (int sub = 0; sub < subDirs.length; sub++) {
                                          if (!subDirs[sub].isDirectory())
                                                continue;
                                          result = findFile(entry, database, subDirs[sub], restOfFileString);
                                          if (result != null)
                                                return result;
                                    }
                              }
                              // We already did the currentDirectory
                              return null;
                        }

                        final Pattern toMatch = Pattern
                              .compile(dirToProcess.replaceAll("\\\\\\\\", "\\\\"));

                        File[] matches = directory.listFiles(new FilenameFilter() {
                              public boolean accept(File arg0, String arg1) {
                                    return toMatch.matcher(arg1).matches();
                              }
                        });
                        if (matches == null || matches.length == 0)
                              return null;

                        directory = matches[0];

                        if (!directory.exists())
                              return null;

                  } // End process directory information
            }
            // Last step check if the given file can be found in this directory
            String filenameToLookFor = expandBrackets(fileParts[fileParts.length - 1], entry, database);

            final Pattern toMatch = Pattern.compile("^"
                  + filenameToLookFor.replaceAll("\\\\\\\\", "\\\\") + "$");

            File[] matches = directory.listFiles(new FilenameFilter() {
                  public boolean accept(File arg0, String arg1) {
                        return toMatch.matcher(arg1).matches();
                  }
            });
            if (matches == null || matches.length == 0)
                  return null;

            try {
                  return matches[0].getCanonicalPath();
            } catch (IOException e) {
                  return null;
            }
      }

      static Pattern squareBracketsPattern = Pattern.compile("\\[.*?\\]");

      /**
       * Takes a string that contains bracketed expression and expands each of
       * these using getFieldAndFormat.
       *
       * Unknown Bracket expressions are silently dropped.
       *
       * @param bracketString
       * @param entry
       * @param database
       * @return
       */
01568       public static String expandBrackets(String bracketString, BibtexEntry entry,
            BibtexDatabase database) {
            Matcher m = squareBracketsPattern.matcher(bracketString);
            StringBuffer s = new StringBuffer();
            while (m.find()) {
                  String replacement = getFieldAndFormat(m.group(), entry, database);
                  if (replacement == null)
                        replacement = "";
                  m.appendReplacement(s, replacement);
            }
            m.appendTail(s);

            return s.toString();
      }

      /**
       * Concatenate all strings in the array from index 'from' to 'to' (excluding
       * to) with the given separator.
       * 
       * Example:
       * 
       * String[] s = "ab/cd/ed".split("/"); join(s, "\\", 0, s.length) ->
       * "ab\\cd\\ed"
       * 
       * @param strings
       * @param separator
       * @param from
       * @param to
       *            Excluding strings[to]
       * @return
       */
01599       public static String join(String[] strings, String separator, int from, int to) {
            if (strings.length == 0 || from >= to)
                  return "";
            
            from = Math.max(from, 0);
            to = Math.min(strings.length, to);

            StringBuffer sb = new StringBuffer();
            for (int i = from; i < to - 1; i++) {
                  sb.append(strings[i]).append(separator);
            }
            return sb.append(strings[to - 1]).toString();
      }

      /**
       * Converts a relative filename to an absolute one, if necessary. Returns
       * null if the file does not exist.
       * 
       * Will look in each of the given dirs starting from the beginning and
       * returning the first found file to match if any.
       */
01620       public static File expandFilename(String name, String[] dir) {

            for (int i = 0; i < dir.length; i++) {
            if (dir[i] != null) {
                File result = expandFilename(name, dir[i]);
                if (result != null) {
                    return result;
                }
            }
        }

            return null;
      }

      /**
       * Converts a relative filename to an absolute one, if necessary. Returns
       * null if the file does not exist.
       */
01638       public static File expandFilename(String name, String dir) {

            File file = null;
            if (name == null || name.length() == 0)
                  return null;
            else {
                  file = new File(name);
            }

            if (!file.exists() && (dir != null)) {
            if (dir.endsWith(System.getProperty("file.separator")))
                name = dir + name;
            else
                name = dir + System.getProperty("file.separator") + name;

            // System.out.println("expanded to: "+name);
            // if (name.startsWith("ftp"))

            file = new File(name);

            if (file.exists())
                return file;
            // Ok, try to fix / and \ problems:
            if (Globals.ON_WIN) {
                // workaround for catching Java bug in regexp replacer
                // and, why, why, why ... I don't get it - wegner 2006/01/22
                try {
                    name = name.replaceAll("/", "\\\\");
                } catch (java.lang.StringIndexOutOfBoundsException exc) {
                    System.err
                        .println("An internal Java error was caused by the entry " +
                            "\"" + name + "\"");
                }
            } else
                name = name.replaceAll("\\\\", "/");
            // System.out.println("expandFilename: "+name);
            file = new File(name);
            if (!file.exists())
                file = null;
        }
        return file;
    }

      private static String findInDir(String key, String dir, OpenFileFilter off, int count) {
        if (count > 20)
            return null; // Make sure an infinite loop doesn't occur.
        File f = new File(dir);
            File[] all = f.listFiles();
            if (all == null)
                  return null; // An error occured. We may not have
            // permission to list the files.

            int numFiles = all.length;

            for (int i = 0; i < numFiles; i++) {
                  File curFile = all[i];

                  if (curFile.isFile()) {
                        String name = curFile.getName();
                        if (name.startsWith(key + ".") && off.accept(name))
                              return curFile.getPath();

                  } else if (curFile.isDirectory()) {
                        String found = findInDir(key, curFile.getPath(), off, count+1);
                        if (found != null)
                              return found;
                  }
            }
            return null;
      }

    /**
     * This methods assures all words in the given entry are recorded in their
     * respective Completers, if any.
     */
01713     public static void updateCompletersForEntry(HashMap<String, AbstractAutoCompleter> autoCompleters, BibtexEntry bibtexEntry) {
      for (Map.Entry<String, AbstractAutoCompleter> entry : autoCompleters.entrySet()){               
            AbstractAutoCompleter comp = entry.getValue();
            comp.addBibtexEntry(bibtexEntry);
        }
    }


      /**
       * Sets empty or non-existing owner fields of bibtex entries inside a List
       * to a specified default value. Timestamp field is also set. Preferences
       * are checked to see if these options are enabled.
       * 
       * @param bibs
       *            List of bibtex entries
       */
01729       public static void setAutomaticFields(Collection<BibtexEntry> bibs,
             boolean overwriteOwner, boolean overwriteTimestamp, boolean markEntries) {


            String timeStampField = Globals.prefs.get("timeStampField");

            String defaultOwner = Globals.prefs.get("defaultOwner");
            String timestamp = easyDateFormat();
            boolean globalSetOwner = Globals.prefs.getBoolean("useOwner"),
                globalSetTimeStamp = Globals.prefs.getBoolean("useTimeStamp");

        // Do not need to do anything if all options are disabled
            if (!(globalSetOwner || globalSetTimeStamp || markEntries))
                  return;

        // Iterate through all entries
            for (BibtexEntry curEntry : bibs){
            boolean setOwner = globalSetOwner &&
                (overwriteOwner || (curEntry.getField(BibtexFields.OWNER)==null));
            boolean setTimeStamp = globalSetTimeStamp &&
                (overwriteTimestamp || (curEntry.getField(timeStampField)==null));
            setAutomaticFields(curEntry, setOwner, defaultOwner, setTimeStamp, timeStampField,
                        timestamp);
            if (markEntries)
                Util.markEntry(curEntry, new NamedCompound(""));
            }

      }

      /**
       * Sets empty or non-existing owner fields of a bibtex entry to a specified
       * default value. Timestamp field is also set. Preferences are checked to
       * see if these options are enabled.
       * 
       * @param entry
       *            The entry to set fields for.
     * @param overwriteOwner
     *              Indicates whether owner should be set if it is already set.
     * @param overwriteTimestamp
     *              Indicates whether timestamp should be set if it is already set.
       */
01770       public static void setAutomaticFields(BibtexEntry entry, boolean overwriteOwner,
                                          boolean overwriteTimestamp) {
            String defaultOwner = Globals.prefs.get("defaultOwner");
            String timestamp = easyDateFormat();
        String timeStampField = Globals.prefs.get("timeStampField");
        boolean setOwner = Globals.prefs.getBoolean("useOwner") &&
            (overwriteOwner || (entry.getField(BibtexFields.OWNER)==null));
        boolean setTimeStamp = Globals.prefs.getBoolean("useTimeStamp") &&
            (overwriteTimestamp || (entry.getField(timeStampField)==null));

            setAutomaticFields(entry, setOwner, defaultOwner, setTimeStamp, timeStampField, timestamp);
      }

      private static void setAutomaticFields(BibtexEntry entry, boolean setOwner, String owner,
            boolean setTimeStamp, String timeStampField, String timeStamp) {

            // Set owner field if this option is enabled:
            if (setOwner) {
                  // No or empty owner field?
                  // if (entry.getField(Globals.OWNER) == null
                  // || ((String) entry.getField(Globals.OWNER)).length() == 0) {
                  // Set owner field to default value
                  entry.setField(BibtexFields.OWNER, owner);
                  // }
            }

            if (setTimeStamp)
                  entry.setField(timeStampField, timeStamp);
      }

      /**
       * Copies a file.
       * 
       * @param source
       *            File Source file
       * @param dest
       *            File Destination file
       * @param deleteIfExists
       *            boolean Determines whether the copy goes on even if the file
       *            exists.
       * @throws IOException
       * @return boolean Whether the copy succeeded, or was stopped due to the
       *         file already existing.
       */
01814       public static boolean copyFile(File source, File dest, boolean deleteIfExists)
            throws IOException {

            BufferedInputStream in = null;
            BufferedOutputStream out = null;
            try {
                  // Check if the file already exists.
                  if (dest.exists()) {
                        if (!deleteIfExists)
                              return false;
                        // else dest.delete();
                  }

                  in = new BufferedInputStream(new FileInputStream(source));
                  out = new BufferedOutputStream(new FileOutputStream(dest));
                  int el;
                  // int tell = 0;
                  while ((el = in.read()) >= 0) {
                        out.write(el);
                  }
            } catch (IOException ex) {
                  throw ex;
            } finally {
                  if (out != null) {
                        out.flush();
                        out.close();
                  }
                  if (in != null)
                        in.close();
            }
            return true;
      }

      /**
       * This method is called at startup, and makes necessary adaptations to
       * preferences for users from an earlier version of Jabref.
       */
01851       public static void performCompatibilityUpdate() {

            // Make sure "abstract" is not in General fields, because
            // Jabref 1.55 moves the abstract to its own tab.
            String genFields = Globals.prefs.get("generalFields");
            // pr(genFields+"\t"+genFields.indexOf("abstract"));
            if (genFields.indexOf("abstract") >= 0) {
                  // pr(genFields+"\t"+genFields.indexOf("abstract"));
                  String newGen;
                  if (genFields.equals("abstract"))
                        newGen = "";
                  else if (genFields.indexOf(";abstract;") >= 0) {
                        newGen = genFields.replaceAll(";abstract;", ";");
                  } else if (genFields.indexOf("abstract;") == 0) {
                        newGen = genFields.replaceAll("abstract;", "");
                  } else if (genFields.indexOf(";abstract") == genFields.length() - 9) {
                        newGen = genFields.replaceAll(";abstract", "");
                  } else
                        newGen = genFields;
                  // pr(newGen);
                  Globals.prefs.put("generalFields", newGen);
            }

      }

    /**
     * Collect file links from the given set of fields, and add them to the list contained
     * in the field GUIGlobals.FILE_FIELD.
     * @param database The database to modify.
     * @param fields The fields to find links in.
     * @return A CompoundEdit specifying the undo operation for the whole operation.
     */
01883     public static NamedCompound upgradePdfPsToFile(BibtexDatabase database, String[] fields) {
        NamedCompound ce = new NamedCompound(Globals.lang("Move external links to 'file' field"));
        
        for (BibtexEntry entry : database.getEntryMap().values()){
            FileListTableModel tableModel = new FileListTableModel();
            // If there are already links in the file field, keep those on top:
            String oldFileContent = entry.getField(GUIGlobals.FILE_FIELD);
            if (oldFileContent != null) {
                tableModel.setContent(oldFileContent);
            }
            int oldRowCount = tableModel.getRowCount();
            for (int j = 0; j < fields.length; j++) {
                String o = entry.getField(fields[j]);
                if (o != null) {
                    String s = o;
                    if (s.trim().length() > 0) {
                        File f = new File(s);
                        FileListEntry flEntry = new FileListEntry(f.getName(), s,
                                Globals.prefs.getExternalFileTypeByExt(fields[j]));
                        tableModel.addEntry(tableModel.getRowCount(), flEntry);
                        
                        entry.clearField(fields[j]);
                        ce.addEdit(new UndoableFieldChange(entry, fields[j], o, null));
                    }
                }
            }
            if (tableModel.getRowCount() != oldRowCount) {
                String newValue = tableModel.getStringRepresentation();
                entry.setField(GUIGlobals.FILE_FIELD, newValue);
                ce.addEdit(new UndoableFieldChange(entry, GUIGlobals.FILE_FIELD, oldFileContent, newValue));
            }
        }
        ce.end();
        return ce;
    }

    // -------------------------------------------------------------------------------

      /**
       * extends the filename with a default Extension, if no Extension '.x' could
       * be found
       */
01925       public static String getCorrectFileName(String orgName, String defaultExtension) {
            if (orgName == null)
                  return "";

            String back = orgName;
            int t = orgName.indexOf(".", 1); // hidden files Linux/Unix (?)
            if (t < 1)
                  back = back + "." + defaultExtension;

            return back;
      }

      /**
       * Quotes each and every character, e.g. '!' as &#33;. Used for verbatim
       * display of arbitrary strings that may contain HTML entities.
       */
01941       public static String quoteForHTML(String s) {
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < s.length(); ++i) {
                  sb.append("&#" + (int) s.charAt(i) + ";");
            }
            return sb.toString();
      }

      public static String quote(String s, String specials, char quoteChar) {
            return quote(s, specials, quoteChar, 0);
      }

      /**
       * Quote special characters.
       * 
       * @param s
       *            The String which may contain special characters.
       * @param specials
       *            A String containing all special characters except the quoting
       *            character itself, which is automatically quoted.
       * @param quoteChar
       *            The quoting character.
       * @param linewrap
       *            The number of characters after which a linebreak is inserted
       *            (this linebreak is undone by unquote()). Set to 0 to disable.
       * @return A String with every special character (including the quoting
       *         character itself) quoted.
       */
01969       public static String quote(String s, String specials, char quoteChar, int linewrap) {
            StringBuffer sb = new StringBuffer();
            char c;
            int linelength = 0;
            boolean isSpecial;
            for (int i = 0; i < s.length(); ++i) {
                  c = s.charAt(i);
                  isSpecial = specials.indexOf(c) >= 0 || c == quoteChar;
                  // linebreak?
                  if (linewrap > 0
                        && (++linelength >= linewrap || (isSpecial && linelength >= linewrap - 1))) {
                        sb.append(quoteChar);
                        sb.append('\n');
                        linelength = 0;
                  }
                  if (isSpecial) {
                        sb.append(quoteChar);
                        ++linelength;
                  }
                  sb.append(c);
            }
            return sb.toString();
      }

      /**
       * Unquote special characters.
       * 
       * @param s
       *            The String which may contain quoted special characters.
       * @param quoteChar
       *            The quoting character.
       * @return A String with all quoted characters unquoted.
       */
02002       public static String unquote(String s, char quoteChar) {
            StringBuffer sb = new StringBuffer();
            char c;
            boolean quoted = false;
            for (int i = 0; i < s.length(); ++i) {
                  c = s.charAt(i);
                  if (quoted) { // append literally...
                        if (c != '\n') // ...unless newline
                              sb.append(c);
                        quoted = false;
                  } else if (c != quoteChar) {
                        sb.append(c);
                  } else { // quote char
                        quoted = true;
                  }
            }
            return sb.toString();
      }

      /**
       * Quote all regular expression meta characters in s, in order to search for
       * s literally.
       */
02025       public static String quoteMeta(String s) {
            // work around a bug: trailing backslashes have to be quoted
            // individually
            int i = s.length() - 1;
            StringBuffer bs = new StringBuffer("");
            while ((i >= 0) && (s.charAt(i) == '\\')) {
                  --i;
                  bs.append("\\\\");
            }
            s = s.substring(0, i + 1);
            return "\\Q" + s.replaceAll("\\\\E", "\\\\E\\\\\\\\E\\\\Q") + "\\E" + bs.toString();
      }

      /*
       * This method "tidies" up e.g. a keyword string, by alphabetizing the words
       * and removing all duplicates.
       */
      public static String sortWordsAndRemoveDuplicates(String text) {

            String[] words = text.split(", ");
            SortedSet<String> set = new TreeSet<String>();
            for (int i = 0; i < words.length; i++)
                  set.add(words[i]);
            StringBuffer sb = new StringBuffer();
            for (Iterator<String> i = set.iterator(); i.hasNext();) {
                  sb.append(i.next());
                  sb.append(", ");
            }
            if (sb.length() > 2)
                  sb.delete(sb.length() - 2, sb.length());
            String result = sb.toString();
            return result.length() > 2 ? result : "";
      }

      /**
       * Warns the user of undesired side effects of an explicit
       * assignment/removal of entries to/from this group. Currently there are
       * four types of groups: AllEntriesGroup, SearchGroup - do not support
       * explicit assignment. ExplicitGroup - never modifies entries. KeywordGroup -
       * only this modifies entries upon assignment/removal. Modifications are
       * acceptable unless they affect a standard field (such as "author") besides
       * the "keywords" field.
       * 
       * @param parent
       *            The Component used as a parent when displaying a confirmation
       *            dialog.
       * @return true if the assignment has no undesired side effects, or the user
       *         chose to perform it anyway. false otherwise (this indicates that
       *         the user has aborted the assignment).
       */
02075       public static boolean warnAssignmentSideEffects(AbstractGroup[] groups, BibtexEntry[] entries,
            BibtexDatabase db, Component parent) {
            Vector<String> affectedFields = new Vector<String>();
            for (int k = 0; k < groups.length; ++k) {
                  if (groups[k] instanceof KeywordGroup) {
                        KeywordGroup kg = (KeywordGroup) groups[k];
                        String field = kg.getSearchField().toLowerCase();
                        if (field.equals("keywords"))
                              continue; // this is not undesired
                        for (int i = 0, len = BibtexFields.numberOfPublicFields(); i < len; ++i) {
                              if (field.equals(BibtexFields.getFieldName(i))) {
                                    affectedFields.add(field);
                                    break;
                              }
                        }
                  }
            }
            if (affectedFields.size() == 0)
                  return true; // no side effects

            // show a warning, then return
            StringBuffer message = // JZTODO lyrics...
            new StringBuffer("This action will modify the following field(s)\n"
                  + "in at least one entry each:\n");
            for (int i = 0; i < affectedFields.size(); ++i)
                  message.append(affectedFields.elementAt(i)).append("\n");
            message.append("This could cause undesired changes to "
                  + "your entries, so it is\nrecommended that you change the grouping field "
                  + "in your group\ndefinition to \"keywords\" or a non-standard name."
                  + "\n\nDo you still want to continue?");
            int choice = JOptionPane.showConfirmDialog(parent, message, Globals.lang("Warning"),
                  JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
            return choice != JOptionPane.NO_OPTION;

            // if (groups instanceof KeywordGroup) {
            // KeywordGroup kg = (KeywordGroup) groups;
            // String field = kg.getSearchField().toLowerCase();
            // if (field.equals("keywords"))
            // return true; // this is not undesired
            // for (int i = 0; i < GUIGlobals.ALL_FIELDS.length; ++i) {
            // if (field.equals(GUIGlobals.ALL_FIELDS[i])) {
            // // show a warning, then return
            // String message = Globals // JZTODO lyrics...
            // .lang(
            // "This action will modify the \"%0\" field "
            // + "of your entries.\nThis could cause undesired changes to "
            // + "your entries, so it is\nrecommended that you change the grouping
            // field "
            // + "in your group\ndefinition to \"keywords\" or a non-standard name."
            // + "\n\nDo you still want to continue?",
            // field);
            // int choice = JOptionPane.showConfirmDialog(parent, message,
            // Globals.lang("Warning"), JOptionPane.YES_NO_OPTION,
            // JOptionPane.WARNING_MESSAGE);
            // return choice != JOptionPane.NO_OPTION;
            // }
            // }
            // }
            // return true; // found no side effects
      }

      // ========================================================
      // lot of abreviations in medline
      // PKC etc convert to {PKC} ...
      // ========================================================
      static Pattern titleCapitalPattern = Pattern.compile("[A-Z]+");

      /**
       * Wrap all uppercase letters, or sequences of uppercase letters, in curly
       * braces. Ignore letters within a pair of # character, as these are part of
       * a string label that should not be modified.
       * 
       * @param s
       *            The string to modify.
       * @return The resulting string after wrapping capitals.
       */
02151       public static String putBracesAroundCapitals(String s) {

            boolean inString = false, isBracing = false, escaped = false;
            int inBrace = 0;
            StringBuffer buf = new StringBuffer();
            for (int i = 0; i < s.length(); i++) {
                  // Update variables based on special characters:
                  int c = s.charAt(i);
                  if (c == '{')
                        inBrace++;
                  else if (c == '}')
                        inBrace--;
                  else if (!escaped && (c == '#'))
                        inString = !inString;

                  // See if we should start bracing:
                  if ((inBrace == 0) && !isBracing && !inString && Character.isLetter((char) c)
                        && Character.isUpperCase((char) c)) {

                        buf.append('{');
                        isBracing = true;
                  }

                  // See if we should close a brace set:
                  if (isBracing && !(Character.isLetter((char) c) && Character.isUpperCase((char) c))) {

                        buf.append('}');
                        isBracing = false;
                  }

                  // Add the current character:
                  buf.append((char) c);

                  // Check if we are entering an escape sequence:
                  if ((c == '\\') && !escaped)
                        escaped = true;
                  else
                        escaped = false;

            }
            // Check if we have an unclosed brace:
            if (isBracing)
                  buf.append('}');

            return buf.toString();

            /*
             * if (s.length() == 0) return s; // Protect against ArrayIndexOutOf....
             * StringBuffer buf = new StringBuffer();
             * 
             * Matcher mcr = titleCapitalPattern.matcher(s.substring(1)); while
             * (mcr.find()) { String replaceStr = mcr.group();
             * mcr.appendReplacement(buf, "{" + replaceStr + "}"); }
             * mcr.appendTail(buf); return s.substring(0, 1) + buf.toString();
             */
      }

      static Pattern bracedTitleCapitalPattern = Pattern.compile("\\{[A-Z]+\\}");

      /**
       * This method looks for occurences of capital letters enclosed in an
       * arbitrary number of pairs of braces, e.g. "{AB}" or "{{T}}". All of these
       * pairs of braces are removed.
       * 
       * @param s
       *            The String to analyze.
       * @return A new String with braces removed.
       */
02219       public static String removeBracesAroundCapitals(String s) {
            String previous = s;
            while ((s = removeSingleBracesAroundCapitals(s)).length() < previous.length()) {
                  previous = s;
            }
            return s;
      }

      /**
       * This method looks for occurences of capital letters enclosed in one pair
       * of braces, e.g. "{AB}". All these are replaced by only the capitals in
       * between the braces.
       * 
       * @param s
       *            The String to analyze.
       * @return A new String with braces removed.
       */
02236       public static String removeSingleBracesAroundCapitals(String s) {
            Matcher mcr = bracedTitleCapitalPattern.matcher(s);
            StringBuffer buf = new StringBuffer();
            while (mcr.find()) {
                  String replaceStr = mcr.group();
                  mcr.appendReplacement(buf, replaceStr.substring(1, replaceStr.length() - 1));
            }
            mcr.appendTail(buf);
            return buf.toString();
      }

      /**
       * This method looks up what kind of external binding is used for the given
       * field, and constructs on OpenFileFilter suitable for browsing for an
       * external file.
       * 
       * @param fieldName
       *            The BibTeX field in question.
       * @return The file filter.
       */
02256       public static OpenFileFilter getFileFilterForField(String fieldName) {
            String s = BibtexFields.getFieldExtras(fieldName);
            final String ext = "." + fieldName.toLowerCase();
            final OpenFileFilter off;
            if (s.equals("browseDocZip"))
                  off = new OpenFileFilter(new String[] { ext, ext + ".gz", ext + ".bz2" });
            else
                  off = new OpenFileFilter(new String[] { ext });
            return off;
      }

      /**
       * This method can be used to display a "rich" error dialog which offers the
       * entire stack trace for an exception.
       * 
       * @param parent
       * @param e
       */
02274       public static void showQuickErrorDialog(JFrame parent, String title, Exception e) {
            // create and configure a text area - fill it with exception text.
            final JPanel pan = new JPanel(), details = new JPanel();
            final CardLayout crd = new CardLayout();
            pan.setLayout(crd);
            final JTextArea textArea = new JTextArea();
            textArea.setFont(new Font("Sans-Serif", Font.PLAIN, 10));
            textArea.setEditable(false);
            StringWriter writer = new StringWriter();
            e.printStackTrace(new PrintWriter(writer));
            textArea.setText(writer.toString());
            JLabel lab = new JLabel(e.getMessage());
            JButton flip = new JButton(Globals.lang("Details"));

            FormLayout layout = new FormLayout("left:pref", "");
            DefaultFormBuilder builder = new DefaultFormBuilder(layout);
            builder.append(lab);
            builder.nextLine();
            builder.append(Box.createVerticalGlue());
            builder.nextLine();
            builder.append(flip);
            final JPanel simple = builder.getPanel();

            // stuff it in a scrollpane with a controlled size.
            JScrollPane scrollPane = new JScrollPane(textArea);
            scrollPane.setPreferredSize(new Dimension(350, 150));
            details.setLayout(new BorderLayout());
            details.add(scrollPane, BorderLayout.CENTER);

            flip.addActionListener(new ActionListener() {
                  public void actionPerformed(ActionEvent event) {
                        crd.show(pan, "details");
                  }
            });
            pan.add(simple, "simple");
            pan.add(details, "details");
            // pass the scrollpane to the joptionpane.
            JOptionPane.showMessageDialog(parent, pan, title, JOptionPane.ERROR_MESSAGE);
      }

      public static String wrapHTML(String s, final int lineWidth) {
            StringBuffer sb = new StringBuffer();
            StringTokenizer tok = new StringTokenizer(s);
            int charsLeft = lineWidth;
            while (tok.hasMoreTokens()) {
                  String word = tok.nextToken();
                  if (charsLeft == lineWidth) { // fresh line
                        sb.append(word);
                        charsLeft -= word.length();
                        if (charsLeft <= 0) {
                              sb.append("<br>\n");
                              charsLeft = lineWidth;
                        }
                  } else { // continue previous line
                        if (charsLeft < word.length() + 1) {
                              sb.append("<br>\n");
                              sb.append(word);
                              if (word.length() >= lineWidth - 1) {
                                    sb.append("<br>\n");
                                    charsLeft = lineWidth;
                              } else {
                                    sb.append(" ");
                                    charsLeft = lineWidth - word.length() - 1;
                              }
                        } else {
                              sb.append(' ').append(word);
                              charsLeft -= word.length() + 1;
                        }
                  }
            }
            return sb.toString();
      }

      /**
       * Creates a String containing the current date (and possibly time),
       * formatted according to the format set in preferences under the key
       * "timeStampFormat".
       * 
       * @return The date string.
       */
02354       public static String easyDateFormat() {
            // Date today = new Date();
            return easyDateFormat(new Date());
      }

      /**
       * Creates a readable Date string from the parameter date. The format is set
       * in preferences under the key "timeStampFormat".
       * 
       * @return The formatted date string.
       */
02365       public static String easyDateFormat(Date date) {
            // first use, create an instance
            if (dateFormatter == null) {
                  String format = Globals.prefs.get("timeStampFormat");
                  dateFormatter = new SimpleDateFormat(format);
            }
            return dateFormatter.format(date);
      }

      public static void markEntry(BibtexEntry be, NamedCompound ce) {
            Object o = be.getField(BibtexFields.MARKED);
            if ((o != null) && (o.toString().indexOf(Globals.prefs.WRAPPED_USERNAME) >= 0))
                  return;
            String newValue;
            if (o == null) {
                  newValue = Globals.prefs.WRAPPED_USERNAME;
            } else {
                  StringBuffer sb = new StringBuffer(o.toString());
                  // sb.append(' ');
                  sb.append(Globals.prefs.WRAPPED_USERNAME);
                  newValue = sb.toString();
            }
            ce.addEdit(new UndoableFieldChange(be, BibtexFields.MARKED, be
                  .getField(BibtexFields.MARKED), newValue));
            be.setField(BibtexFields.MARKED, newValue);
      }

      public static void unmarkEntry(BibtexEntry be, BibtexDatabase database, NamedCompound ce) {
            Object o = be.getField(BibtexFields.MARKED);
            if (o != null) {
                  String s = o.toString();
                  if (s.equals("0")) {
                        unmarkOldStyle(be, database, ce);
                        return;
                  }

                  int piv = 0, hit;
                  StringBuffer sb = new StringBuffer();
                  while ((hit = s.indexOf(Globals.prefs.WRAPPED_USERNAME, piv)) >= 0) {
                        if (hit > 0)
                              sb.append(s.substring(piv, hit));
                        piv = hit + Globals.prefs.WRAPPED_USERNAME.length();
                  }
                  if (piv < s.length() - 1) {
                        sb.append(s.substring(piv));
                  }
                  String newVal = sb.length() > 0 ? sb.toString() : null;
                  ce.addEdit(new UndoableFieldChange(be, BibtexFields.MARKED, be
                        .getField(BibtexFields.MARKED), newVal));
                  be.setField(BibtexFields.MARKED, newVal);
            }
      }

      /**
       * An entry is marked with a "0", not in the new style with user names. We
       * want to unmark it as transparently as possible. Since this shouldn't
       * happen too often, we do it by scanning the "owner" fields of the entire
       * database, collecting all user names. We then mark the entry for all users
       * except the current one. Thus only the user who unmarks will see that it
       * is unmarked, and we get rid of the old-style marking.
       * 
       * @param be
       * @param ce
       */
02429       private static void unmarkOldStyle(BibtexEntry be, BibtexDatabase database, NamedCompound ce) {
            TreeSet<Object> owners = new TreeSet<Object>();
            for (BibtexEntry entry : database.getEntries()){
                  Object o = entry.getField(BibtexFields.OWNER);
                  if (o != null)
                        owners.add(o);
                  // System.out.println("Owner: "+entry.getField(Globals.OWNER));
            }
            owners.remove(Globals.prefs.get("defaultOwner"));
            StringBuffer sb = new StringBuffer();
            for (Iterator<Object> i = owners.iterator(); i.hasNext();) {
                  sb.append('[');
                  sb.append(i.next().toString());
                  sb.append(']');
            }
            String newVal = sb.toString();
            if (newVal.length() == 0)
                  newVal = null;
            ce.addEdit(new UndoableFieldChange(be, BibtexFields.MARKED, be
                  .getField(BibtexFields.MARKED), newVal));
            be.setField(BibtexFields.MARKED, newVal);

      }

      public static boolean isMarked(BibtexEntry be) {
            Object fieldVal = be.getField(BibtexFields.MARKED);
            if (fieldVal == null)
                  return false;
            String s = (String) fieldVal;
            return (s.equals("0") || (s.indexOf(Globals.prefs.WRAPPED_USERNAME) >= 0));
      }

      /**
       * Set a given field to a given value for all entries in a Collection. This
       * method DOES NOT update any UndoManager, but returns a relevant
       * CompoundEdit that should be registered by the caller.
       * 
       * @param entries
       *            The entries to set the field for.
       * @param field
       *            The name of the field to set.
       * @param text
       *            The value to set. This value can be null, indicating that the
       *            field should be cleared.
       * @param overwriteValues
       *            Indicate whether the value should be set even if an entry
       *            already has the field set.
       * @return A CompoundEdit for the entire operation.
       */
02478       public static UndoableEdit massSetField(Collection<BibtexEntry> entries, String field, String text,
            boolean overwriteValues) {

            NamedCompound ce = new NamedCompound(Globals.lang("Set field"));
            for (BibtexEntry entry : entries){
                  String oldVal = entry.getField(field);
                  // If we are not allowed to overwrite values, check if there is a
                  // nonempty
                  // value already for this entry:
                  if (!overwriteValues && (oldVal != null) && ((oldVal).length() > 0))
                        continue;
                  if (text != null)
                        entry.setField(field, text);
                  else
                        entry.clearField(field);
                  ce.addEdit(new UndoableFieldChange(entry, field, oldVal, text));
            }
            ce.end();
            return ce;
      }

    /**
     * Move contents from one field to another for a Collection of entries.
     * @param entries The entries to do this operation for.
     * @param field The field to move contents from.
     * @param newField The field to move contents into.
     * @param overwriteValues If true, overwrites any existing values in the new field.
     *          If false, makes no change for entries with existing value in the new field.
     * @return A CompoundEdit for the entire operation.
     */
02508     public static UndoableEdit massRenameField(Collection<BibtexEntry> entries, String field,
                String newField, boolean overwriteValues) {
        NamedCompound ce = new NamedCompound(Globals.lang("Rename field"));
            for (BibtexEntry entry : entries){
                  String valToMove = entry.getField(field);
            // If there is no value, do nothing:
            if ((valToMove == null) || (valToMove.length() == 0))
                continue;
            // If we are not allowed to overwrite values, check if there is a
                  // nonempy value already for this entry for the new field:
            String valInNewField = entry.getField(newField);
            if (!overwriteValues && (valInNewField != null) && (valInNewField.length() > 0))
                continue;

                  entry.setField(newField, valToMove);
            ce.addEdit(new UndoableFieldChange(entry, newField, valInNewField,valToMove));
            entry.clearField(field);
            ce.addEdit(new UndoableFieldChange(entry, field, valToMove, null));
            }
            ce.end();
            return ce;
    }

      /**
       * Make a list of supported character encodings that can encode all
       * characters in the given String.
       * 
       * @param characters
       *            A String of characters that should be supported by the
       *            encodings.
       * @return A List of character encodings
       */
02540       public static List<String> findEncodingsForString(String characters) {
            List<String> encodings = new ArrayList<String>();
            for (int i = 0; i < Globals.ENCODINGS.length; i++) {
                  CharsetEncoder encoder = Charset.forName(Globals.ENCODINGS[i]).newEncoder();
                  if (encoder.canEncode(characters))
                        encodings.add(Globals.ENCODINGS[i]);
            }
            return encodings;
      }

      /**
       * Will convert a two digit year using the following scheme (describe at
       * http://www.filemaker.com/help/02-Adding%20and%20view18.html):
       * 
       * If a two digit year is encountered they are matched against the last 69
       * years and future 30 years.
       * 
       * For instance if it is the year 1992 then entering 23 is taken to be 1923
       * but if you enter 23 in 1993 then it will evaluate to 2023.
       * 
       * @param year
       *            The year to convert to 4 digits.
       * @return
       */
02564       public static String toFourDigitYear(String year) {
            if (thisYear == 0) {
                  thisYear = Calendar.getInstance().get(Calendar.YEAR);
            }
            return toFourDigitYear(year, thisYear);
      }

      public static int thisYear;

      /**
       * Will convert a two digit year using the following scheme (describe at
       * http://www.filemaker.com/help/02-Adding%20and%20view18.html):
       * 
       * If a two digit year is encountered they are matched against the last 69
       * years and future 30 years.
       * 
       * For instance if it is the year 1992 then entering 23 is taken to be 1923
       * but if you enter 23 in 1993 then it will evaluate to 2023.
       * 
       * @param year
       *            The year to convert to 4 digits.
       * @return
       */
02587       public static String toFourDigitYear(String year, int thisYear) {
            if (year.length() != 2)
                  return year;
            try {
                  int thisYearTwoDigits = thisYear % 100;
                  int thisCentury = thisYear - thisYearTwoDigits;

                  int yearNumber = Integer.parseInt(year);

                  if (yearNumber == thisYearTwoDigits) {
                        return String.valueOf(thisYear);
                  }
                  // 20 , 90
                  // 99 > 30
                  if ((yearNumber + 100 - thisYearTwoDigits) % 100 > 30) {
                        if (yearNumber < thisYearTwoDigits) {
                              return String.valueOf(thisCentury + yearNumber);
                        } else {
                              return String.valueOf(thisCentury - 100 + yearNumber);
                        }
                  } else {
                        if (yearNumber < thisYearTwoDigits) {
                              return String.valueOf(thisCentury + 100 + yearNumber);
                        } else {
                              return String.valueOf(thisCentury + yearNumber);
                        }
                  }
            } catch (NumberFormatException e) {
                  return year;
            }
      }

      /**
       * Will return an integer indicating the month of the entry from 0 to 11.
       * 
       * -1 signals a unknown month string.
       * 
       * This method accepts three types of months given:
       *  - Single and Double Digit months from 1 to 12 (01 to 12)
       *  - 3 Digit BibTex strings (jan, feb, mar...)
       *  - Full English Month identifiers.
       * 
       * @param month
       * @return
       */
02632       public static int getMonthNumber(String month) {

            month = month.replaceAll("#", "").toLowerCase();

            for (int i = 0; i < Globals.MONTHS.length; i++) {
                  if (month.startsWith(Globals.MONTHS[i])) {
                        return i;
                  }
            }

            try {
                  return Integer.parseInt(month) - 1;
            } catch (NumberFormatException e) {
            }
            return -1;
      }


    /**
     * Encodes a two-dimensional String array into a single string, using ':' and
     * ';' as separators. The characters ':' and ';' are escaped with '\'.
     * @param values The String array.
     * @return The encoded String.
     */
02656     public static String encodeStringArray(String[][] values) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < values.length; i++) {
            sb.append(encodeStringArray(values[i]));
            if (i < values.length-1)
                sb.append(';');
        }
        return sb.toString();
    }

    /**
     * Encodes a String array into a single string, using ':' as separator.
     * The characters ':' and ';' are escaped with '\'.
     * @param entry The String array.
     * @return The encoded String.
     */
02672     public static String encodeStringArray(String[] entry) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < entry.length; i++) {
            sb.append(encodeString(entry[i]));
            if (i < entry.length-1)
                sb.append(':');

        }
        return sb.toString();
    }

    /**
     * Decodes an encoded double String array back into array form. The array
     * is assumed to be square, and delimited by the characters ';' (first dim) and
     * ':' (second dim).
     * @param value The encoded String to be decoded.
     * @return The decoded String array.
     */
02690     public static String[][] decodeStringDoubleArray(String value) {
        ArrayList<ArrayList<String>> newList = new ArrayList<ArrayList<String>>();
        StringBuilder sb = new StringBuilder();
        ArrayList<String> thisEntry = new ArrayList<String>();
        boolean escaped = false;
        for (int i=0; i<value.length(); i++) {
            char c = value.charAt(i);
            if (!escaped && (c == '\\')) {
                escaped = true;
                continue;
            }
            else if (!escaped && (c == ':')) {
                thisEntry.add(sb.toString());
                sb = new StringBuilder();
            }
            else if (!escaped && (c == ';')) {
                thisEntry.add(sb.toString());
                sb = new StringBuilder();
                newList.add(thisEntry);
                thisEntry = new ArrayList<String>();
            }
            else sb.append(c);
            escaped = false;
        }
        if (sb.length() > 0)
            thisEntry.add(sb.toString());
        if (thisEntry.size() > 0)
            newList.add(thisEntry);

        // Convert to String[][]:
        String[][] res = new String[newList.size()][];
        for (int i = 0; i < res.length; i++) {
            res[i] = new String[newList.get(i).size()];
            for (int j = 0; j < res[i].length; j++) {
                res[i][j] = newList.get(i).get(j);
            }
        }
        return res;
    }

    private static String encodeString(String s) {
        if (s == null)
            return null;
        StringBuilder sb = new StringBuilder();
        for (int i=0; i<s.length(); i++) {
            char c = s.charAt(i);
            if ((c == ';') || (c == ':') || (c == '\\'))
                sb.append('\\');
            sb.append(c);
        }
        return sb.toString();
    }

    /**
       * Static equals that can also return the right result when one of the
       * objects is null.
       * 
       * @param one
       *            The object whose equals method is called if the first is not
       *            null.
       * @param two
       *            The object passed to the first one if the first is not null.
       * @return <code>one == null ? two == null : one.equals(two);</code>
       */
02754       public static boolean equals(Object one, Object two) {
            return one == null ? two == null : one.equals(two);
      }

      /**
       * Returns the given string but with the first character turned into an
       * upper case character.
       * 
       * Example: testTest becomes TestTest
       * 
       * @param string
       *            The string to change the first character to upper case to.
       * @return A string has the first character turned to upper case and the
       *         rest unchanged from the given one.
       */
02769       public static String toUpperFirstLetter(String string){
            if (string == null)
                  throw new IllegalArgumentException();
            
            if (string.length() == 0)
                  return string;
            
            return Character.toUpperCase(string.charAt(0)) + string.substring(1);
      }


    /**
     * Run an AbstractWorker's methods using Spin features to put each method
     * on the correct thread.
     * @param worker The worker to run.
     * @throws Throwable 
     */
02786     public static void runAbstractWorker(AbstractWorker worker) throws Throwable {
        // This part uses Spin's features:
        Worker wrk = worker.getWorker();
        // The Worker returned by getWorker() has been wrapped
        // by Spin.off(), which makes its methods be run in
        // a different thread from the EDT.
        CallBack clb = worker.getCallBack();

        worker.init(); // This method runs in this same thread, the EDT.
        // Useful for initial GUI actions, like printing a message.

        // The CallBack returned by getCallBack() has been wrapped
        // by Spin.over(), which makes its methods be run on
        // the EDT.
        wrk.run(); // Runs the potentially time-consuming action
        // without freezing the GUI. The magic is that THIS line
        // of execution will not continue until run() is finished.
        clb.update(); // Runs the update() method on the EDT.
    }

    /**
     * This method checks whether there is a lock file for the given file. If
     * there is, it waits for 500 ms. This is repeated until the lock is gone
     * or we have waited the maximum number of times.
     *
     * @param file The file to check the lock for.
     * @param maxWaitCount The maximum number of times to wait.
     * @return true if the lock file is gone, false if it is still there.
     */
02815     public static boolean waitForFileLock(File file, int maxWaitCount) {
        // Check if the file is locked by another JabRef user:
        int lockCheckCount = 0;
        while (Util.hasLockFile(file)) {

            System.out.println("File locked... waiting");
            if (lockCheckCount++ == maxWaitCount) {
                System.out.println("Giving up wait.");
                return false;
            }
            try { Thread.sleep(500); } catch (InterruptedException ex) {}
        }
        return true;
    }

    /**
     * Check whether a lock file exists for this file.
     * @param file The file to check.
     * @return true if a lock file exists, false otherwise.
     */
02835     public static boolean hasLockFile(File file) {
        File lock = new File(file.getPath()+ SaveSession.LOCKFILE_SUFFIX);
        return lock.exists();
    }

    /**
     * Find the lock file's last modified time, if it has a lock file.
     * @param file The file to check.
     * @return the last modified time if lock file exists, -1 otherwise.
     */
02845     public static long getLockFileTimeStamp(File file) {
        File lock = new File(file.getPath()+ SaveSession.LOCKFILE_SUFFIX);
        return lock.exists() ? lock.lastModified() : -1;
    }

        /**
     * Check if a lock file exists, and delete it if it does.
     * @return true if the lock file existed, false otherwise.
     * @throws IOException if something goes wrong.
     */
02855     public static boolean deleteLockFile(File file) {
        File lock = new File(file.getPath()+SaveSession.LOCKFILE_SUFFIX);
        if (!lock.exists()) {
            return false;
        }
        lock.delete();
        return true;
    }
}

Generated by  Doxygen 1.6.0   Back to index