PageRenderTime 39ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 1ms

/src/main/java/net/sf/jabref/Util.java

https://github.com/edukempf/jabref
Java | 3514 lines | 2282 code | 311 blank | 921 comment | 583 complexity | e0f03a63a637dde3a4cf26453b617b0c MD5 | raw file
  1. /* Copyright (C) 2003-2012 JabRef contributors.
  2. This program is free software; you can redistribute it and/or modify
  3. it under the terms of the GNU General Public License as published by
  4. the Free Software Foundation; either version 2 of the License, or
  5. (at your option) any later version.
  6. This program is distributed in the hope that it will be useful,
  7. but WITHOUT ANY WARRANTY; without even the implied warranty of
  8. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  9. GNU General Public License for more details.
  10. You should have received a copy of the GNU General Public License along
  11. with this program; if not, write to the Free Software Foundation, Inc.,
  12. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  13. */
  14. // created by : Morten O. Alver 2003
  15. package net.sf.jabref;
  16. import java.awt.BorderLayout;
  17. import java.awt.CardLayout;
  18. import java.awt.Color;
  19. import java.awt.Component;
  20. import java.awt.Desktop;
  21. import java.awt.Dimension;
  22. import java.awt.Font;
  23. import java.awt.event.ActionEvent;
  24. import java.awt.event.ActionListener;
  25. import java.io.*;
  26. import java.net.MalformedURLException;
  27. import java.net.URI;
  28. import java.net.URISyntaxException;
  29. import java.net.URL;
  30. import java.net.URLConnection;
  31. import java.net.URLDecoder;
  32. import java.nio.charset.Charset;
  33. import java.nio.charset.CharsetEncoder;
  34. import java.text.NumberFormat;
  35. import java.text.SimpleDateFormat;
  36. import java.util.ArrayList;
  37. import java.util.Arrays;
  38. import java.util.Calendar;
  39. import java.util.Collection;
  40. import java.util.Collections;
  41. import java.util.Date;
  42. import java.util.HashMap;
  43. import java.util.HashSet;
  44. import java.util.LinkedList;
  45. import java.util.List;
  46. import java.util.Map;
  47. import java.util.Set;
  48. import java.util.StringTokenizer;
  49. import java.util.TreeSet;
  50. import java.util.Vector;
  51. import java.util.regex.Matcher;
  52. import java.util.regex.Pattern;
  53. import javax.swing.Action;
  54. import javax.swing.ActionMap;
  55. import javax.swing.BorderFactory;
  56. import javax.swing.Box;
  57. import javax.swing.InputMap;
  58. import javax.swing.JButton;
  59. import javax.swing.JComponent;
  60. import javax.swing.JDialog;
  61. import javax.swing.JFrame;
  62. import javax.swing.JLabel;
  63. import javax.swing.JOptionPane;
  64. import javax.swing.JPanel;
  65. import javax.swing.JProgressBar;
  66. import javax.swing.JRootPane;
  67. import javax.swing.JScrollPane;
  68. import javax.swing.JTextArea;
  69. import javax.swing.SwingUtilities;
  70. import javax.swing.undo.AbstractUndoableEdit;
  71. import javax.swing.undo.UndoableEdit;
  72. import net.sf.jabref.autocompleter.AbstractAutoCompleter;
  73. import net.sf.jabref.export.SaveSession;
  74. import net.sf.jabref.export.layout.Layout;
  75. import net.sf.jabref.export.layout.LayoutHelper;
  76. import net.sf.jabref.external.ExternalFileType;
  77. import net.sf.jabref.external.ExternalFileTypeEntryEditor;
  78. import net.sf.jabref.external.RegExpFileSearch;
  79. import net.sf.jabref.external.UnknownExternalFileType;
  80. import net.sf.jabref.groups.AbstractGroup;
  81. import net.sf.jabref.groups.KeywordGroup;
  82. import net.sf.jabref.gui.FileListEntry;
  83. import net.sf.jabref.gui.FileListEntryEditor;
  84. import net.sf.jabref.gui.FileListTableModel;
  85. import net.sf.jabref.labelPattern.LabelPatternUtil;
  86. import net.sf.jabref.net.URLDownload;
  87. import net.sf.jabref.undo.NamedCompound;
  88. import net.sf.jabref.undo.UndoableFieldChange;
  89. import net.sf.jabref.util.FileNameCleaner;
  90. import com.jgoodies.forms.builder.DefaultFormBuilder;
  91. import com.jgoodies.forms.layout.FormLayout;
  92. /**
  93. * utility functions
  94. */
  95. public class Util {
  96. /**
  97. * A static Object for date formatting. Please do not create the object
  98. * here, because there are some references from the Globals class.....
  99. *
  100. */
  101. private static SimpleDateFormat dateFormatter = null;
  102. /*
  103. * Colors are defined here.
  104. *
  105. */
  106. public static Color fieldsCol = new Color(180, 180, 200);
  107. /*
  108. * Integer values for indicating result of duplicate check (for entries):
  109. *
  110. */
  111. final static int TYPE_MISMATCH = -1, NOT_EQUAL = 0, EQUAL = 1, EMPTY_IN_ONE = 2,
  112. EMPTY_IN_TWO = 3, EMPTY_IN_BOTH = 4;
  113. final static NumberFormat idFormat;
  114. public static Pattern remoteLinkPattern = Pattern.compile("[a-z]+://.*");
  115. public static int MARK_COLOR_LEVELS = 6,
  116. MAX_MARKING_LEVEL = MARK_COLOR_LEVELS-1,
  117. IMPORT_MARK_LEVEL = MARK_COLOR_LEVELS;
  118. public static Pattern markNumberPattern = Pattern.compile(JabRefPreferences.getInstance().MARKING_WITH_NUMBER_PATTERN);
  119. static {
  120. idFormat = NumberFormat.getInstance();
  121. idFormat.setMinimumIntegerDigits(8);
  122. idFormat.setGroupingUsed(false);
  123. }
  124. public static int getMinimumIntegerDigits(){
  125. return idFormat.getMinimumIntegerDigits();
  126. }
  127. public static void pr(String s) {
  128. Globals.logger(s);
  129. }
  130. public static String nCase(String s) {
  131. // Make first character of String uppercase, and the
  132. // rest lowercase.
  133. if (s.length() > 1)
  134. return s.substring(0, 1).toUpperCase() + s.substring(1, s.length()).toLowerCase();
  135. else
  136. return s.toUpperCase();
  137. }
  138. public static String checkName(String s) {
  139. // Append '.bib' to the string unless it ends with that.
  140. if (s.length() < 4 || !s.substring(s.length() - 4).equalsIgnoreCase(".bib")) {
  141. return s + ".bib";
  142. }
  143. return s;
  144. }
  145. private static int idCounter = 0;
  146. public synchronized static String createNeutralId() {
  147. return idFormat.format(idCounter++);
  148. }
  149. /**
  150. * This method sets the location of a Dialog such that it is centered with
  151. * regard to another window, but not outside the screen on the left and the
  152. * top.
  153. */
  154. public static void placeDialog(java.awt.Dialog diag, java.awt.Container win) {
  155. diag.setLocationRelativeTo(win);
  156. }
  157. /**
  158. * This method translates a field or string from Bibtex notation, with
  159. * possibly text contained in " " or { }, and string references,
  160. * concatenated by '#' characters, into Bibkeeper notation, where string
  161. * references are enclosed in a pair of '#' characters.
  162. */
  163. public static String parseField(String content) {
  164. if (content.length() == 0)
  165. return content;
  166. String[] strings = content.split("#");
  167. StringBuffer result = new StringBuffer();
  168. for (String string : strings) {
  169. String s = string.trim();
  170. if (s.length() > 0) {
  171. char c = s.charAt(0);
  172. // String reference or not?
  173. if (c == '{' || c == '"') {
  174. result.append(shaveString(string));
  175. } else {
  176. // This part should normally be a string reference, but if it's
  177. // a pure number, it is not.
  178. String s2 = shaveString(s);
  179. try {
  180. Integer.parseInt(s2);
  181. // If there's no exception, it's a number.
  182. result.append(s2);
  183. } catch (NumberFormatException ex) {
  184. // otherwise append with hashes...
  185. result.append("#").append(s2).append("#");
  186. }
  187. }
  188. }
  189. }
  190. return result.toString();
  191. }
  192. /**
  193. * Will return the publication date of the given bibtex entry in conformance
  194. * to ISO 8601, i.e. either YYYY or YYYY-MM.
  195. *
  196. * @param entry
  197. * @return will return the publication date of the entry or null if no year
  198. * was found.
  199. */
  200. public static String getPublicationDate(BibtexEntry entry) {
  201. Object o = entry.getField("year");
  202. if (o == null)
  203. return null;
  204. String year = toFourDigitYear(o.toString());
  205. o = entry.getField("month");
  206. if (o != null) {
  207. int month = Util.getMonthNumber(o.toString());
  208. if (month != -1) {
  209. return year + "-" + (month + 1 < 10 ? "0" : "") + (month + 1);
  210. }
  211. }
  212. return year;
  213. }
  214. public static String shaveString(String s) {
  215. // returns the string, after shaving off whitespace at the beginning
  216. // and end, and removing (at most) one pair of braces or " surrounding
  217. // it.
  218. if (s == null)
  219. return null;
  220. char ch, ch2;
  221. int beg = 0, end = s.length();
  222. // We start out assuming nothing will be removed.
  223. boolean begok = false, endok = false;
  224. while (!begok) {
  225. if (beg < s.length()) {
  226. ch = s.charAt(beg);
  227. if (Character.isWhitespace(ch))
  228. beg++;
  229. else
  230. begok = true;
  231. } else
  232. begok = true;
  233. }
  234. while (!endok) {
  235. if (end > beg + 1) {
  236. ch = s.charAt(end - 1);
  237. if (Character.isWhitespace(ch))
  238. end--;
  239. else
  240. endok = true;
  241. } else
  242. endok = true;
  243. }
  244. if (end > beg + 1) {
  245. ch = s.charAt(beg);
  246. ch2 = s.charAt(end - 1);
  247. if (((ch == '{') && (ch2 == '}')) || ((ch == '"') && (ch2 == '"'))) {
  248. beg++;
  249. end--;
  250. }
  251. }
  252. s = s.substring(beg, end);
  253. return s;
  254. }
  255. public static String rtrim(String s) {
  256. return s.replaceAll("\\s+$","");
  257. }
  258. /**
  259. * This method returns a String similar to the one passed in, except that it
  260. * is molded into a form that is acceptable for bibtex.
  261. *
  262. * Watch-out that the returned string might be of length 0 afterwards.
  263. *
  264. * @param key
  265. * mayBeNull
  266. */
  267. public static String checkLegalKey(String key) {
  268. if (key == null)
  269. return null;
  270. if (!JabRefPreferences.getInstance().getBoolean("enforceLegalBibtexKey")) {
  271. // User doesn't want us to enforce legal characters. We must still look
  272. // for whitespace and some characters such as commas, since these would
  273. // interfere with parsing:
  274. StringBuilder newKey = new StringBuilder();
  275. for (int i = 0; i < key.length(); i++) {
  276. char c = key.charAt(i);
  277. if (!Character.isWhitespace(c) && (c != '{') && (c != '\\') && (c != '"')
  278. && (c != '}') && (c != ','))
  279. newKey.append(c);
  280. }
  281. return newKey.toString();
  282. }
  283. StringBuilder newKey = new StringBuilder();
  284. for (int i = 0; i < key.length(); i++) {
  285. char c = key.charAt(i);
  286. if (!Character.isWhitespace(c) && (c != '#') && (c != '{') && (c != '\\') && (c != '"')
  287. && (c != '}') && (c != '~') && (c != ',') && (c != '^') && (c != '\''))
  288. newKey.append(c);
  289. }
  290. // Replace non-english characters like umlauts etc. with a sensible
  291. // letter or letter combination that bibtex can accept.
  292. String newKeyS = replaceSpecialCharacters(newKey.toString());
  293. return newKeyS;
  294. }
  295. /**
  296. * Replace non-english characters like umlauts etc. with a sensible letter
  297. * or letter combination that bibtex can accept. The basis for replacement
  298. * is the HashMap GLobals.UNICODE_CHARS.
  299. */
  300. public static String replaceSpecialCharacters(String s) {
  301. for (Map.Entry<String, String> chrAndReplace : Globals.UNICODE_CHARS.entrySet()){
  302. s = s.replaceAll(chrAndReplace.getKey(), chrAndReplace.getValue());
  303. }
  304. return s;
  305. }
  306. static public String _wrap2(String in, int wrapAmount) {
  307. // The following line cuts out all whitespace and replaces them with
  308. // single
  309. // spaces:
  310. // in = in.replaceAll("[ ]+"," ").replaceAll("[\\t]+"," ");
  311. // StringBuffer out = new StringBuffer(in);
  312. StringBuffer out = new StringBuffer(in.replaceAll("[ \\t\\r]+", " "));
  313. int p = in.length() - wrapAmount;
  314. int lastInserted = -1;
  315. while (p > 0) {
  316. p = out.lastIndexOf(" ", p);
  317. if (p <= 0 || p <= 20)
  318. break;
  319. int lbreak = out.indexOf("\n", p);
  320. System.out.println(lbreak + " " + lastInserted);
  321. if ((lbreak > p) && ((lastInserted >= 0) && (lbreak < lastInserted))) {
  322. p = lbreak - wrapAmount;
  323. } else {
  324. out.insert(p, "\n\t");
  325. lastInserted = p;
  326. p -= wrapAmount;
  327. }
  328. }
  329. return out.toString();
  330. }
  331. static public String wrap2(String in, int wrapAmount) {
  332. return net.sf.jabref.imports.FieldContentParser.wrap(in, wrapAmount);
  333. }
  334. static public String __wrap2(String in, int wrapAmount) {
  335. // The following line cuts out all whitespace except line breaks, and
  336. // replaces
  337. // with single spaces. Line breaks are padded with a tab character:
  338. StringBuffer out = new StringBuffer(in.replaceAll("[ \\t\\r]+", " "));
  339. int p = 0;
  340. // int lastInserted = -1;
  341. while (p < out.length()) {
  342. int q = out.indexOf(" ", p + wrapAmount);
  343. if ((q < 0) || (q >= out.length()))
  344. break;
  345. int lbreak = out.indexOf("\n", p);
  346. // System.out.println(lbreak);
  347. if ((lbreak > p) && (lbreak < q)) {
  348. p = lbreak + 1;
  349. int piv = lbreak + 1;
  350. if ((out.length() > piv) && !(out.charAt(piv) == '\t'))
  351. out.insert(piv, "\n\t");
  352. } else {
  353. // System.out.println(q+" "+out.length());
  354. out.deleteCharAt(q);
  355. out.insert(q, "\n\t");
  356. p = q + 1;
  357. }
  358. }
  359. return out.toString();// .replaceAll("\n", "\n\t");
  360. }
  361. public static TreeSet<String> findDeliminatedWordsInField(BibtexDatabase db, String field,
  362. String deliminator) {
  363. TreeSet<String> res = new TreeSet<String>();
  364. for (String s : db.getKeySet()){
  365. BibtexEntry be = db.getEntryById(s);
  366. Object o = be.getField(field);
  367. if (o != null) {
  368. String fieldValue = o.toString().trim();
  369. StringTokenizer tok = new StringTokenizer(fieldValue, deliminator);
  370. while (tok.hasMoreTokens())
  371. res.add(nCase(tok.nextToken().trim()));
  372. }
  373. }
  374. return res;
  375. }
  376. /**
  377. * Returns a HashMap containing all words used in the database in the given
  378. * field type. Characters in <code>remove</code> are not included.
  379. *
  380. * @param db
  381. * a <code>BibtexDatabase</code> value
  382. * @param field
  383. * a <code>String</code> value
  384. * @param remove
  385. * a <code>String</code> value
  386. * @return a <code>HashSet</code> value
  387. */
  388. public static TreeSet<String> findAllWordsInField(BibtexDatabase db, String field, String remove) {
  389. TreeSet<String> res = new TreeSet<String>();
  390. StringTokenizer tok;
  391. for (String s : db.getKeySet()){
  392. BibtexEntry be = db.getEntryById(s);
  393. Object o = be.getField(field);
  394. if (o != null) {
  395. tok = new StringTokenizer(o.toString(), remove, false);
  396. while (tok.hasMoreTokens())
  397. res.add(nCase(tok.nextToken().trim()));
  398. }
  399. }
  400. return res;
  401. }
  402. /**
  403. * Finds all authors' last names in all the given fields for the given database.
  404. * @param db The database.
  405. * @param fields The fields to look in.
  406. * @return a set containing the names.
  407. */
  408. public static Set<String> findAuthorLastNames(BibtexDatabase db, List<String> fields) {
  409. Set<String> res = new TreeSet<String>();
  410. for (String s : db.getKeySet()){
  411. BibtexEntry be = db.getEntryById(s);
  412. for (String field : fields) {
  413. String val = be.getField(field);
  414. if ((val != null) && (val.length() > 0)) {
  415. AuthorList al = AuthorList.getAuthorList(val);
  416. for (int i=0; i<al.size(); i++) {
  417. AuthorList.Author a = al.getAuthor(i);
  418. String lastName = a.getLast();
  419. if ((lastName != null) && (lastName.length() > 0))
  420. res.add(lastName);
  421. }
  422. }
  423. }
  424. }
  425. return res;
  426. }
  427. /**
  428. * Takes a String array and returns a string with the array's elements
  429. * delimited by a certain String.
  430. *
  431. * @param strs
  432. * String array to convert.
  433. * @param delimiter
  434. * String to use as delimiter.
  435. * @return Delimited String.
  436. */
  437. public static String stringArrayToDelimited(String[] strs, String delimiter) {
  438. if ((strs == null) || (strs.length == 0))
  439. return "";
  440. if (strs.length == 1)
  441. return strs[0];
  442. StringBuffer sb = new StringBuffer();
  443. for (int i = 0; i < strs.length - 1; i++) {
  444. sb.append(strs[i]);
  445. sb.append(delimiter);
  446. }
  447. sb.append(strs[strs.length - 1]);
  448. return sb.toString();
  449. }
  450. /**
  451. * Takes a delimited string, splits it and returns
  452. *
  453. * @param names
  454. * a <code>String</code> value
  455. * @return a <code>String[]</code> value
  456. */
  457. public static String[] delimToStringArray(String names, String delimiter) {
  458. if (names == null)
  459. return null;
  460. return names.split(delimiter);
  461. }
  462. /**
  463. * Creates a substring from a text
  464. *
  465. * @param text
  466. * @param i
  467. * @param terminateOnEndBraceOnly
  468. * @return
  469. */
  470. public static String getPart(String text, int i, boolean terminateOnEndBraceOnly) {
  471. char c;
  472. int count = 0;
  473. StringBuffer part = new StringBuffer();
  474. // advance to first char and skip whitespace
  475. i++;
  476. while (i < text.length() && Character.isWhitespace(text.charAt(i))){
  477. i++;
  478. }
  479. // then grab whathever is the first token (counting braces)
  480. while (i < text.length()){
  481. c = text.charAt(i);
  482. if (!terminateOnEndBraceOnly && count == 0 && Character.isWhitespace(c)) {
  483. i--; // end argument and leave whitespace for further
  484. // processing
  485. break;
  486. }
  487. if (c == '}' && --count < 0)
  488. break;
  489. else if (c == '{')
  490. count++;
  491. part.append(c);
  492. i++;
  493. }
  494. return part.toString();
  495. }
  496. /**
  497. * Open a http/pdf/ps viewer for the given link string.
  498. */
  499. public static void openExternalViewer(MetaData metaData, String link, String fieldName)
  500. throws IOException {
  501. if (fieldName.equals("ps") || fieldName.equals("pdf")) {
  502. // Find the default directory for this field type:
  503. String[] dir = metaData.getFileDirectory(fieldName);
  504. File file = expandFilename(link, dir);
  505. // Check that the file exists:
  506. if ((file == null) || !file.exists()) {
  507. throw new IOException(Globals.lang("File not found") + " (" + fieldName + "): '"
  508. + link + "'.");
  509. }
  510. link = file.getCanonicalPath();
  511. // Use the correct viewer even if pdf and ps are mixed up:
  512. String[] split = file.getName().split("\\.");
  513. if (split.length >= 2) {
  514. if (split[split.length - 1].equalsIgnoreCase("pdf"))
  515. fieldName = "pdf";
  516. else if (split[split.length - 1].equalsIgnoreCase("ps")
  517. || (split.length >= 3 && split[split.length - 2].equalsIgnoreCase("ps")))
  518. fieldName = "ps";
  519. }
  520. } else if (fieldName.equals("doi")) {
  521. fieldName = "url";
  522. // sanitizing is done below at the treatment of "URL"
  523. // in sanatizeUrl a doi-link is correctly treated
  524. } else if (fieldName.equals("eprint")) {
  525. fieldName = "url";
  526. link = sanitizeUrl(link);
  527. // Check to see if link field already contains a well formated URL
  528. if (!link.startsWith("http://")) {
  529. link = Globals.ARXIV_LOOKUP_PREFIX + link;
  530. }
  531. }
  532. if (fieldName.equals("url")) { // html
  533. try {
  534. link = sanitizeUrl(link);
  535. ExternalFileType fileType = Globals.prefs.getExternalFileTypeByExt("html");
  536. openExternalFilePlatformIndependent(fileType, link);
  537. } catch (IOException e) {
  538. System.err.println(Globals.lang("Error_opening_file_'%0'.", link));
  539. e.printStackTrace();
  540. }
  541. } else if (fieldName.equals("ps")) {
  542. try {
  543. if (Globals.ON_MAC) {
  544. ExternalFileType type = Globals.prefs.getExternalFileTypeByExt("ps");
  545. String viewer = type != null ? type.getOpenWith() : Globals.prefs.get("psviewer");
  546. String[] cmd = { "/usr/bin/open", "-a", viewer, link };
  547. Runtime.getRuntime().exec(cmd);
  548. } else if (Globals.ON_WIN) {
  549. openFileOnWindows(link, true);
  550. /*
  551. * cmdArray[0] = Globals.prefs.get("psviewer"); cmdArray[1] =
  552. * link; Process child = Runtime.getRuntime().exec(
  553. * cmdArray[0] + " " + cmdArray[1]);
  554. */
  555. } else {
  556. ExternalFileType type = Globals.prefs.getExternalFileTypeByExt("ps");
  557. String viewer = type != null ? type.getOpenWith() : "xdg-open";
  558. String[] cmdArray = new String[2];
  559. cmdArray[0] = viewer;
  560. cmdArray[1] = link;
  561. Runtime.getRuntime().exec(cmdArray);
  562. }
  563. } catch (IOException e) {
  564. System.err.println("An error occured on the command: "
  565. + Globals.prefs.get("psviewer") + " " + link);
  566. }
  567. } else if (fieldName.equals("pdf")) {
  568. try {
  569. if (Globals.ON_MAC) {
  570. ExternalFileType type = Globals.prefs.getExternalFileTypeByExt("pdf");
  571. String viewer = type != null ? type.getOpenWith() : Globals.prefs.get("psviewer");
  572. String[] cmd = { "/usr/bin/open", "-a", viewer, link };
  573. Runtime.getRuntime().exec(cmd);
  574. } else if (Globals.ON_WIN) {
  575. openFileOnWindows(link, true);
  576. /*
  577. * String[] spl = link.split("\\\\"); StringBuffer sb = new
  578. * StringBuffer(); for (int i = 0; i < spl.length; i++) { if
  579. * (i > 0) sb.append("\\"); if (spl[i].indexOf(" ") >= 0)
  580. * spl[i] = "\"" + spl[i] + "\""; sb.append(spl[i]); }
  581. * //pr(sb.toString()); link = sb.toString();
  582. *
  583. * String cmd = "cmd.exe /c start " + link;
  584. *
  585. * Process child = Runtime.getRuntime().exec(cmd);
  586. */
  587. } else {
  588. ExternalFileType type = Globals.prefs.getExternalFileTypeByExt("pdf");
  589. String viewer = type != null ? type.getOpenWith() : Globals.prefs.get("psviewer");
  590. String[] cmdArray = new String[2];
  591. cmdArray[0] = viewer;
  592. cmdArray[1] = link;
  593. // Process child = Runtime.getRuntime().exec(cmdArray[0]+"
  594. // "+cmdArray[1]);
  595. Runtime.getRuntime().exec(cmdArray);
  596. }
  597. } catch (IOException e) {
  598. e.printStackTrace();
  599. System.err.println("An error occured on the command: "
  600. + Globals.prefs.get("pdfviewer") + " #" + link);
  601. System.err.println(e.getMessage());
  602. }
  603. } else {
  604. System.err
  605. .println("Message: currently only PDF, PS and HTML files can be opened by double clicking");
  606. }
  607. }
  608. /**
  609. * Opens a file on a Windows system, using its default viewer.
  610. *
  611. * @param link
  612. * The file name.
  613. * @param localFile
  614. * true if it is a local file, not an URL.
  615. * @throws IOException
  616. */
  617. public static void openFileOnWindows(String link, boolean localFile) throws IOException {
  618. /*
  619. * if (localFile) { String[] spl = link.split("\\\\"); StringBuffer sb =
  620. * new StringBuffer(); for (int i = 0; i < spl.length; i++) { if (i > 0)
  621. * sb.append("\\"); if (spl[i].indexOf(" ") >= 0) spl[i] = "\"" + spl[i] +
  622. * "\""; sb.append(spl[i]); } link = sb.toString(); }
  623. */
  624. link = link.replaceAll("&", "\"&\"").replaceAll(" ", "\" \"");
  625. // Bug fix for:
  626. // http://sourceforge.net/tracker/index.php?func=detail&aid=1489454&group_id=92314&atid=600306
  627. String cmd;
  628. if (Globals.osName.startsWith("Windows 9")) {
  629. cmd = "command.com /c start " + link;
  630. } else {
  631. cmd = "cmd.exe /c start " + link;
  632. }
  633. Runtime.getRuntime().exec(cmd);
  634. }
  635. /**
  636. * Opens a file on a Windows system, using the given application.
  637. *
  638. * @param link The file name.
  639. * @param application Link to the app that opens the file.
  640. * @throws IOException
  641. */
  642. public static void openFileWithApplicationOnWindows(String link, String application)
  643. throws IOException {
  644. link = link.replaceAll("&", "\"&\"").replaceAll(" ", "\" \"");
  645. Runtime.getRuntime().exec(application + " " + link);
  646. }
  647. /**
  648. * Open an external file, attempting to use the correct viewer for it.
  649. *
  650. * @param metaData
  651. * The MetaData for the database this file belongs to.
  652. * @param link
  653. * The file name.
  654. * @return false if the link couldn't be resolved, true otherwise.
  655. */
  656. public static boolean openExternalFileAnyFormat(final MetaData metaData, String link,
  657. final ExternalFileType fileType) throws IOException {
  658. boolean httpLink = false;
  659. if (remoteLinkPattern.matcher(link.toLowerCase()).matches()) {
  660. httpLink = true;
  661. }
  662. /*if (link.toLowerCase().startsWith("file://")) {
  663. link = link.substring(7);
  664. }
  665. final String ln = link;
  666. if (remoteLinkPattern.matcher(link.toLowerCase()).matches()) {
  667. (new Thread(new Runnable() {
  668. public void run() {
  669. openRemoteExternalFile(metaData, ln, fileType);
  670. }
  671. })).start();
  672. return true;
  673. }*/
  674. //boolean httpLink = link.toLowerCase().startsWith("http:")
  675. // || link.toLowerCase().startsWith("ftp:");
  676. // For other platforms we'll try to find the file type:
  677. File file = new File(link);
  678. if (!httpLink) {
  679. File tmp = expandFilename(metaData, link);
  680. if (tmp != null)
  681. file = tmp;
  682. }
  683. // Check if we have arrived at a file type, and either an http link or an existing file:
  684. if ((httpLink || file.exists()) && (fileType != null)) {
  685. // Open the file:
  686. String filePath = httpLink ? link : file.getPath();
  687. openExternalFilePlatformIndependent(fileType, filePath);
  688. return true;
  689. } else {
  690. return false;
  691. // No file matched the name, or we didn't know the file type.
  692. }
  693. }
  694. private static void openExternalFilePlatformIndependent(ExternalFileType fileType, String filePath) throws IOException {
  695. if (Globals.ON_MAC) {
  696. // Use "-a <application>" if the app is specified, and just "open <filename>" otherwise:
  697. String[] cmd = ((fileType.getOpenWith() != null) && (fileType.getOpenWith().length() > 0)) ?
  698. new String[] { "/usr/bin/open", "-a", fileType.getOpenWith(), filePath } :
  699. new String[] { "/usr/bin/open", filePath };
  700. Runtime.getRuntime().exec(cmd);
  701. } else if (Globals.ON_WIN) {
  702. if ((fileType.getOpenWith() != null) && (fileType.getOpenWith().length() > 0)) {
  703. // Application is specified. Use it:
  704. openFileWithApplicationOnWindows(filePath, fileType.getOpenWith());
  705. } else
  706. openFileOnWindows(filePath, true);
  707. } else {
  708. // Use the given app if specified, and the universal "xdg-open" otherwise:
  709. String[] openWith;
  710. if ((fileType.getOpenWith() != null) && (fileType.getOpenWith().length() > 0))
  711. openWith = fileType.getOpenWith().split(" ");
  712. else
  713. openWith = new String[] {"xdg-open"};
  714. String[] cmdArray = new String[openWith.length+1];
  715. System.arraycopy(openWith, 0, cmdArray, 0, openWith.length);
  716. cmdArray[cmdArray.length-1] = filePath;
  717. Runtime.getRuntime().exec(cmdArray);
  718. }
  719. }
  720. public static void openRemoteExternalFile(final MetaData metaData,
  721. final String link, final ExternalFileType fileType) {
  722. File temp = null;
  723. try {
  724. temp = File.createTempFile("jabref-link", "."+fileType.getExtension());
  725. temp.deleteOnExit();
  726. System.out.println("Downloading to '"+temp.getPath()+"'");
  727. URLDownload ud = new URLDownload(null, new URL(link), temp);
  728. ud.download();
  729. System.out.println("Done");
  730. } catch (MalformedURLException ex) {
  731. ex.printStackTrace();
  732. } catch (IOException ex) {
  733. ex.printStackTrace();
  734. }
  735. final String ln = temp.getPath();
  736. SwingUtilities.invokeLater(new Runnable() {
  737. public void run() {
  738. try {
  739. openExternalFileAnyFormat(metaData, ln, fileType);
  740. } catch (IOException ex) {
  741. ex.printStackTrace();
  742. }
  743. }
  744. });
  745. }
  746. public static boolean openExternalFileUnknown(JabRefFrame frame, BibtexEntry entry, MetaData metaData,
  747. String link, UnknownExternalFileType fileType) throws IOException {
  748. String cancelMessage = Globals.lang("Unable to open file.");
  749. String[] options = new String[] {Globals.lang("Define '%0'", fileType.getName()),
  750. Globals.lang("Change file type"), Globals.lang("Cancel")};
  751. String defOption = options[0];
  752. int answer = JOptionPane.showOptionDialog(frame, Globals.lang("This external link is of the type '%0', which is undefined. What do you want to do?",
  753. fileType.getName()),
  754. Globals.lang("Undefined file type"), JOptionPane.YES_NO_CANCEL_OPTION,
  755. JOptionPane.QUESTION_MESSAGE, null, options, defOption);
  756. if (answer == JOptionPane.CANCEL_OPTION) {
  757. frame.output(cancelMessage);
  758. return false;
  759. }
  760. else if (answer == JOptionPane.YES_OPTION) {
  761. // User wants to define the new file type. Show the dialog:
  762. ExternalFileType newType = new ExternalFileType(fileType.getName(), "", "", "", "new");
  763. ExternalFileTypeEntryEditor editor = new ExternalFileTypeEntryEditor(frame, newType);
  764. editor.setVisible(true);
  765. if (editor.okPressed()) {
  766. // Get the old list of types, add this one, and update the list in prefs:
  767. List<ExternalFileType> fileTypes = new ArrayList<ExternalFileType>();
  768. ExternalFileType[] oldTypes = Globals.prefs.getExternalFileTypeSelection();
  769. Collections.addAll(fileTypes, oldTypes);
  770. fileTypes.add(newType);
  771. Collections.sort(fileTypes);
  772. Globals.prefs.setExternalFileTypes(fileTypes);
  773. // Finally, open the file:
  774. return openExternalFileAnyFormat(metaData, link, newType);
  775. } else {
  776. // Cancelled:
  777. frame.output(cancelMessage);
  778. return false;
  779. }
  780. }
  781. else {
  782. // User wants to change the type of this link.
  783. // First get a model of all file links for this entry:
  784. FileListTableModel tModel = new FileListTableModel();
  785. String oldValue = entry.getField(GUIGlobals.FILE_FIELD);
  786. tModel.setContent(oldValue);
  787. FileListEntry flEntry = null;
  788. // Then find which one we are looking at:
  789. for (int i=0; i<tModel.getRowCount(); i++) {
  790. FileListEntry iEntry = tModel.getEntry(i);
  791. if (iEntry.getLink().equals(link)) {
  792. flEntry = iEntry;
  793. break;
  794. }
  795. }
  796. if (flEntry == null) {
  797. // This shouldn't happen, so I'm not sure what to put in here:
  798. throw new RuntimeException("Could not find the file list entry "+link+" in "+entry.toString());
  799. }
  800. FileListEntryEditor editor = new FileListEntryEditor(frame, flEntry, false, true, metaData);
  801. editor.setVisible(true, false);
  802. if (editor.okPressed()) {
  803. // Store the changes and add an undo edit:
  804. String newValue = tModel.getStringRepresentation();
  805. UndoableFieldChange ce = new UndoableFieldChange(entry, GUIGlobals.FILE_FIELD,
  806. oldValue, newValue);
  807. entry.setField(GUIGlobals.FILE_FIELD, newValue);
  808. frame.basePanel().undoManager.addEdit(ce);
  809. frame.basePanel().markBaseChanged();
  810. // Finally, open the link:
  811. return openExternalFileAnyFormat(metaData, flEntry.getLink(), flEntry.getType());
  812. } else {
  813. // Cancelled:
  814. frame.output(cancelMessage);
  815. return false;
  816. }
  817. }
  818. }
  819. /**
  820. * Make sure an URL is "portable", in that it doesn't contain bad characters
  821. * that break the open command in some OSes.
  822. *
  823. * A call to this method will also remove \\url{} enclosings and clean DOI links.
  824. *
  825. * @param link :the URL to sanitize.
  826. * @return Sanitized URL
  827. */
  828. public static String sanitizeUrl(String link) {
  829. link = link.trim();
  830. // First check if it is enclosed in \\url{}. If so, remove
  831. // the wrapper.
  832. if (link.startsWith("\\url{") && link.endsWith("}"))
  833. link = link.substring(5, link.length() - 1);
  834. if (link.matches("^doi:/*.*")){
  835. // Remove 'doi:'
  836. link = link.replaceFirst("^doi:/*", "");
  837. link = Globals.DOI_LOOKUP_PREFIX + link;
  838. }
  839. // converts doi-only link to full http address
  840. // Morten Alver 6 Nov 2012: this extracts a nonfunctional DOI from some complete
  841. // http addresses (e.g. http://onlinelibrary.wiley.com/doi/10.1002/rra.999/abstract, where
  842. // the trailing "/abstract" is included but doesn't lead to a resolvable DOI).
  843. // To prevent mangling of working URLs I'm disabling this check if the link is already
  844. // a full http link:
  845. if (checkForPlainDOI(link) && !link.startsWith("http://")) {
  846. link = Globals.DOI_LOOKUP_PREFIX + getDOI(link);
  847. }
  848. link = link.replaceAll("\\+", "%2B");
  849. try {
  850. link = URLDecoder.decode(link, "UTF-8");
  851. } catch (UnsupportedEncodingException ignored) {
  852. }
  853. /**
  854. * Fix for: [ 1574773 ] sanitizeUrl() breaks ftp:// and file:///
  855. *
  856. * http://sourceforge.net/tracker/index.php?func=detail&aid=1574773&group_id=92314&atid=600306
  857. */
  858. try {
  859. return new URI(null, link, null).toASCIIString();
  860. } catch (URISyntaxException e) {
  861. return link;
  862. }
  863. }
  864. /**
  865. * Searches the given directory and subdirectories for a pdf file with name
  866. * as given + ".pdf"
  867. */
  868. public static String findPdf(String key, String extension, String directory, OpenFileFilter off) {
  869. // String filename = key + "."+extension;
  870. /*
  871. * Simon Fischer's patch for replacing a regexp in keys before
  872. * converting to filename:
  873. *
  874. * String regex = Globals.prefs.get("basenamePatternRegex"); if ((regex !=
  875. * null) && (regex.trim().length() > 0)) { String replacement =
  876. * Globals.prefs.get("basenamePatternReplacement"); key =
  877. * key.replaceAll(regex, replacement); }
  878. */
  879. if (!directory.endsWith(System.getProperty("file.separator")))
  880. directory += System.getProperty("file.separator");
  881. String found = findInDir(key, directory, off, 0);
  882. if (found != null)
  883. return found.substring(directory.length());
  884. else
  885. return null;
  886. }
  887. public static Map<BibtexEntry, List<File>> findAssociatedFiles(Collection<BibtexEntry> entries, Collection<String> extensions, Collection<File> directories){
  888. HashMap<BibtexEntry, List<File>> result = new HashMap<BibtexEntry, List<File>>();
  889. // First scan directories
  890. Set<File> filesWithExtension = findFiles(extensions, directories);
  891. // Initialize Result-Set
  892. for (BibtexEntry entry : entries){
  893. result.put(entry, new ArrayList<File>());
  894. }
  895. boolean exactOnly = Globals.prefs.getBoolean(JabRefPreferences.AUTOLINK_EXACT_KEY_ONLY);
  896. // Now look for keys
  897. nextFile:
  898. for (File file : filesWithExtension){
  899. String name = file.getName();
  900. int dot = name.lastIndexOf('.');
  901. // First, look for exact matches:
  902. for (BibtexEntry entry : entries){
  903. String citeKey = entry.getCiteKey();
  904. if ((citeKey != null) && (citeKey.length() > 0)) {
  905. if (dot > 0) {
  906. if (name.substring(0, dot).equals(citeKey)) {
  907. result.get(entry).add(file);
  908. continue nextFile;
  909. }
  910. }
  911. }
  912. }
  913. // If we get here, we didn't find any exact matches. If non-exact
  914. // matches are allowed, try to find one:
  915. if (!exactOnly) {
  916. for (BibtexEntry entry : entries){
  917. String citeKey = entry.getCiteKey();
  918. if ((citeKey != null) && (citeKey.length() > 0)) {
  919. if (name.startsWith(citeKey)){
  920. result.get(entry).add(file);
  921. continue nextFile;
  922. }
  923. }
  924. }
  925. }
  926. }
  927. return result;
  928. }
  929. public static Set<File> findFiles(Collection<String> extensions, Collection<File> directories) {
  930. Set<File> result = new HashSet<File>();
  931. for (File directory : directories){
  932. result.addAll(findFiles(extensions, directory));
  933. }
  934. return result;
  935. }
  936. private static Collection<? extends File> findFiles(Collection<String> extensions, File directory) {
  937. Set<File> result = new HashSet<File>();
  938. File[] children = directory.listFiles();
  939. if (children == null)
  940. return result; // No permission?
  941. for (File child : children){
  942. if (child.isDirectory()) {
  943. result.addAll(findFiles(extensions, child));
  944. } else {
  945. String extension = getFileExtension(child);
  946. if (extension != null){
  947. if (extensions.contains(extension)){
  948. result.add(child);
  949. }
  950. }
  951. }
  952. }
  953. return result;
  954. }
  955. /**
  956. * Returns the extension of a file or null if the file does not have one (no . in name).
  957. *
  958. * @param file
  959. *
  960. * @return The extension, trimmed and in lowercase.
  961. */
  962. public static String getFileExtension(File file) {
  963. String name = file.getName();
  964. int pos = name.lastIndexOf('.');
  965. String extension = ((pos >= 0) && (pos < name.length() - 1)) ? name.substring(pos + 1)
  966. .trim().toLowerCase() : null;
  967. return extension;
  968. }
  969. /**
  970. * New version of findPdf that uses findFiles.
  971. *
  972. * The search pattern will be read from the preferences.
  973. *
  974. * The [extension]-tags in this pattern will be replace by the given
  975. * extension parameter.
  976. *
  977. */
  978. public static String findPdf(BibtexEntry entry, String extension, String directory) {
  979. return findPdf(entry, extension, new String[] { directory });
  980. }
  981. /**
  982. * Convenience method for findPDF. Can search multiple PDF directories.
  983. */
  984. public static String findPdf(BibtexEntry entry, String extension, String[] directories) {
  985. String regularExpression;
  986. if (Globals.prefs.getBoolean(JabRefPreferences.USE_REG_EXP_SEARCH_KEY)) {
  987. regularExpression = Globals.prefs.get(JabRefPreferences.REG_EXP_SEARCH_EXPRESSION_KEY);
  988. } else {
  989. regularExpression = Globals.prefs
  990. .get(JabRefPreferences.DEFAULT_REG_EXP_SEARCH_EXPRESSION_KEY);
  991. }
  992. regularExpression = regularExpression.replaceAll("\\[extension\\]", extension);
  993. return findFile(entry, null, directories, regularExpression, true);
  994. }
  995. /**
  996. * Convenience menthod for findPDF. Searches for a file of the given type.
  997. * @param entry The BibtexEntry to search for a link for.
  998. * @param fileType The file type to search for.
  999. * @return The link to the file found, or null if not found.
  1000. */
  1001. public static String findFile(BibtexEntry entry, ExternalFileType fileType, List<String> extraDirs) {
  1002. List<String> dirs = new ArrayList<String>();
  1003. dirs.addAll(extraDirs);
  1004. if (Globals.prefs.hasKey(fileType.getExtension()+"Directory")) {
  1005. dirs.add(Globals.prefs.get(fileType.getExtension()+"Directory"));
  1006. }
  1007. String [] directories = dirs.toArray(new String[dirs.size()]);
  1008. return findPdf(entry, fileType.getExtension(), directories);
  1009. }
  1010. /**
  1011. * Searches the given directory and file name pattern for a file for the
  1012. * bibtexentry.
  1013. *
  1014. * Used to fix:
  1015. *
  1016. * http://sourceforge.net/tracker/index.php?func=detail&aid=1503410&group_id=92314&atid=600309
  1017. *
  1018. * Requirements:
  1019. * - Be able to find the associated PDF in a set of given directories.
  1020. * - Be able to return a relative path or absolute path.
  1021. * - Be fast.
  1022. * - Allow for flexible naming schemes in the PDFs.
  1023. *
  1024. * Syntax scheme for file:
  1025. * <ul>
  1026. * <li>* Any subDir</li>
  1027. * <li>** Any subDir (recursiv)</li>
  1028. * <li>[key] Key from bibtex file and database</li>
  1029. * <li>.* Anything else is taken to be a Regular expression.</li>
  1030. * </ul>
  1031. *
  1032. * @param entry
  1033. * non-null
  1034. * @param database
  1035. * non-null
  1036. * @param directory
  1037. * A set of root directories to start the search from. Paths are
  1038. * returned relative to these directories if relative is set to
  1039. * true. These directories will not be expanded or anything. Use
  1040. * the file attribute for this.
  1041. * @param file
  1042. * non-null
  1043. *
  1044. * @param relative
  1045. * whether to return relative file paths or absolute ones
  1046. *
  1047. * @return Will return the first file found to match the given criteria or
  1048. * null if none was found.
  1049. */
  1050. public static String findFile(BibtexEntry entry, BibtexDatabase database, String[] directory,
  1051. String file, boolean relative) {
  1052. for (String aDirectory : directory) {
  1053. String result = findFile(entry, database, aDirectory, file, relative);
  1054. if (result != null) {
  1055. return result;
  1056. }
  1057. }
  1058. return null;
  1059. }
  1060. /**
  1061. * Removes optional square brackets from the string s
  1062. *
  1063. * @param s
  1064. * @return
  1065. */
  1066. public static String stripBrackets(String s) {
  1067. int beginIndex = (s.startsWith("[") ? 1 : 0);
  1068. int endIndex = (s.endsWith("]") ? s.length() - 1 : s.length());
  1069. return s.substring(beginIndex, endIndex);
  1070. }
  1071. public static ArrayList<String[]> parseMethodsCalls(String calls) throws RuntimeException {
  1072. ArrayList<String[]> result = new ArrayList<String[]>();
  1073. char[] c = calls.toCharArray();
  1074. int i = 0;
  1075. while (i < c.length) {
  1076. int start = i;
  1077. if (Character.isJavaIdentifierStart(c[i])) {
  1078. i++;
  1079. while (i < c.length && (Character.isJavaIdentifierPart(c[i]) || c[i] == '.')) {
  1080. i++;
  1081. }
  1082. if (i < c.length && c[i] == '(') {
  1083. String method = calls.substring(start, i);
  1084. // Skip the brace
  1085. i++;
  1086. if (i < c.length){
  1087. if (c[i] == '"'){
  1088. // Parameter is in format "xxx"
  1089. // Skip "
  1090. i++;
  1091. int startParam = i;
  1092. i++;
  1093. boolean escaped = false;
  1094. while (i + 1 < c.length &&
  1095. !(!escaped && c[i] == '"' && c[i + 1] == ')')) {
  1096. if (c[i] == '\\') {
  1097. escaped = !escaped;
  1098. }
  1099. else
  1100. escaped = false;
  1101. i++;
  1102. }
  1103. String param = calls.substring(startParam, i);
  1104. result.add(new String[] { method, param });
  1105. } else {
  1106. // Parameter is in format xxx
  1107. int startParam = i;
  1108. while (i < c.length && c[i] != ')') {
  1109. i++;
  1110. }
  1111. String param = calls.substring(startParam, i);
  1112. result.add(new String[] { method, param });
  1113. }
  1114. } else {
  1115. // Incorrecly terminated open brace
  1116. result.add(new String[] { method });
  1117. }
  1118. } else {
  1119. String method = calls.substring(start, i);
  1120. result.add(new String[] { method });
  1121. }
  1122. }
  1123. i++;
  1124. }
  1125. return result;
  1126. }
  1127. /**
  1128. * Accepts a string like [author:lower] or [title:abbr] or [auth],
  1129. * whereas the first part signifies the bibtex-field to get, or the key generator
  1130. * field marker to use, while the others are the modifiers that will be applied.
  1131. *
  1132. * @param fieldAndFormat
  1133. * @param entry
  1134. * @param database
  1135. * @return
  1136. */
  1137. public static String getFieldAndFormat(String fieldAndFormat, BibtexEntry entry,
  1138. BibtexDatabase database) {
  1139. fieldAndFormat = stripBrackets(fieldAndFormat);
  1140. int colon = fieldAndFormat.indexOf(':');
  1141. String beforeColon, afterColon;
  1142. if (colon == -1) {
  1143. beforeColon = fieldAndFormat;
  1144. afterColon = null;
  1145. } else {
  1146. beforeColon = fieldAndFormat.substring(0, colon);
  1147. afterColon = fieldAndFormat.substring(colon + 1);
  1148. }
  1149. beforeColon = beforeColon.trim();
  1150. if (beforeColon.length() == 0) {
  1151. return null;
  1152. }
  1153. String fieldValue = BibtexDatabase.getResolvedField(beforeColon, entry, database);
  1154. // If no field value was found, try to interpret it as a key generator field marker:
  1155. if (fieldValue == null)
  1156. fieldValue = LabelPatternUtil.makeLabel(entry, beforeColon);
  1157. if (fieldValue == null)
  1158. return null;
  1159. if (afterColon == null || afterColon.length() == 0)
  1160. return fieldValue;
  1161. String[] parts = afterColon.split(":");
  1162. fieldValue = LabelPatternUtil.applyModifiers(fieldValue, parts, 0);
  1163. return fieldValue;
  1164. }
  1165. /**
  1166. * Convenience function for absolute search.
  1167. *
  1168. * Uses findFile(BibtexEntry, BibtexDatabase, (String)null, String, false).
  1169. */
  1170. public static String findFile(BibtexEntry entry, BibtexDatabase database, String file) {
  1171. return findFile(entry, database, (String) null, file, false);
  1172. }
  1173. /**
  1174. * Internal Version of findFile, which also accepts a current directory to
  1175. * base the search on.
  1176. *
  1177. */
  1178. public static String findFile(BibtexEntry entry, BibtexDatabase database, String directory,
  1179. String file, boolean relative) {
  1180. File root;
  1181. if (directory == null) {
  1182. root = new File(".");
  1183. } else {
  1184. root = new File(directory);
  1185. }
  1186. if (!root.exists())
  1187. return null;
  1188. String found = findFile(entry, database, root, file);
  1189. if (directory == null || !relative) {
  1190. return found;
  1191. }
  1192. if (found != null) {
  1193. try {
  1194. /**
  1195. * [ 1601651 ] PDF subdirectory - missing first character
  1196. *
  1197. * http://sourceforge.net/tracker/index.php?func=detail&aid=1601651&group_id=92314&atid=600306
  1198. */
  1199. // Changed by M. Alver 2007.01.04:
  1200. // Remove first character if it is a directory separator character:
  1201. String tmp = found.substring(root.getCanonicalPath().length());
  1202. if ((tmp.length() > 1) && (tmp.charAt(0) == File.separatorChar))
  1203. tmp = tmp.substring(1);
  1204. return tmp;
  1205. //return found.substring(root.getCanonicalPath().length());
  1206. } catch (IOException e) {
  1207. return null;
  1208. }
  1209. }
  1210. return null;
  1211. }
  1212. /**
  1213. * The actual work-horse. Will find absolute filepaths starting from the
  1214. * given directory using the given regular expression string for search.
  1215. */
  1216. protected static String findFile(BibtexEntry entry, BibtexDatabase database, File directory,
  1217. String file) {
  1218. if (file.startsWith("/")) {
  1219. directory = new File(".");
  1220. file = file.substring(1);
  1221. }
  1222. // Escape handling...
  1223. Matcher m = Pattern.compile("([^\\\\])\\\\([^\\\\])").matcher(file);
  1224. StringBuffer s = new StringBuffer();
  1225. while (m.find()) {
  1226. m.appendReplacement(s, m.group(1) + "/" + m.group(2));
  1227. }
  1228. m.appendTail(s);
  1229. file = s.toString();
  1230. String[] fileParts = file.split("/");
  1231. if (fileParts.length == 0)
  1232. return null;
  1233. if (fileParts.length > 1) {
  1234. for (int i = 0; i < fileParts.length - 1; i++) {
  1235. String dirToProcess = fileParts[i];
  1236. dirToProcess = expandBrackets(dirToProcess, entry, database);
  1237. if (dirToProcess.matches("^.:$")) { // Windows Drive Letter
  1238. directory = new File(dirToProcess + "/");
  1239. continue;
  1240. }
  1241. if (dirToProcess.equals(".")) { // Stay in current directory
  1242. continue;
  1243. }
  1244. if (dirToProcess.equals("..")) {
  1245. directory = new File(directory.getParent());
  1246. continue;
  1247. }
  1248. if (dirToProcess.equals("*")) { // Do for all direct subdirs
  1249. File[] subDirs = directory.listFiles();
  1250. if (subDirs == null)
  1251. return null; // No permission?
  1252. String restOfFileString = join(fileParts, "/", i + 1, fileParts.length);
  1253. for (File subDir : subDirs) {
  1254. if (subDir.isDirectory()) {
  1255. String result = findFile(entry, database, subDir,
  1256. restOfFileString);
  1257. if (result != null)
  1258. return result;
  1259. }
  1260. }
  1261. return null;
  1262. }
  1263. // Do for all direct and indirect subdirs
  1264. if (dirToProcess.equals("**")) {
  1265. List<File> toDo = new LinkedList<File>();
  1266. toDo.add(directory);
  1267. String restOfFileString = join(fileParts, "/", i + 1, fileParts.length);
  1268. // Before checking the subdirs, we first check the current
  1269. // dir
  1270. String result = findFile(entry, database, directory, restOfFileString);
  1271. if (result != null)
  1272. return result;
  1273. while (!toDo.isEmpty()) {
  1274. // Get all subdirs of each of the elements found in toDo
  1275. File[] subDirs = toDo.remove(0).listFiles();
  1276. if (subDirs == null) // No permission?
  1277. continue;
  1278. toDo.addAll(Arrays.asList(subDirs));
  1279. for (File subDir : subDirs) {
  1280. if (!subDir.isDirectory())
  1281. continue;
  1282. result = findFile(entry, database, subDir, restOfFileString);
  1283. if (result != null)
  1284. return result;
  1285. }
  1286. }
  1287. // We already did the currentDirectory
  1288. return null;
  1289. }
  1290. final Pattern toMatch = Pattern
  1291. .compile(dirToProcess.replaceAll("\\\\\\\\", "\\\\"));
  1292. File[] matches = directory.listFiles(new FilenameFilter() {
  1293. public boolean accept(File arg0, String arg1) {
  1294. return toMatch.matcher(arg1).matches();
  1295. }
  1296. });
  1297. if (matches == null || matches.length == 0)
  1298. return null;
  1299. directory = matches[0];
  1300. if (!directory.exists())
  1301. return null;
  1302. } // End process directory information
  1303. }
  1304. // Last step check if the given file can be found in this directory
  1305. String filenameToLookFor = expandBrackets(fileParts[fileParts.length - 1], entry, database);
  1306. final Pattern toMatch = Pattern.compile("^"
  1307. + filenameToLookFor.replaceAll("\\\\\\\\", "\\\\") + "$");
  1308. File[] matches = directory.listFiles(new FilenameFilter() {
  1309. public boolean accept(File arg0, String arg1) {
  1310. return toMatch.matcher(arg1).matches();
  1311. }
  1312. });
  1313. if (matches == null || matches.length == 0)
  1314. return null;
  1315. try {
  1316. return matches[0].getCanonicalPath();
  1317. } catch (IOException e) {
  1318. return null;
  1319. }
  1320. }
  1321. static Pattern squareBracketsPattern = Pattern.compile("\\[.*?\\]");
  1322. /**
  1323. * Takes a string that contains bracketed expression and expands each of
  1324. * these using getFieldAndFormat.
  1325. *
  1326. * Unknown Bracket expressions are silently dropped.
  1327. *
  1328. * @param bracketString
  1329. * @param entry
  1330. * @param database
  1331. * @return
  1332. */
  1333. public static String expandBrackets(String bracketString, BibtexEntry entry,
  1334. BibtexDatabase database) {
  1335. Matcher m = squareBracketsPattern.matcher(bracketString);
  1336. StringBuffer s = new StringBuffer();
  1337. while (m.find()) {
  1338. String replacement = getFieldAndFormat(m.group(), entry, database);
  1339. if (replacement == null)
  1340. replacement = "";
  1341. m.appendReplacement(s, replacement);
  1342. }
  1343. m.appendTail(s);
  1344. return s.toString();
  1345. }
  1346. /**
  1347. * Concatenate all strings in the array from index 'from' to 'to' (excluding
  1348. * to) with the given separator.
  1349. *
  1350. * Example:
  1351. *
  1352. * String[] s = "ab/cd/ed".split("/"); join(s, "\\", 0, s.length) ->
  1353. * "ab\\cd\\ed"
  1354. *
  1355. * @param strings
  1356. * @param separator
  1357. * @param from
  1358. * @param to
  1359. * Excluding strings[to]
  1360. * @return
  1361. */
  1362. public static String join(String[] strings, String separator, int from, int to) {
  1363. if (strings.length == 0 || from >= to)
  1364. return "";
  1365. from = Math.max(from, 0);
  1366. to = Math.min(strings.length, to);
  1367. StringBuffer sb = new StringBuffer();
  1368. for (int i = from; i < to - 1; i++) {
  1369. sb.append(strings[i]).append(separator);
  1370. }
  1371. return sb.append(strings[to - 1]).toString();
  1372. }
  1373. public static String join(String[] strings, String separator) {
  1374. return join(strings, separator, 0, strings.length);
  1375. }
  1376. /**
  1377. * Converts a relative filename to an absolute one, if necessary. Returns
  1378. * null if the file does not exist.<br/>
  1379. *
  1380. * Uses <ul>
  1381. * <li>the default directory associated with the extension of the file</li>
  1382. * <li>the standard file directory</li>
  1383. * <li>the directory of the bib file</li>
  1384. * </ul>
  1385. *
  1386. * @param metaData
  1387. * The MetaData for the database this file belongs to.
  1388. * @param name
  1389. * The file name, may also be a relative path to the file
  1390. */
  1391. public static File expandFilename(final MetaData metaData, String name) {
  1392. int pos = name.lastIndexOf('.');
  1393. String extension = ((pos >= 0) && (pos < name.length() - 1)) ? name
  1394. .substring(pos + 1).trim().toLowerCase() : null;
  1395. // Find the default directory for this field type, if any:
  1396. String[] dir = metaData.getFileDirectory(extension);
  1397. // Include the standard "file" directory:
  1398. String[] fileDir = metaData.getFileDirectory(GUIGlobals.FILE_FIELD);
  1399. // Include the directory of the bib file:
  1400. ArrayList<String> al = new ArrayList<String>();
  1401. for (String aDir : dir)
  1402. if (!al.contains(aDir))
  1403. al.add(aDir);
  1404. for (String aFileDir : fileDir)
  1405. if (!al.contains(aFileDir))
  1406. al.add(aFileDir);
  1407. String[] dirs = al.toArray(new String[al.size()]);
  1408. return expandFilename(name, dirs);
  1409. }
  1410. /**
  1411. * Converts a relative filename to an absolute one, if necessary. Returns
  1412. * null if the file does not exist.
  1413. *
  1414. * Will look in each of the given dirs starting from the beginning and
  1415. * returning the first found file to match if any.
  1416. */
  1417. public static File expandFilename(String name, String[] dir) {
  1418. for (String aDir : dir) {
  1419. if (aDir != null) {
  1420. File result = expandFilename(name, aDir);
  1421. if (result != null) {
  1422. return result;
  1423. }
  1424. }
  1425. }
  1426. return null;
  1427. }
  1428. /**
  1429. * Converts a relative filename to an absolute one, if necessary. Returns
  1430. * null if the file does not exist.
  1431. */
  1432. public static File expandFilename(String name, String dir) {
  1433. File file = null;
  1434. if (name == null || name.length() == 0)
  1435. return null;
  1436. else {
  1437. file = new File(name);
  1438. }
  1439. if (!file.exists() && (dir != null)) {
  1440. if (dir.endsWith(System.getProperty("file.separator")))
  1441. name = dir + name;
  1442. else
  1443. name = dir + System.getProperty("file.separator") + name;
  1444. // System.out.println("expanded to: "+name);
  1445. // if (name.startsWith("ftp"))
  1446. file = new File(name);
  1447. if (file.exists())
  1448. return file;
  1449. // Ok, try to fix / and \ problems:
  1450. if (Globals.ON_WIN) {
  1451. // workaround for catching Java bug in regexp replacer
  1452. // and, why, why, why ... I don't get it - wegner 2006/01/22
  1453. try {
  1454. name = name.replaceAll("/", "\\\\");
  1455. } catch (java.lang.StringIndexOutOfBoundsException exc) {
  1456. System.err
  1457. .println("An internal Java error was caused by the entry " +
  1458. "\"" + name + "\"");
  1459. }
  1460. } else
  1461. name = name.replaceAll("\\\\", "/");
  1462. // System.out.println("expandFilename: "+name);
  1463. file = new File(name);
  1464. if (!file.exists())
  1465. file = null;
  1466. }
  1467. return file;
  1468. }
  1469. /**
  1470. * Converts an absolute filename to a relative one, if necessary.
  1471. * Returns the parameter fileName itself if no shortening is possible
  1472. *
  1473. * This method works correctly only if dirs are sorted decent in their length
  1474. * i.e. /home/user/literature/important before /home/user/literature
  1475. *
  1476. * @param fileName the file name to be shortened
  1477. * @param dirs directories to check.
  1478. */
  1479. public static File shortenFileName(File fileName, String[] dirs) {
  1480. if (fileName == null || fileName.length() == 0)
  1481. return fileName;
  1482. if (!fileName.isAbsolute() || (dirs == null))
  1483. return fileName;
  1484. for (String dir: dirs) {
  1485. if (dir!=null) {
  1486. File result = shortenFileName(fileName, dir);
  1487. if ((result != null) && (!result.equals(fileName)))
  1488. return result;
  1489. }
  1490. }
  1491. return fileName;
  1492. }
  1493. public static File shortenFileName(File fileName, String dir) {
  1494. if (fileName == null || fileName.length() == 0)
  1495. return fileName;
  1496. if (!fileName.isAbsolute() || dir == null)
  1497. return fileName;
  1498. String longName;
  1499. if (Globals.ON_WIN) {
  1500. // case-insensitive matching on Windows
  1501. longName = fileName.toString().toLowerCase();
  1502. dir = dir.toLowerCase();
  1503. } else {
  1504. longName = fileName.toString();
  1505. }
  1506. if (!dir.endsWith(System.getProperty("file.separator")))
  1507. dir = dir.concat(System.getProperty("file.separator"));
  1508. if (longName.startsWith(dir)) {
  1509. // result is based on original name, not on lower-cased name
  1510. String newName = fileName.toString().substring(dir.length());
  1511. return new File(newName);
  1512. } else {
  1513. return fileName;
  1514. }
  1515. }
  1516. private static String findInDir(String key, String dir, OpenFileFilter off, int count) {
  1517. if (count > 20)
  1518. return null; // Make sure an infinite loop doesn't occur.
  1519. File f = new File(dir);
  1520. File[] all = f.listFiles();
  1521. if (all == null)
  1522. return null; // An error occured. We may not have
  1523. // permission to list the files.
  1524. int numFiles = all.length;
  1525. for (File curFile : all) {
  1526. if (curFile.isFile()) {
  1527. String name = curFile.getName();
  1528. if (name.startsWith(key + ".") && off.accept(name))
  1529. return curFile.getPath();
  1530. } else if (curFile.isDirectory()) {
  1531. String found = findInDir(key, curFile.getPath(), off, count + 1);
  1532. if (found != null)
  1533. return found;
  1534. }
  1535. }
  1536. return null;
  1537. }
  1538. /**
  1539. * This methods assures all words in the given entry are recorded in their
  1540. * respective Completers, if any.
  1541. */
  1542. public static void updateCompletersForEntry(HashMap<String, AbstractAutoCompleter> autoCompleters, BibtexEntry bibtexEntry) {
  1543. for (Map.Entry<String, AbstractAutoCompleter> entry : autoCompleters.entrySet()){
  1544. AbstractAutoCompleter comp = entry.getValue();
  1545. comp.addBibtexEntry(bibtexEntry);
  1546. }
  1547. }
  1548. /**
  1549. * Sets empty or non-existing owner fields of bibtex entries inside a List
  1550. * to a specified default value. Timestamp field is also set. Preferences
  1551. * are checked to see if these options are enabled.
  1552. *
  1553. * @param bibs
  1554. * List of bibtex entries
  1555. */
  1556. public static void setAutomaticFields(Collection<BibtexEntry> bibs,
  1557. boolean overwriteOwner, boolean overwriteTimestamp, boolean markEntries) {
  1558. String timeStampField = Globals.prefs.get("timeStampField");
  1559. String defaultOwner = Globals.prefs.get("defaultOwner");
  1560. String timestamp = easyDateFormat();
  1561. boolean globalSetOwner = Globals.prefs.getBoolean("useOwner"),
  1562. globalSetTimeStamp = Globals.prefs.getBoolean("useTimeStamp");
  1563. // Do not need to do anything if all options are disabled
  1564. if (!(globalSetOwner || globalSetTimeStamp || markEntries))
  1565. return;
  1566. // Iterate through all entries
  1567. for (BibtexEntry curEntry : bibs){
  1568. boolean setOwner = globalSetOwner &&
  1569. (overwriteOwner || (curEntry.getField(BibtexFields.OWNER)==null));
  1570. boolean setTimeStamp = globalSetTimeStamp &&
  1571. (overwriteTimestamp || (curEntry.getField(timeStampField)==null));
  1572. setAutomaticFields(curEntry, setOwner, defaultOwner, setTimeStamp, timeStampField,
  1573. timestamp);
  1574. if (markEntries)
  1575. Util.markEntry(curEntry, IMPORT_MARK_LEVEL, false, new NamedCompound(""));
  1576. }
  1577. }
  1578. /**
  1579. * Sets empty or non-existing owner fields of a bibtex entry to a specified
  1580. * default value. Timestamp field is also set. Preferences are checked to
  1581. * see if these options are enabled.
  1582. *
  1583. * @param entry
  1584. * The entry to set fields for.
  1585. * @param overwriteOwner
  1586. * Indicates whether owner should be set if it is already set.
  1587. * @param overwriteTimestamp
  1588. * Indicates whether timestamp should be set if it is already set.
  1589. */
  1590. public static void setAutomaticFields(BibtexEntry entry, boolean overwriteOwner,
  1591. boolean overwriteTimestamp) {
  1592. String defaultOwner = Globals.prefs.get("defaultOwner");
  1593. String timestamp = easyDateFormat();
  1594. String timeStampField = Globals.prefs.get("timeStampField");
  1595. boolean setOwner = Globals.prefs.getBoolean("useOwner") &&
  1596. (overwriteOwner || (entry.getField(BibtexFields.OWNER)==null));
  1597. boolean setTimeStamp = Globals.prefs.getBoolean("useTimeStamp") &&
  1598. (overwriteTimestamp || (entry.getField(timeStampField)==null));
  1599. setAutomaticFields(entry, setOwner, defaultOwner, setTimeStamp, timeStampField, timestamp);
  1600. }
  1601. private static void setAutomaticFields(BibtexEntry entry, boolean setOwner, String owner,
  1602. boolean setTimeStamp, String timeStampField, String timeStamp) {
  1603. // Set owner field if this option is enabled:
  1604. if (setOwner) {
  1605. // No or empty owner field?
  1606. // if (entry.getField(Globals.OWNER) == null
  1607. // || ((String) entry.getField(Globals.OWNER)).length() == 0) {
  1608. // Set owner field to default value
  1609. entry.setField(BibtexFields.OWNER, owner);
  1610. // }
  1611. }
  1612. if (setTimeStamp)
  1613. entry.setField(timeStampField, timeStamp);
  1614. }
  1615. /**
  1616. * Copies a file.
  1617. *
  1618. * @param source
  1619. * File Source file
  1620. * @param dest
  1621. * File Destination file
  1622. * @param deleteIfExists
  1623. * boolean Determines whether the copy goes on even if the file
  1624. * exists.
  1625. * @throws IOException
  1626. * @return boolean Whether the copy succeeded, or was stopped due to the
  1627. * file already existing.
  1628. */
  1629. public static boolean copyFile(File source, File dest, boolean deleteIfExists)
  1630. throws IOException {
  1631. BufferedInputStream in = null;
  1632. BufferedOutputStream out = null;
  1633. try {
  1634. // Check if the file already exists.
  1635. if (dest.exists()) {
  1636. if (!deleteIfExists)
  1637. return false;
  1638. // else dest.delete();
  1639. }
  1640. in = new BufferedInputStream(new FileInputStream(source));
  1641. out = new BufferedOutputStream(new FileOutputStream(dest));
  1642. int el;
  1643. // int tell = 0;
  1644. while ((el = in.read()) >= 0) {
  1645. out.write(el);
  1646. }
  1647. } finally {
  1648. if (out != null) {
  1649. out.flush();
  1650. out.close();
  1651. }
  1652. if (in != null) {
  1653. in.close();
  1654. }
  1655. }
  1656. return true;
  1657. }
  1658. /**
  1659. * This method is called at startup, and makes necessary adaptations to
  1660. * preferences for users from an earlier version of Jabref.
  1661. */
  1662. public static void performCompatibilityUpdate() {
  1663. // Make sure "abstract" is not in General fields, because
  1664. // Jabref 1.55 moves the abstract to its own tab.
  1665. String genFields = Globals.prefs.get("generalFields");
  1666. // pr(genFields+"\t"+genFields.indexOf("abstract"));
  1667. if (genFields.contains("abstract")) {
  1668. // pr(genFields+"\t"+genFields.indexOf("abstract"));
  1669. String newGen;
  1670. if (genFields.equals("abstract"))
  1671. newGen = "";
  1672. else if (genFields.contains(";abstract;")) {
  1673. newGen = genFields.replaceAll(";abstract;", ";");
  1674. } else if (genFields.indexOf("abstract;") == 0) {
  1675. newGen = genFields.replaceAll("abstract;", "");
  1676. } else if (genFields.indexOf(";abstract") == genFields.length() - 9) {
  1677. newGen = genFields.replaceAll(";abstract", "");
  1678. } else
  1679. newGen = genFields;
  1680. // pr(newGen);
  1681. Globals.prefs.put("generalFields", newGen);
  1682. }
  1683. }
  1684. /**
  1685. * Collect file links from the given set of fields, and add them to the list contained
  1686. * in the field GUIGlobals.FILE_FIELD.
  1687. * @param database The database to modify.
  1688. * @param fields The fields to find links in.
  1689. * @return A CompoundEdit specifying the undo operation for the whole operation.
  1690. */
  1691. public static NamedCompound upgradePdfPsToFile(BibtexDatabase database, String[] fields) {
  1692. return upgradePdfPsToFile(database.getEntryMap().values(), fields);
  1693. }
  1694. /**
  1695. * Collect file links from the given set of fields, and add them to the list contained
  1696. * in the field GUIGlobals.FILE_FIELD.
  1697. * @param entries The entries to modify.
  1698. * @param fields The fields to find links in.
  1699. * @return A CompoundEdit specifying the undo operation for the whole operation.
  1700. */
  1701. public static NamedCompound upgradePdfPsToFile(Collection<BibtexEntry> entries, String[] fields) {
  1702. NamedCompound ce = new NamedCompound(Globals.lang("Move external links to 'file' field"));
  1703. for (BibtexEntry entry : entries){
  1704. FileListTableModel tableModel = new FileListTableModel();
  1705. // If there are already links in the file field, keep those on top:
  1706. String oldFileContent = entry.getField(GUIGlobals.FILE_FIELD);
  1707. if (oldFileContent != null) {
  1708. tableModel.setContent(oldFileContent);
  1709. }
  1710. int oldRowCount = tableModel.getRowCount();
  1711. for (String field : fields) {
  1712. String o = entry.getField(field);
  1713. if (o != null) {
  1714. String s = o;
  1715. if (s.trim().length() > 0) {
  1716. File f = new File(s);
  1717. FileListEntry flEntry = new FileListEntry(f.getName(), s,
  1718. Globals.prefs.getExternalFileTypeByExt(field));
  1719. tableModel.addEntry(tableModel.getRowCount(), flEntry);
  1720. entry.clearField(field);
  1721. ce.addEdit(new UndoableFieldChange(entry, field, o, null));
  1722. }
  1723. }
  1724. }
  1725. if (tableModel.getRowCount() != oldRowCount) {
  1726. String newValue = tableModel.getStringRepresentation();
  1727. entry.setField(GUIGlobals.FILE_FIELD, newValue);
  1728. ce.addEdit(new UndoableFieldChange(entry, GUIGlobals.FILE_FIELD, oldFileContent, newValue));
  1729. }
  1730. }
  1731. ce.end();
  1732. return ce;
  1733. }
  1734. // -------------------------------------------------------------------------------
  1735. /**
  1736. * extends the filename with a default Extension, if no Extension '.x' could
  1737. * be found
  1738. */
  1739. public static String getCorrectFileName(String orgName, String defaultExtension) {
  1740. if (orgName == null)
  1741. return "";
  1742. String back = orgName;
  1743. int t = orgName.indexOf(".", 1); // hidden files Linux/Unix (?)
  1744. if (t < 1)
  1745. back = back + "." + defaultExtension;
  1746. return back;
  1747. }
  1748. /**
  1749. * Quotes each and every character, e.g. '!' as &#33;. Used for verbatim
  1750. * display of arbitrary strings that may contain HTML entities.
  1751. */
  1752. public static String quoteForHTML(String s) {
  1753. StringBuffer sb = new StringBuffer();
  1754. for (int i = 0; i < s.length(); ++i) {
  1755. sb.append("&#").append((int) s.charAt(i)).append(";");
  1756. }
  1757. return sb.toString();
  1758. }
  1759. public static String quote(String s, String specials, char quoteChar) {
  1760. return quote(s, specials, quoteChar, 0);
  1761. }
  1762. /**
  1763. * Quote special characters.
  1764. *
  1765. * @param s
  1766. * The String which may contain special characters.
  1767. * @param specials
  1768. * A String containing all special characters except the quoting
  1769. * character itself, which is automatically quoted.
  1770. * @param quoteChar
  1771. * The quoting character.
  1772. * @param linewrap
  1773. * The number of characters after which a linebreak is inserted
  1774. * (this linebreak is undone by unquote()). Set to 0 to disable.
  1775. * @return A String with every special character (including the quoting
  1776. * character itself) quoted.
  1777. */
  1778. public static String quote(String s, String specials, char quoteChar, int linewrap) {
  1779. StringBuffer sb = new StringBuffer();
  1780. char c;
  1781. int linelength = 0;
  1782. boolean isSpecial;
  1783. for (int i = 0; i < s.length(); ++i) {
  1784. c = s.charAt(i);
  1785. isSpecial = specials.indexOf(c) >= 0 || c == quoteChar;
  1786. // linebreak?
  1787. if (linewrap > 0
  1788. && (++linelength >= linewrap || (isSpecial && linelength >= linewrap - 1))) {
  1789. sb.append(quoteChar);
  1790. sb.append('\n');
  1791. linelength = 0;
  1792. }
  1793. if (isSpecial) {
  1794. sb.append(quoteChar);
  1795. ++linelength;
  1796. }
  1797. sb.append(c);
  1798. }
  1799. return sb.toString();
  1800. }
  1801. /**
  1802. * Unquote special characters.
  1803. *
  1804. * @param s
  1805. * The String which may contain quoted special characters.
  1806. * @param quoteChar
  1807. * The quoting character.
  1808. * @return A String with all quoted characters unquoted.
  1809. */
  1810. public static String unquote(String s, char quoteChar) {
  1811. StringBuffer sb = new StringBuffer();
  1812. char c;
  1813. boolean quoted = false;
  1814. for (int i = 0; i < s.length(); ++i) {
  1815. c = s.charAt(i);
  1816. if (quoted) { // append literally...
  1817. if (c != '\n') // ...unless newline
  1818. sb.append(c);
  1819. quoted = false;
  1820. } else if (c != quoteChar) {
  1821. sb.append(c);
  1822. } else { // quote char
  1823. quoted = true;
  1824. }
  1825. }
  1826. return sb.toString();
  1827. }
  1828. /**
  1829. * Quote all regular expression meta characters in s, in order to search for
  1830. * s literally.
  1831. */
  1832. public static String quoteMeta(String s) {
  1833. // work around a bug: trailing backslashes have to be quoted
  1834. // individually
  1835. int i = s.length() - 1;
  1836. StringBuffer bs = new StringBuffer("");
  1837. while ((i >= 0) && (s.charAt(i) == '\\')) {
  1838. --i;
  1839. bs.append("\\\\");
  1840. }
  1841. s = s.substring(0, i + 1);
  1842. return "\\Q" + s.replaceAll("\\\\E", "\\\\E\\\\\\\\E\\\\Q") + "\\E" + bs.toString();
  1843. }
  1844. /**
  1845. * This method "tidies" up e.g. a keyword string, by alphabetizing the words
  1846. * and removing all duplicates.
  1847. *
  1848. * Currently not used anywhere
  1849. */
  1850. public static String sortWordsAndRemoveDuplicates(String text) {
  1851. ArrayList<String> words = getSeparatedKeywords(text);
  1852. // by adding the words to a set, they are automatically sorted
  1853. TreeSet<String> set = new TreeSet<String>(words);
  1854. StringBuffer sb = new StringBuffer();
  1855. for (String aSet : set) {
  1856. sb.append(aSet);
  1857. sb.append(", ");
  1858. }
  1859. if (sb.length() > 2)
  1860. sb.delete(sb.length() - 2, sb.length());
  1861. String result = sb.toString();
  1862. return result.length() > 2 ? result : "";
  1863. }
  1864. /**
  1865. * Warns the user of undesired side effects of an explicit
  1866. * assignment/removal of entries to/from this group. Currently there are
  1867. * four types of groups: AllEntriesGroup, SearchGroup - do not support
  1868. * explicit assignment. ExplicitGroup - never modifies entries. KeywordGroup -
  1869. * only this modifies entries upon assignment/removal. Modifications are
  1870. * acceptable unless they affect a standard field (such as "author") besides
  1871. * the "keywords" field.
  1872. *
  1873. * @param parent
  1874. * The Component used as a parent when displaying a confirmation
  1875. * dialog.
  1876. * @return true if the assignment has no undesired side effects, or the user
  1877. * chose to perform it anyway. false otherwise (this indicates that
  1878. * the user has aborted the assignment).
  1879. */
  1880. public static boolean warnAssignmentSideEffects(AbstractGroup[] groups, BibtexEntry[] entries,
  1881. BibtexDatabase db, Component parent) {
  1882. Vector<String> affectedFields = new Vector<String>();
  1883. for (AbstractGroup group : groups) {
  1884. if (group instanceof KeywordGroup) {
  1885. KeywordGroup kg = (KeywordGroup) group;
  1886. String field = kg.getSearchField().toLowerCase();
  1887. if (field.equals("keywords"))
  1888. continue; // this is not undesired
  1889. for (int i = 0, len = BibtexFields.numberOfPublicFields(); i < len; ++i) {
  1890. if (field.equals(BibtexFields.getFieldName(i))) {
  1891. affectedFields.add(field);
  1892. break;
  1893. }
  1894. }
  1895. }
  1896. }
  1897. if (affectedFields.size() == 0)
  1898. return true; // no side effects
  1899. // show a warning, then return
  1900. StringBuffer message = // JZTODO lyrics...
  1901. new StringBuffer("This action will modify the following field(s)\n"
  1902. + "in at least one entry each:\n");
  1903. for (int i = 0; i < affectedFields.size(); ++i)
  1904. message.append(affectedFields.elementAt(i)).append("\n");
  1905. message.append("This could cause undesired changes to "
  1906. + "your entries, so it is\nrecommended that you change the grouping field "
  1907. + "in your group\ndefinition to \"keywords\" or a non-standard name."
  1908. + "\n\nDo you still want to continue?");
  1909. int choice = JOptionPane.showConfirmDialog(parent, message, Globals.lang("Warning"),
  1910. JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
  1911. return choice != JOptionPane.NO_OPTION;
  1912. // if (groups instanceof KeywordGroup) {
  1913. // KeywordGroup kg = (KeywordGroup) groups;
  1914. // String field = kg.getSearchField().toLowerCase();
  1915. // if (field.equals("keywords"))
  1916. // return true; // this is not undesired
  1917. // for (int i = 0; i < GUIGlobals.ALL_FIELDS.length; ++i) {
  1918. // if (field.equals(GUIGlobals.ALL_FIELDS[i])) {
  1919. // // show a warning, then return
  1920. // String message = Globals // JZTODO lyrics...
  1921. // .lang(
  1922. // "This action will modify the \"%0\" field "
  1923. // + "of your entries.\nThis could cause undesired changes to "
  1924. // + "your entries, so it is\nrecommended that you change the grouping
  1925. // field "
  1926. // + "in your group\ndefinition to \"keywords\" or a non-standard name."
  1927. // + "\n\nDo you still want to continue?",
  1928. // field);
  1929. // int choice = JOptionPane.showConfirmDialog(parent, message,
  1930. // Globals.lang("Warning"), JOptionPane.YES_NO_OPTION,
  1931. // JOptionPane.WARNING_MESSAGE);
  1932. // return choice != JOptionPane.NO_OPTION;
  1933. // }
  1934. // }
  1935. // }
  1936. // return true; // found no side effects
  1937. }
  1938. // ========================================================
  1939. // lot of abreviations in medline
  1940. // PKC etc convert to {PKC} ...
  1941. // ========================================================
  1942. static Pattern titleCapitalPattern = Pattern.compile("[A-Z]+");
  1943. /**
  1944. * Wrap all uppercase letters, or sequences of uppercase letters, in curly
  1945. * braces. Ignore letters within a pair of # character, as these are part of
  1946. * a string label that should not be modified.
  1947. *
  1948. * @param s
  1949. * The string to modify.
  1950. * @return The resulting string after wrapping capitals.
  1951. */
  1952. public static String putBracesAroundCapitals(String s) {
  1953. boolean inString = false, isBracing = false, escaped = false;
  1954. int inBrace = 0;
  1955. StringBuffer buf = new StringBuffer();
  1956. for (int i = 0; i < s.length(); i++) {
  1957. // Update variables based on special characters:
  1958. int c = s.charAt(i);
  1959. if (c == '{')
  1960. inBrace++;
  1961. else if (c == '}')
  1962. inBrace--;
  1963. else if (!escaped && (c == '#'))
  1964. inString = !inString;
  1965. // See if we should start bracing:
  1966. if ((inBrace == 0) && !isBracing && !inString && Character.isLetter((char) c)
  1967. && Character.isUpperCase((char) c)) {
  1968. buf.append('{');
  1969. isBracing = true;
  1970. }
  1971. // See if we should close a brace set:
  1972. if (isBracing && !(Character.isLetter((char) c) && Character.isUpperCase((char) c))) {
  1973. buf.append('}');
  1974. isBracing = false;
  1975. }
  1976. // Add the current character:
  1977. buf.append((char) c);
  1978. // Check if we are entering an escape sequence:
  1979. escaped = (c == '\\') && !escaped;
  1980. }
  1981. // Check if we have an unclosed brace:
  1982. if (isBracing)
  1983. buf.append('}');
  1984. return buf.toString();
  1985. /*
  1986. * if (s.length() == 0) return s; // Protect against ArrayIndexOutOf....
  1987. * StringBuffer buf = new StringBuffer();
  1988. *
  1989. * Matcher mcr = titleCapitalPattern.matcher(s.substring(1)); while
  1990. * (mcr.find()) { String replaceStr = mcr.group();
  1991. * mcr.appendReplacement(buf, "{" + replaceStr + "}"); }
  1992. * mcr.appendTail(buf); return s.substring(0, 1) + buf.toString();
  1993. */
  1994. }
  1995. static Pattern bracedTitleCapitalPattern = Pattern.compile("\\{[A-Z]+\\}");
  1996. /**
  1997. * This method looks for occurences of capital letters enclosed in an
  1998. * arbitrary number of pairs of braces, e.g. "{AB}" or "{{T}}". All of these
  1999. * pairs of braces are removed.
  2000. *
  2001. * @param s
  2002. * The String to analyze.
  2003. * @return A new String with braces removed.
  2004. */
  2005. public static String removeBracesAroundCapitals(String s) {
  2006. String previous = s;
  2007. while ((s = removeSingleBracesAroundCapitals(s)).length() < previous.length()) {
  2008. previous = s;
  2009. }
  2010. return s;
  2011. }
  2012. /**
  2013. * This method looks for occurences of capital letters enclosed in one pair
  2014. * of braces, e.g. "{AB}". All these are replaced by only the capitals in
  2015. * between the braces.
  2016. *
  2017. * @param s
  2018. * The String to analyze.
  2019. * @return A new String with braces removed.
  2020. */
  2021. public static String removeSingleBracesAroundCapitals(String s) {
  2022. Matcher mcr = bracedTitleCapitalPattern.matcher(s);
  2023. StringBuffer buf = new StringBuffer();
  2024. while (mcr.find()) {
  2025. String replaceStr = mcr.group();
  2026. mcr.appendReplacement(buf, replaceStr.substring(1, replaceStr.length() - 1));
  2027. }
  2028. mcr.appendTail(buf);
  2029. return buf.toString();
  2030. }
  2031. /**
  2032. * This method looks up what kind of external binding is used for the given
  2033. * field, and constructs on OpenFileFilter suitable for browsing for an
  2034. * external file.
  2035. *
  2036. * @param fieldName
  2037. * The BibTeX field in question.
  2038. * @return The file filter.
  2039. */
  2040. public static OpenFileFilter getFileFilterForField(String fieldName) {
  2041. String s = BibtexFields.getFieldExtras(fieldName);
  2042. final String ext = "." + fieldName.toLowerCase();
  2043. final OpenFileFilter off;
  2044. if (s.equals("browseDocZip"))
  2045. off = new OpenFileFilter(new String[] { ext, ext + ".gz", ext + ".bz2" });
  2046. else
  2047. off = new OpenFileFilter(new String[] { ext });
  2048. return off;
  2049. }
  2050. /**
  2051. * This method can be used to display a "rich" error dialog which offers the
  2052. * entire stack trace for an exception.
  2053. *
  2054. * @param parent
  2055. * @param e
  2056. */
  2057. public static void showQuickErrorDialog(JFrame parent, String title, Exception e) {
  2058. // create and configure a text area - fill it with exception text.
  2059. final JPanel pan = new JPanel(), details = new JPanel();
  2060. final CardLayout crd = new CardLayout();
  2061. pan.setLayout(crd);
  2062. final JTextArea textArea = new JTextArea();
  2063. textArea.setFont(new Font("Sans-Serif", Font.PLAIN, 10));
  2064. textArea.setEditable(false);
  2065. StringWriter writer = new StringWriter();
  2066. e.printStackTrace(new PrintWriter(writer));
  2067. textArea.setText(writer.toString());
  2068. JLabel lab = new JLabel(e.getMessage());
  2069. JButton flip = new JButton(Globals.lang("Details"));
  2070. FormLayout layout = new FormLayout("left:pref", "");
  2071. DefaultFormBuilder builder = new DefaultFormBuilder(layout);
  2072. builder.append(lab);
  2073. builder.nextLine();
  2074. builder.append(Box.createVerticalGlue());
  2075. builder.nextLine();
  2076. builder.append(flip);
  2077. final JPanel simple = builder.getPanel();
  2078. // stuff it in a scrollpane with a controlled size.
  2079. JScrollPane scrollPane = new JScrollPane(textArea);
  2080. scrollPane.setPreferredSize(new Dimension(350, 150));
  2081. details.setLayout(new BorderLayout());
  2082. details.add(scrollPane, BorderLayout.CENTER);
  2083. flip.addActionListener(new ActionListener() {
  2084. public void actionPerformed(ActionEvent event) {
  2085. crd.show(pan, "details");
  2086. }
  2087. });
  2088. pan.add(simple, "simple");
  2089. pan.add(details, "details");
  2090. // pass the scrollpane to the joptionpane.
  2091. JOptionPane.showMessageDialog(parent, pan, title, JOptionPane.ERROR_MESSAGE);
  2092. }
  2093. public static String wrapHTML(String s, final int lineWidth) {
  2094. StringBuffer sb = new StringBuffer();
  2095. StringTokenizer tok = new StringTokenizer(s);
  2096. int charsLeft = lineWidth;
  2097. while (tok.hasMoreTokens()) {
  2098. String word = tok.nextToken();
  2099. if (charsLeft == lineWidth) { // fresh line
  2100. sb.append(word);
  2101. charsLeft -= word.length();
  2102. if (charsLeft <= 0) {
  2103. sb.append("<br>\n");
  2104. charsLeft = lineWidth;
  2105. }
  2106. } else { // continue previous line
  2107. if (charsLeft < word.length() + 1) {
  2108. sb.append("<br>\n");
  2109. sb.append(word);
  2110. if (word.length() >= lineWidth - 1) {
  2111. sb.append("<br>\n");
  2112. charsLeft = lineWidth;
  2113. } else {
  2114. sb.append(" ");
  2115. charsLeft = lineWidth - word.length() - 1;
  2116. }
  2117. } else {
  2118. sb.append(' ').append(word);
  2119. charsLeft -= word.length() + 1;
  2120. }
  2121. }
  2122. }
  2123. return sb.toString();
  2124. }
  2125. /**
  2126. * Creates a String containing the current date (and possibly time),
  2127. * formatted according to the format set in preferences under the key
  2128. * "timeStampFormat".
  2129. *
  2130. * @return The date string.
  2131. */
  2132. public static String easyDateFormat() {
  2133. // Date today = new Date();
  2134. return easyDateFormat(new Date());
  2135. }
  2136. /**
  2137. * Creates a readable Date string from the parameter date. The format is set
  2138. * in preferences under the key "timeStampFormat".
  2139. *
  2140. * @return The formatted date string.
  2141. */
  2142. public static String easyDateFormat(Date date) {
  2143. // first use, create an instance
  2144. if (dateFormatter == null) {
  2145. String format = Globals.prefs.get("timeStampFormat");
  2146. dateFormatter = new SimpleDateFormat(format);
  2147. }
  2148. return dateFormatter.format(date);
  2149. }
  2150. public static void markEntry(BibtexEntry be, int markIncrement, boolean increment, NamedCompound ce) {
  2151. Object o = be.getField(BibtexFields.MARKED);
  2152. int prevMarkLevel = 0;
  2153. String newValue = null;
  2154. if (o != null) {
  2155. String s = o.toString();
  2156. int index = s.indexOf(Globals.prefs.WRAPPED_USERNAME);
  2157. if (index >= 0) {
  2158. // Already marked 1 for this user.
  2159. prevMarkLevel = 1;
  2160. newValue = s.substring(0, index)
  2161. + s.substring(index+Globals.prefs.WRAPPED_USERNAME.length())
  2162. + Globals.prefs.WRAPPED_USERNAME.substring(0,
  2163. Globals.prefs.WRAPPED_USERNAME.length()-1)+":"+
  2164. (increment ? Math.min(MAX_MARKING_LEVEL, prevMarkLevel+markIncrement)
  2165. : markIncrement)+"]";
  2166. }
  2167. else {
  2168. Matcher m = markNumberPattern.matcher(s);
  2169. if (m.find()) {
  2170. try {
  2171. prevMarkLevel = Integer.parseInt(m.group(1));
  2172. newValue = s.substring(0, m.start(1))+
  2173. (increment ? Math.min(MAX_MARKING_LEVEL, prevMarkLevel+markIncrement)
  2174. : markIncrement)+
  2175. s.substring(m.end(1));
  2176. } catch (NumberFormatException ex) {
  2177. // Do nothing.
  2178. }
  2179. }
  2180. }
  2181. }
  2182. if (newValue == null)
  2183. newValue = Globals.prefs.WRAPPED_USERNAME.substring(0,
  2184. Globals.prefs.WRAPPED_USERNAME.length()-1)+":"+markIncrement+"]";
  2185. ce.addEdit(new UndoableFieldChange(be, BibtexFields.MARKED, be
  2186. .getField(BibtexFields.MARKED), newValue));
  2187. be.setField(BibtexFields.MARKED, newValue);
  2188. }
  2189. public static void unmarkEntry(BibtexEntry be, boolean onlyMaxLevel,
  2190. BibtexDatabase database, NamedCompound ce) {
  2191. Object o = be.getField(BibtexFields.MARKED);
  2192. if (o != null) {
  2193. String s = o.toString();
  2194. if (s.equals("0")) {
  2195. if (!onlyMaxLevel) {
  2196. unmarkOldStyle(be, database, ce);
  2197. }
  2198. return;
  2199. }
  2200. String newValue = null;
  2201. int index = s.indexOf(Globals.prefs.WRAPPED_USERNAME);
  2202. if (index >= 0) {
  2203. // Marked 1 for this user.
  2204. if (!onlyMaxLevel)
  2205. newValue = s.substring(0, index)
  2206. + s.substring(index+Globals.prefs.WRAPPED_USERNAME.length());
  2207. else return;
  2208. }
  2209. else {
  2210. Matcher m = markNumberPattern.matcher(s);
  2211. if (m.find()) {
  2212. try {
  2213. int prevMarkLevel = Integer.parseInt(m.group(1));
  2214. if (!onlyMaxLevel || (prevMarkLevel == MARK_COLOR_LEVELS)) {
  2215. if (prevMarkLevel > 1)
  2216. newValue = s.substring(0, m.start(1))+
  2217. s.substring(m.end(1));
  2218. else {
  2219. String toRemove = Globals.prefs.WRAPPED_USERNAME.substring(0,
  2220. Globals.prefs.WRAPPED_USERNAME.length()-1)+":1]";
  2221. index = s.indexOf(toRemove);
  2222. if (index >= 0) {
  2223. newValue = s.substring(0, index)
  2224. + s.substring(index+toRemove.length());
  2225. }
  2226. }
  2227. }
  2228. else return;
  2229. } catch (NumberFormatException ex) {
  2230. // Do nothing.
  2231. }
  2232. }
  2233. }
  2234. /*int piv = 0, hit;
  2235. StringBuffer sb = new StringBuffer();
  2236. while ((hit = s.indexOf(G047749118118
  2237. 1110lobals.prefs.WRAPPED_USERNAME, piv)) >= 0) {
  2238. if (hit > 0)
  2239. sb.append(s.substring(piv, hit));
  2240. piv = hit + Globals.prefs.WRAPPED_USERNAME.length();
  2241. }
  2242. if (piv < s.length() - 1) {
  2243. sb.append(s.substring(piv));
  2244. }
  2245. String newVal = sb.length() > 0 ? sb.toString() : null;*/
  2246. ce.addEdit(new UndoableFieldChange(be, BibtexFields.MARKED, be
  2247. .getField(BibtexFields.MARKED), newValue));
  2248. be.setField(BibtexFields.MARKED, newValue);
  2249. }
  2250. }
  2251. /**
  2252. * An entry is marked with a "0", not in the new style with user names. We
  2253. * want to unmark it as transparently as possible. Since this shouldn't
  2254. * happen too often, we do it by scanning the "owner" fields of the entire
  2255. * database, collecting all user names. We then mark the entry for all users
  2256. * except the current one. Thus only the user who unmarks will see that it
  2257. * is unmarked, and we get rid of the old-style marking.
  2258. *
  2259. * @param be
  2260. * @param ce
  2261. */
  2262. private static void unmarkOldStyle(BibtexEntry be, BibtexDatabase database, NamedCompound ce) {
  2263. TreeSet<Object> owners = new TreeSet<Object>();
  2264. for (BibtexEntry entry : database.getEntries()){
  2265. Object o = entry.getField(BibtexFields.OWNER);
  2266. if (o != null)
  2267. owners.add(o);
  2268. // System.out.println("Owner: "+entry.getField(Globals.OWNER));
  2269. }
  2270. owners.remove(Globals.prefs.get("defaultOwner"));
  2271. StringBuffer sb = new StringBuffer();
  2272. for (Object owner : owners) {
  2273. sb.append('[');
  2274. sb.append(owner.toString());
  2275. sb.append(']');
  2276. }
  2277. String newVal = sb.toString();
  2278. if (newVal.length() == 0)
  2279. newVal = null;
  2280. ce.addEdit(new UndoableFieldChange(be, BibtexFields.MARKED, be
  2281. .getField(BibtexFields.MARKED), newVal));
  2282. be.setField(BibtexFields.MARKED, newVal);
  2283. }
  2284. public static int isMarked(BibtexEntry be) {
  2285. Object fieldVal = be.getField(BibtexFields.MARKED);
  2286. if (fieldVal == null)
  2287. return 0;
  2288. String s = (String) fieldVal;
  2289. if (s.equals("0"))
  2290. return 1;
  2291. int index = s.indexOf(Globals.prefs.WRAPPED_USERNAME);
  2292. if (index >= 0)
  2293. return 1;
  2294. Matcher m = markNumberPattern.matcher(s);
  2295. if (m.find()) {
  2296. try {
  2297. int value = Integer.parseInt(m.group(1));
  2298. return value;
  2299. } catch (NumberFormatException ex) {
  2300. return 1;
  2301. }
  2302. }
  2303. else return 0;
  2304. }
  2305. /**
  2306. * Set a given field to a given value for all entries in a Collection. This
  2307. * method DOES NOT update any UndoManager, but returns a relevant
  2308. * CompoundEdit that should be registered by the caller.
  2309. *
  2310. * @param entries
  2311. * The entries to set the field for.
  2312. * @param field
  2313. * The name of the field to set.
  2314. * @param text
  2315. * The value to set. This value can be null, indicating that the
  2316. * field should be cleared.
  2317. * @param overwriteValues
  2318. * Indicate whether the value should be set even if an entry
  2319. * already has the field set.
  2320. * @return A CompoundEdit for the entire operation.
  2321. */
  2322. public static UndoableEdit massSetField(Collection<BibtexEntry> entries, String field, String text,
  2323. boolean overwriteValues) {
  2324. NamedCompound ce = new NamedCompound(Globals.lang("Set field"));
  2325. for (BibtexEntry entry : entries){
  2326. String oldVal = entry.getField(field);
  2327. // If we are not allowed to overwrite values, check if there is a
  2328. // nonempty
  2329. // value already for this entry:
  2330. if (!overwriteValues && (oldVal != null) && ((oldVal).length() > 0))
  2331. continue;
  2332. if (text != null)
  2333. entry.setField(field, text);
  2334. else
  2335. entry.clearField(field);
  2336. ce.addEdit(new UndoableFieldChange(entry, field, oldVal, text));
  2337. }
  2338. ce.end();
  2339. return ce;
  2340. }
  2341. /**
  2342. * Move contents from one field to another for a Collection of entries.
  2343. * @param entries The entries to do this operation for.
  2344. * @param field The field to move contents from.
  2345. * @param newField The field to move contents into.
  2346. * @param overwriteValues If true, overwrites any existing values in the new field.
  2347. * If false, makes no change for entries with existing value in the new field.
  2348. * @return A CompoundEdit for the entire operation.
  2349. */
  2350. public static UndoableEdit massRenameField(Collection<BibtexEntry> entries, String field,
  2351. String newField, boolean overwriteValues) {
  2352. NamedCompound ce = new NamedCompound(Globals.lang("Rename field"));
  2353. for (BibtexEntry entry : entries){
  2354. String valToMove = entry.getField(field);
  2355. // If there is no value, do nothing:
  2356. if ((valToMove == null) || (valToMove.length() == 0))
  2357. continue;
  2358. // If we are not allowed to overwrite values, check if there is a
  2359. // nonempy value already for this entry for the new field:
  2360. String valInNewField = entry.getField(newField);
  2361. if (!overwriteValues && (valInNewField != null) && (valInNewField.length() > 0))
  2362. continue;
  2363. entry.setField(newField, valToMove);
  2364. ce.addEdit(new UndoableFieldChange(entry, newField, valInNewField,valToMove));
  2365. entry.clearField(field);
  2366. ce.addEdit(new UndoableFieldChange(entry, field, valToMove, null));
  2367. }
  2368. ce.end();
  2369. return ce;
  2370. }
  2371. /**
  2372. * Make a list of supported character encodings that can encode all
  2373. * characters in the given String.
  2374. *
  2375. * @param characters
  2376. * A String of characters that should be supported by the
  2377. * encodings.
  2378. * @return A List of character encodings
  2379. */
  2380. public static List<String> findEncodingsForString(String characters) {
  2381. List<String> encodings = new ArrayList<String>();
  2382. for (int i = 0; i < Globals.ENCODINGS.length; i++) {
  2383. CharsetEncoder encoder = Charset.forName(Globals.ENCODINGS[i]).newEncoder();
  2384. if (encoder.canEncode(characters))
  2385. encodings.add(Globals.ENCODINGS[i]);
  2386. }
  2387. return encodings;
  2388. }
  2389. /**
  2390. * Will convert a two digit year using the following scheme (describe at
  2391. * http://www.filemaker.com/help/02-Adding%20and%20view18.html):
  2392. *
  2393. * If a two digit year is encountered they are matched against the last 69
  2394. * years and future 30 years.
  2395. *
  2396. * For instance if it is the year 1992 then entering 23 is taken to be 1923
  2397. * but if you enter 23 in 1993 then it will evaluate to 2023.
  2398. *
  2399. * @param year
  2400. * The year to convert to 4 digits.
  2401. * @return
  2402. */
  2403. public static String toFourDigitYear(String year) {
  2404. if (thisYear == 0) {
  2405. thisYear = Calendar.getInstance().get(Calendar.YEAR);
  2406. }
  2407. return toFourDigitYear(year, thisYear);
  2408. }
  2409. public static int thisYear;
  2410. /**
  2411. * Will convert a two digit year using the following scheme (describe at
  2412. * http://www.filemaker.com/help/02-Adding%20and%20view18.html):
  2413. *
  2414. * If a two digit year is encountered they are matched against the last 69
  2415. * years and future 30 years.
  2416. *
  2417. * For instance if it is the year 1992 then entering 23 is taken to be 1923
  2418. * but if you enter 23 in 1993 then it will evaluate to 2023.
  2419. *
  2420. * @param year
  2421. * The year to convert to 4 digits.
  2422. * @return
  2423. */
  2424. public static String toFourDigitYear(String year, int thisYear) {
  2425. if (year.length() != 2)
  2426. return year;
  2427. try {
  2428. int thisYearTwoDigits = thisYear % 100;
  2429. int thisCentury = thisYear - thisYearTwoDigits;
  2430. int yearNumber = Integer.parseInt(year);
  2431. if (yearNumber == thisYearTwoDigits) {
  2432. return String.valueOf(thisYear);
  2433. }
  2434. // 20 , 90
  2435. // 99 > 30
  2436. if ((yearNumber + 100 - thisYearTwoDigits) % 100 > 30) {
  2437. if (yearNumber < thisYearTwoDigits) {
  2438. return String.valueOf(thisCentury + yearNumber);
  2439. } else {
  2440. return String.valueOf(thisCentury - 100 + yearNumber);
  2441. }
  2442. } else {
  2443. if (yearNumber < thisYearTwoDigits) {
  2444. return String.valueOf(thisCentury + 100 + yearNumber);
  2445. } else {
  2446. return String.valueOf(thisCentury + yearNumber);
  2447. }
  2448. }
  2449. } catch (NumberFormatException e) {
  2450. return year;
  2451. }
  2452. }
  2453. /**
  2454. * Will return an integer indicating the month of the entry from 0 to 11.
  2455. *
  2456. * -1 signals a unknown month string.
  2457. *
  2458. * This method accepts three types of months given:
  2459. * - Single and Double Digit months from 1 to 12 (01 to 12)
  2460. * - 3 Digit BibTex strings (jan, feb, mar...)
  2461. * - Full English Month identifiers.
  2462. *
  2463. * @param month
  2464. * @return
  2465. */
  2466. public static int getMonthNumber(String month) {
  2467. month = month.replaceAll("#", "").toLowerCase();
  2468. for (int i = 0; i < Globals.MONTHS.length; i++) {
  2469. if (month.startsWith(Globals.MONTHS[i])) {
  2470. return i;
  2471. }
  2472. }
  2473. try {
  2474. return Integer.parseInt(month) - 1;
  2475. } catch (NumberFormatException ignored) {
  2476. }
  2477. return -1;
  2478. }
  2479. /**
  2480. * Encodes a two-dimensional String array into a single string, using ':' and
  2481. * ';' as separators. The characters ':' and ';' are escaped with '\'.
  2482. * @param values The String array.
  2483. * @return The encoded String.
  2484. */
  2485. public static String encodeStringArray(String[][] values) {
  2486. StringBuilder sb = new StringBuilder();
  2487. for (int i = 0; i < values.length; i++) {
  2488. sb.append(encodeStringArray(values[i]));
  2489. if (i < values.length-1)
  2490. sb.append(';');
  2491. }
  2492. return sb.toString();
  2493. }
  2494. /**
  2495. * Encodes a String array into a single string, using ':' as separator.
  2496. * The characters ':' and ';' are escaped with '\'.
  2497. * @param entry The String array.
  2498. * @return The encoded String.
  2499. */
  2500. public static String encodeStringArray(String[] entry) {
  2501. StringBuilder sb = new StringBuilder();
  2502. for (int i = 0; i < entry.length; i++) {
  2503. sb.append(encodeString(entry[i]));
  2504. if (i < entry.length-1)
  2505. sb.append(':');
  2506. }
  2507. return sb.toString();
  2508. }
  2509. /**
  2510. * Decodes an encoded double String array back into array form. The array
  2511. * is assumed to be square, and delimited by the characters ';' (first dim) and
  2512. * ':' (second dim).
  2513. * @param value The encoded String to be decoded.
  2514. * @return The decoded String array.
  2515. */
  2516. public static String[][] decodeStringDoubleArray(String value) {
  2517. ArrayList<ArrayList<String>> newList = new ArrayList<ArrayList<String>>();
  2518. StringBuilder sb = new StringBuilder();
  2519. ArrayList<String> thisEntry = new ArrayList<String>();
  2520. boolean escaped = false;
  2521. for (int i=0; i<value.length(); i++) {
  2522. char c = value.charAt(i);
  2523. if (!escaped && (c == '\\')) {
  2524. escaped = true;
  2525. continue;
  2526. }
  2527. else if (!escaped && (c == ':')) {
  2528. thisEntry.add(sb.toString());
  2529. sb = new StringBuilder();
  2530. }
  2531. else if (!escaped && (c == ';')) {
  2532. thisEntry.add(sb.toString());
  2533. sb = new StringBuilder();
  2534. newList.add(thisEntry);
  2535. thisEntry = new ArrayList<String>();
  2536. }
  2537. else sb.append(c);
  2538. escaped = false;
  2539. }
  2540. if (sb.length() > 0)
  2541. thisEntry.add(sb.toString());
  2542. if (thisEntry.size() > 0)
  2543. newList.add(thisEntry);
  2544. // Convert to String[][]:
  2545. String[][] res = new String[newList.size()][];
  2546. for (int i = 0; i < res.length; i++) {
  2547. res[i] = new String[newList.get(i).size()];
  2548. for (int j = 0; j < res[i].length; j++) {
  2549. res[i][j] = newList.get(i).get(j);
  2550. }
  2551. }
  2552. return res;
  2553. }
  2554. public static String encodeString(String s) {
  2555. if (s == null)
  2556. return null;
  2557. StringBuilder sb = new StringBuilder();
  2558. for (int i=0; i<s.length(); i++) {
  2559. char c = s.charAt(i);
  2560. if ((c == ';') || (c == ':') || (c == '\\'))
  2561. sb.append('\\');
  2562. sb.append(c);
  2563. }
  2564. return sb.toString();
  2565. }
  2566. /**
  2567. * Static equals that can also return the right result when one of the
  2568. * objects is null.
  2569. *
  2570. * @param one
  2571. * The object whose equals method is called if the first is not
  2572. * null.
  2573. * @param two
  2574. * The object passed to the first one if the first is not null.
  2575. * @return <code>one == null ? two == null : one.equals(two);</code>
  2576. */
  2577. public static boolean equals(Object one, Object two) {
  2578. return one == null ? two == null : one.equals(two);
  2579. }
  2580. /**
  2581. * Returns the given string but with the first character turned into an
  2582. * upper case character.
  2583. *
  2584. * Example: testTest becomes TestTest
  2585. *
  2586. * @param string
  2587. * The string to change the first character to upper case to.
  2588. * @return A string has the first character turned to upper case and the
  2589. * rest unchanged from the given one.
  2590. */
  2591. public static String toUpperFirstLetter(String string){
  2592. if (string == null)
  2593. throw new IllegalArgumentException();
  2594. if (string.length() == 0)
  2595. return string;
  2596. return Character.toUpperCase(string.charAt(0)) + string.substring(1);
  2597. }
  2598. /**
  2599. * Run an AbstractWorker's methods using Spin features to put each method
  2600. * on the correct thread.
  2601. * @param worker The worker to run.
  2602. * @throws Throwable
  2603. */
  2604. public static void runAbstractWorker(AbstractWorker worker) throws Throwable {
  2605. // This part uses Spin's features:
  2606. Worker wrk = worker.getWorker();
  2607. // The Worker returned by getWorker() has been wrapped
  2608. // by Spin.off(), which makes its methods be run in
  2609. // a different thread from the EDT.
  2610. CallBack clb = worker.getCallBack();
  2611. worker.init(); // This method runs in this same thread, the EDT.
  2612. // Useful for initial GUI actions, like printing a message.
  2613. // The CallBack returned by getCallBack() has been wrapped
  2614. // by Spin.over(), which makes its methods be run on
  2615. // the EDT.
  2616. wrk.run(); // Runs the potentially time-consuming action
  2617. // without freezing the GUI. The magic is that THIS line
  2618. // of execution will not continue until run() is finished.
  2619. clb.update(); // Runs the update() method on the EDT.
  2620. }
  2621. /**
  2622. * This method checks whether there is a lock file for the given file. If
  2623. * there is, it waits for 500 ms. This is repeated until the lock is gone
  2624. * or we have waited the maximum number of times.
  2625. *
  2626. * @param file The file to check the lock for.
  2627. * @param maxWaitCount The maximum number of times to wait.
  2628. * @return true if the lock file is gone, false if it is still there.
  2629. */
  2630. public static boolean waitForFileLock(File file, int maxWaitCount) {
  2631. // Check if the file is locked by another JabRef user:
  2632. int lockCheckCount = 0;
  2633. while (Util.hasLockFile(file)) {
  2634. if (lockCheckCount++ == maxWaitCount) {
  2635. return false;
  2636. }
  2637. try { Thread.sleep(500); } catch (InterruptedException ignored) {}
  2638. }
  2639. return true;
  2640. }
  2641. /**
  2642. * Check whether a lock file exists for this file.
  2643. * @param file The file to check.
  2644. * @return true if a lock file exists, false otherwise.
  2645. */
  2646. public static boolean hasLockFile(File file) {
  2647. File lock = new File(file.getPath()+ SaveSession.LOCKFILE_SUFFIX);
  2648. return lock.exists();
  2649. }
  2650. /**
  2651. * Find the lock file's last modified time, if it has a lock file.
  2652. * @param file The file to check.
  2653. * @return the last modified time if lock file exists, -1 otherwise.
  2654. */
  2655. public static long getLockFileTimeStamp(File file) {
  2656. File lock = new File(file.getPath()+ SaveSession.LOCKFILE_SUFFIX);
  2657. return lock.exists() ? lock.lastModified() : -1;
  2658. }
  2659. /**
  2660. * Check if a lock file exists, and delete it if it does.
  2661. * @return true if the lock file existed, false otherwise.
  2662. * @throws IOException if something goes wrong.
  2663. */
  2664. public static boolean deleteLockFile(File file) {
  2665. File lock = new File(file.getPath()+SaveSession.LOCKFILE_SUFFIX);
  2666. if (!lock.exists()) {
  2667. return false;
  2668. }
  2669. lock.delete();
  2670. return true;
  2671. }
  2672. /**
  2673. * Build a String array containing all those elements of all that are not
  2674. * in subset.
  2675. * @param all The array of all values.
  2676. * @param subset The subset of values.
  2677. * @return The remainder that is not part of the subset.
  2678. */
  2679. public static String[] getRemainder(String[] all, String[] subset) {
  2680. ArrayList<String> al = new ArrayList<String>();
  2681. for (String anAll : all) {
  2682. boolean found = false;
  2683. for (String aSubset : subset) {
  2684. if (aSubset.equals(anAll)) {
  2685. found = true;
  2686. break;
  2687. }
  2688. }
  2689. if (!found) al.add(anAll);
  2690. }
  2691. return al.toArray(new String[al.size()]);
  2692. }
  2693. public static <T> T[] concat(T[] first, T[] second) {
  2694. T[] result = Arrays.copyOf(first, first.length + second.length);
  2695. System.arraycopy(second, 0, result, first.length, second.length);
  2696. return result;
  2697. }
  2698. /**
  2699. * Determines filename provided by an entry in a database
  2700. *
  2701. * @param database the database, where the entry is located
  2702. * @param entry the entry to which the file should be linked to
  2703. * @return a suggested fileName
  2704. */
  2705. public static String getLinkedFileName(BibtexDatabase database, BibtexEntry entry) {
  2706. String targetName = entry.getCiteKey() == null ? "default" : entry.getCiteKey();
  2707. StringReader sr = new StringReader(Globals.prefs.get(ImportSettingsTab.PREF_IMPORT_FILENAMEPATTERN));
  2708. Layout layout = null;
  2709. try {
  2710. layout = new LayoutHelper(sr).getLayoutFromText(Globals.FORMATTER_PACKAGE);
  2711. } catch (Exception e) {
  2712. Globals.logger(Globals.lang("Wrong Format").concat(" ").concat(e.toString()));
  2713. }
  2714. if (layout != null) {
  2715. targetName = layout.doLayout(entry, database);
  2716. }
  2717. //Removes illegal characters from filename
  2718. targetName = FileNameCleaner.cleanFileName(targetName);
  2719. return targetName;
  2720. }
  2721. // DOI-regexp provided by http://stackoverflow.com/a/10324802/873282
  2722. // Some DOI's are not caught by the regexp in the above link, i.e. 10.1002/(SICI)1522-2594(199911)42:5<952::AID-MRM16>3.0.CO;2-S
  2723. // Removed <> from non-permitted characters
  2724. private static final String REGEXP_PLAINDOI = "\\b(10[.][0-9]{4,}(?:[.][0-9]+)*/(?:(?![\"&\\'])\\S)+)\\b";
  2725. private static final String REGEXP_DOI_WITH_HTTP_PREFIX = "http[s]?://[^\\s]*?" + REGEXP_PLAINDOI;
  2726. private static final Pattern PATTERN_PLAINDOI = Pattern.compile(REGEXP_PLAINDOI);
  2727. /**
  2728. * Check if the String matches a DOI (with http://...)
  2729. */
  2730. public static boolean checkForDOIwithHTTPprefix(String check) {
  2731. if (check == null)
  2732. return false;
  2733. return check.matches(".*" + REGEXP_DOI_WITH_HTTP_PREFIX + ".*");
  2734. }
  2735. /**
  2736. *
  2737. * @param check - string to check
  2738. * @return true if "check" contains a DOI
  2739. */
  2740. public static boolean checkForPlainDOI(String check) {
  2741. if (check == null)
  2742. return false;
  2743. return check.matches(".*" + REGEXP_PLAINDOI + ".*");
  2744. }
  2745. /**
  2746. * Remove the http://... from DOI
  2747. *
  2748. * @param doi - may not be null
  2749. * @return first DOI in the given String (without http://... prefix). If no DOI exists, the complete string is returned
  2750. */
  2751. public static String getDOI(String doi) {
  2752. Matcher matcher = PATTERN_PLAINDOI.matcher(doi);
  2753. if (matcher.find()) {
  2754. return matcher.group();
  2755. } else {
  2756. return doi;
  2757. }
  2758. }
  2759. public static void removeDOIfromBibtexEntryField(BibtexEntry bes, String fieldName, NamedCompound ce) {
  2760. String origValue = bes.getField(fieldName);
  2761. String value = origValue;
  2762. value = value.replaceAll(REGEXP_DOI_WITH_HTTP_PREFIX, "");
  2763. value = value.replaceAll(REGEXP_PLAINDOI, "");
  2764. value = value.trim();
  2765. if (value.isEmpty()) value = null;
  2766. if (!origValue.equals(value)) {
  2767. ce.addEdit(new UndoableFieldChange(bes, fieldName, origValue, value));
  2768. bes.setField(fieldName, value);
  2769. }
  2770. }
  2771. /**
  2772. *
  2773. * @param fileName
  2774. * @param destFilename
  2775. * @return
  2776. */
  2777. public static boolean renameFile(String fileName, String destFilename)
  2778. {
  2779. // File (or directory) with old name
  2780. File fromFile = new File(fileName);
  2781. // File (or directory) with new name
  2782. File toFile = new File(destFilename);
  2783. // Rename file (or directory)
  2784. boolean success = fromFile.renameTo(toFile);
  2785. return success;
  2786. }
  2787. public static ArrayList<String> getSeparatedKeywords(String keywords) {
  2788. ArrayList<String> res = new ArrayList<String>();
  2789. if (keywords == null) return res;
  2790. // _NOSPACE is a hack to support keywords such as "choreography transactions"
  2791. // a more intelligent algorithm would check for the separator chosen (SEPARATING_CHARS_NOSPACE)
  2792. // if nothing is found, " " is likely to be the separating char.
  2793. // solution by RisKeywords.java: s.split(",[ ]*")
  2794. StringTokenizer tok = new StringTokenizer(keywords, Globals.SEPARATING_CHARS_NOSPACE);
  2795. while (tok.hasMoreTokens()) {
  2796. String word = tok.nextToken().trim();
  2797. res.add(word);
  2798. }
  2799. return res;
  2800. }
  2801. public static ArrayList<String> getSeparatedKeywords(BibtexEntry be) {
  2802. return getSeparatedKeywords(be.getField("keywords"));
  2803. }
  2804. public static void putKeywords(BibtexEntry entry, ArrayList<String> keywords, NamedCompound ce) {
  2805. // Set Keyword Field
  2806. String oldValue = entry.getField("keywords");
  2807. String newValue;
  2808. if (keywords.size() > 0) {
  2809. StringBuilder sb = new StringBuilder();
  2810. for (String keyword: keywords) {
  2811. sb.append(keyword);
  2812. sb.append(", ");
  2813. }
  2814. sb.delete(sb.length()-2, sb.length());
  2815. newValue = sb.toString();
  2816. } else {
  2817. newValue = null;
  2818. }
  2819. if ((oldValue == null) && (newValue == null))
  2820. return;
  2821. if ((oldValue == null) || (!oldValue.equals(newValue))) {
  2822. entry.setField("keywords", newValue);
  2823. if (ce!=null) ce.addEdit(new UndoableFieldChange(entry, "keywords", oldValue, newValue));
  2824. }
  2825. }
  2826. /**
  2827. * @param ce indicates the undo named compound. May be null
  2828. */
  2829. public static void updateField(BibtexEntry be, String field, String newValue, NamedCompound ce) {
  2830. updateField(be, field, newValue, ce, false);
  2831. }
  2832. /**
  2833. * @param ce indicates the undo named compound. May be null
  2834. */
  2835. public static void updateField(BibtexEntry be, String field, String newValue, NamedCompound ce, Boolean nullFieldIfValueIsTheSame) {
  2836. String oldValue = be.getField(field);
  2837. if (nullFieldIfValueIsTheSame && (oldValue != null) && (oldValue.equals(newValue))) {
  2838. // if oldValue == newValue then reset field if required by parameter
  2839. newValue = null;
  2840. }
  2841. if ((oldValue == null) && (newValue == null))
  2842. return;
  2843. if ((oldValue==null) || (!oldValue.equals(newValue))) {
  2844. be.setField(field, newValue);
  2845. if (ce!=null) ce.addEdit(new UndoableFieldChange(be, field, oldValue, newValue));
  2846. }
  2847. }
  2848. /**
  2849. * Binds ESC-Key to cancel button
  2850. * @param rootPane the pane to bind the action to. Typically, this variable is retrieved by this.getRootPane();
  2851. * @param cancelAction the action to bind
  2852. */
  2853. public static void bindCloseDialogKeyToCancelAction(JRootPane rootPane,
  2854. Action cancelAction) {
  2855. InputMap im = rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
  2856. ActionMap am = rootPane.getActionMap();
  2857. im.put(Globals.prefs.getKey("Close dialog"), "close");
  2858. am.put("close", cancelAction);
  2859. }
  2860. /**
  2861. * Download the URL and return contents as a String.
  2862. * @param source
  2863. * @return
  2864. * @throws IOException
  2865. */
  2866. public static String getResults(URLConnection source) throws IOException {
  2867. return getResultsWithEncoding(source, null);
  2868. }
  2869. /**
  2870. * Download the URL using specified encoding and return contents as a String.
  2871. * @param source
  2872. * encoding
  2873. * @return
  2874. * @throws IOException
  2875. */
  2876. public static String getResultsWithEncoding(URLConnection source, String encoding) throws IOException {
  2877. InputStreamReader in;
  2878. if(encoding != null) {
  2879. in = new InputStreamReader(source.getInputStream(), encoding);
  2880. } else {
  2881. in = new InputStreamReader(source.getInputStream());
  2882. }
  2883. StringBuilder sb = new StringBuilder();
  2884. while(true) {
  2885. int byteRead = in.read();
  2886. if(byteRead == -1) break;
  2887. sb.append((char)byteRead);
  2888. }
  2889. return sb.toString();
  2890. }
  2891. public static boolean updateTimeStampIsSet() {
  2892. return (Globals.prefs.getBoolean("useTimeStamp") &&
  2893. Globals.prefs.getBoolean(JabRefPreferences.UPDATE_TIMESTAMP));
  2894. }
  2895. /**
  2896. * Updates the timestamp of the given entry,
  2897. * nests the given undaoableEdit in a named compound,
  2898. * and returns that named compound
  2899. */
  2900. public static NamedCompound doUpdateTimeStamp(BibtexEntry entry, AbstractUndoableEdit undoableEdit) {
  2901. NamedCompound ce = new NamedCompound(undoableEdit.getPresentationName());
  2902. ce.addEdit(undoableEdit);
  2903. String timeStampField = Globals.prefs.get("timeStampField");
  2904. String timestamp = Util.easyDateFormat();
  2905. Util.updateField(entry, timeStampField, timestamp, ce);
  2906. return ce;
  2907. }
  2908. /**
  2909. * Automatically add links for this set of entries, based on the globally stored list of
  2910. * external file types. The entries are modified, and corresponding UndoEdit elements
  2911. * added to the NamedCompound given as argument. Furthermore, all entries which are modified
  2912. * are added to the Set of entries given as an argument.
  2913. *
  2914. * The entries' bibtex keys must have been set - entries lacking key are ignored.
  2915. * The operation is done in a new thread, which is returned for the caller to wait for
  2916. * if needed.
  2917. *
  2918. * @param entries A collection of BibtexEntry objects to find links for.
  2919. * @param ce A NamedCompound to add UndoEdit elements to.
  2920. * @param changedEntries MODIFIED, optional. A Set of BibtexEntry objects to which all modified entries is added. This is used for status output and debugging
  2921. * @param singleTableModel UGLY HACK. The table model to insert links into. Already existing links are not duplicated or removed. This parameter has to be null if entries.count() != 1.
  2922. * The hack has been introduced as a bibtexentry does not (yet) support the function getListTableModel() and the FileListEntryEditor editor holds an instance of that table model and does not reconstruct it after the search has succeeded.
  2923. * @param metaData The MetaData providing the relevant file directory, if any.
  2924. * @param callback An ActionListener that is notified (on the event dispatch thread) when the search is
  2925. * finished. The ActionEvent has id=0 if no new links were added, and id=1 if one or more links were added.
  2926. * This parameter can be null, which means that no callback will be notified.
  2927. * @param diag An instantiated modal JDialog which will be used to display the progress of the autosetting.
  2928. * This parameter can be null, which means that no progress update will be shown.
  2929. * @return the thread performing the autosetting
  2930. */
  2931. public static Thread autoSetLinks(final Collection<BibtexEntry> entries,
  2932. final NamedCompound ce,
  2933. final Set<BibtexEntry> changedEntries,
  2934. final FileListTableModel singleTableModel,
  2935. final MetaData metaData,
  2936. final ActionListener callback,
  2937. final JDialog diag) {
  2938. final ExternalFileType[] types = Globals.prefs.getExternalFileTypeSelection();
  2939. if (diag != null) {
  2940. final JProgressBar prog = new JProgressBar(JProgressBar.HORIZONTAL, 0, types.length-1);
  2941. final JLabel label = new JLabel(Globals.lang("Searching for files"));
  2942. prog.setIndeterminate(true);
  2943. prog.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
  2944. diag.setTitle(Globals.lang("Autosetting links"));
  2945. diag.getContentPane().add(prog, BorderLayout.CENTER);
  2946. diag.getContentPane().add(label, BorderLayout.SOUTH);
  2947. diag.pack();
  2948. diag.setLocationRelativeTo(diag.getParent());
  2949. }
  2950. Runnable r = new Runnable() {
  2951. public void run() {
  2952. // determine directories to search in
  2953. ArrayList<File> dirs = new ArrayList<File>();
  2954. String[] dirsS = metaData.getFileDirectory(GUIGlobals.FILE_FIELD);
  2955. for (String dirs1 : dirsS) {
  2956. dirs.add(new File(dirs1));
  2957. }
  2958. // determine extensions
  2959. Collection<String> extensions = new ArrayList<String>();
  2960. for (final ExternalFileType type : types) {
  2961. extensions.add(type.getExtension());
  2962. }
  2963. // Run the search operation:
  2964. Map<BibtexEntry, java.util.List<File>> result;
  2965. if (Globals.prefs.getBoolean(JabRefPreferences.USE_REG_EXP_SEARCH_KEY)) {
  2966. String regExp = Globals.prefs.get(JabRefPreferences.REG_EXP_SEARCH_EXPRESSION_KEY);
  2967. result = RegExpFileSearch.findFilesForSet(entries, extensions, dirs, regExp);
  2968. } else {
  2969. result = Util.findAssociatedFiles(entries, extensions, dirs);
  2970. }
  2971. boolean foundAny = false;
  2972. // Iterate over the entries:
  2973. for (BibtexEntry anEntry : result.keySet()) {
  2974. FileListTableModel tableModel;
  2975. String oldVal = anEntry.getField(GUIGlobals.FILE_FIELD);
  2976. if (singleTableModel == null) {
  2977. tableModel = new FileListTableModel();
  2978. if (oldVal != null)
  2979. tableModel.setContent(oldVal);
  2980. } else {
  2981. assert (entries.size() == 1);
  2982. tableModel = singleTableModel;
  2983. }
  2984. List<File> files = result.get(anEntry);
  2985. for (File f : files) {
  2986. f = Util.shortenFileName(f, dirsS);
  2987. boolean alreadyHas = false;
  2988. //System.out.println("File: "+f.getPath());
  2989. for (int j = 0; j < tableModel.getRowCount(); j++) {
  2990. FileListEntry existingEntry = tableModel.getEntry(j);
  2991. //System.out.println("Comp: "+existingEntry.getLink());
  2992. if (new File(existingEntry.getLink()).equals(f)) {
  2993. alreadyHas = true;
  2994. break;
  2995. }
  2996. }
  2997. if (!alreadyHas) {
  2998. foundAny = true;
  2999. ExternalFileType type;
  3000. int index = f.getPath().lastIndexOf('.');
  3001. if ((index >= 0) && (index < f.getPath().length() - 1)) {
  3002. type = Globals.prefs.getExternalFileTypeByExt
  3003. (f.getPath().substring(index + 1).toLowerCase());
  3004. } else {
  3005. type = new UnknownExternalFileType("");
  3006. }
  3007. FileListEntry flEntry = new FileListEntry(f.getName(), f.getPath(), type);
  3008. tableModel.addEntry(tableModel.getRowCount(), flEntry);
  3009. String newVal = tableModel.getStringRepresentation();
  3010. if (newVal.length() == 0)
  3011. newVal = null;
  3012. if (ce != null) {
  3013. // store undo information
  3014. UndoableFieldChange change = new UndoableFieldChange(anEntry,
  3015. GUIGlobals.FILE_FIELD, oldVal, newVal);
  3016. ce.addEdit(change);
  3017. }
  3018. // hack: if table model is given, do NOT modify entry
  3019. if (singleTableModel == null) {
  3020. anEntry.setField(GUIGlobals.FILE_FIELD, newVal);
  3021. }
  3022. if (changedEntries != null)
  3023. changedEntries.add(anEntry);
  3024. }
  3025. }
  3026. }
  3027. // handle callbacks and dialog
  3028. final int id = foundAny ? 1 : 0;
  3029. SwingUtilities.invokeLater(new Runnable() {
  3030. public void run() {
  3031. if (diag != null)
  3032. diag.dispose();
  3033. if (callback != null)
  3034. callback.actionPerformed(new ActionEvent(this, id, ""));
  3035. }
  3036. });
  3037. }
  3038. };
  3039. Thread t = new Thread(r);
  3040. t.start();
  3041. if (diag != null) {
  3042. diag.setVisible(true);
  3043. }
  3044. return t;
  3045. }
  3046. /**
  3047. * Automatically add links for this entry to the table model given as an argument, based on
  3048. * the globally stored list of external file types. The entry itself is not modified. The entry's
  3049. * bibtex key must have been set.
  3050. * The operation is done in a new thread, which is returned for the caller to wait for
  3051. * if needed.
  3052. *
  3053. * @param entry The BibtexEntry to find links for.
  3054. * @param singleTableModel The table model to insert links into. Already existing links are not duplicated or removed.
  3055. * @param metaData The MetaData providing the relevant file directory, if any.
  3056. * @param callback An ActionListener that is notified (on the event dispatch thread) when the search is
  3057. * finished. The ActionEvent has id=0 if no new links were added, and id=1 if one or more links were added.
  3058. * This parameter can be null, which means that no callback will be notified. The passed ActionEvent is constructed with
  3059. * (this, id, ""), where id is 1 if something has been done and 0 if nothing has been done.
  3060. * @param diag An instantiated modal JDialog which will be used to display the progress of the autosetting.
  3061. * This parameter can be null, which means that no progress update will be shown.
  3062. * @return the thread performing the autosetting
  3063. */
  3064. public static Thread autoSetLinks(
  3065. final BibtexEntry entry,
  3066. final FileListTableModel singleTableModel,
  3067. final MetaData metaData,
  3068. final ActionListener callback,
  3069. final JDialog diag) {
  3070. final Collection<BibtexEntry> entries = new ArrayList<BibtexEntry>();
  3071. entries.add(entry);
  3072. return autoSetLinks(entries, null, null, singleTableModel, metaData, callback, diag);
  3073. }
  3074. /**
  3075. * Opens a file browser of the folder of the given file. If possible, the file is selected
  3076. * @param fileLink the location of the file
  3077. * @throws IOException
  3078. */
  3079. public static void openFolderAndSelectFile(String fileLink) throws IOException {
  3080. if (Globals.ON_WIN) {
  3081. openFolderAndSelectFileOnWindows(fileLink);
  3082. } else if (Globals.ON_LINUX){
  3083. openFolderAndSelectFileOnLinux(fileLink);
  3084. } else {
  3085. openFolderAndSelectFileGeneric(fileLink);
  3086. }
  3087. }
  3088. private static void openFolderAndSelectFileOnLinux(String fileLink) throws IOException {
  3089. String desktopSession = System.getenv("DESKTOP_SESSION").toLowerCase();
  3090. String cmd = "";
  3091. if (desktopSession.contains("gnome")) {
  3092. cmd = "nautilus " + fileLink;
  3093. } else if (desktopSession.contains("kde")) {
  3094. cmd = "dolphin --select " + fileLink;
  3095. } else {
  3096. cmd = "xdg-open " + fileLink.substring(0, fileLink.lastIndexOf(File.separator));
  3097. }
  3098. Runtime.getRuntime().exec(cmd);
  3099. }
  3100. private static void openFolderAndSelectFileGeneric(String fileLink) throws IOException {
  3101. File f = new File(fileLink);
  3102. Desktop.getDesktop().open(f.getParentFile());
  3103. }
  3104. private static void openFolderAndSelectFileOnWindows(String link) throws IOException {
  3105. link = link.replace("&", "\"&\"");
  3106. String cmd = "explorer.exe /select,\"" + link + "\"";
  3107. Runtime.getRuntime().exec(cmd);
  3108. }
  3109. /**
  3110. * Returns the list of linked files. The files have the absolute filename
  3111. *
  3112. * @param bes list of BibTeX entries
  3113. * @param fileDirs list of directories to try for expansion
  3114. *
  3115. * @return list of files. May be empty
  3116. */
  3117. public static List<File> getListOfLinkedFiles(BibtexEntry[] bes, String[] fileDirs) {
  3118. ArrayList<File> res = new ArrayList<File>();
  3119. for (BibtexEntry entry : bes) {
  3120. FileListTableModel tm = new FileListTableModel();
  3121. tm.setContent(entry.getField("file"));
  3122. for (int i=0; i< tm.getRowCount(); i++) {
  3123. FileListEntry flEntry = tm.getEntry(i);
  3124. File f = Util.expandFilename(flEntry.getLink(), fileDirs);
  3125. if (f != null) {
  3126. res.add(f);
  3127. }
  3128. }
  3129. }
  3130. return res;
  3131. }
  3132. }