/*
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
*/
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.....
*
*/
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.
*/
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.
*/
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.
*/
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
*/
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.
*/
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
*/
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.
*/
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.
*/
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
*/
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.
*/
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
*/
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
*/
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.
*/
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
*/
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"
*/
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.
*/
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.
*
*/
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.
*/
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.
*/
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.
*/
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
*/
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
*/
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).
*/
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.
*
*/
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.
*/
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
*/
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
*/
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.
*/
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.
*/
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.
*/
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
*/
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.
*/
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.
*/
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.
*/
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.
*/
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
*/
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 !. Used for verbatim
* display of arbitrary strings that may contain HTML entities.
*/
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.
*/
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.
*/
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.
*/
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).
*/
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.
*/
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.
*/
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.
*/
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.
*/
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
*/
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.
*/
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.
*/
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
*/
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.
*/
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.
*/
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
*/
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
*/
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
*/
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
*/
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.
*/
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.
*/
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.
*/
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>
*/
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.
*/
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
*/
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.
*/
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.
*/
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.
*/
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.
*/
public static boolean deleteLockFile(File file) {
File lock = new File(file.getPath()+SaveSession.LOCKFILE_SUFFIX);
if (!lock.exists()) {
return false;
}
lock.delete();
return true;
}
}