2 Copyright (C) 2003 Morten O. Alver
4 All programs in this directory and
5 subdirectories are published under the GNU General Public License as
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License, or (at
11 your option) any later version.
13 This program is distributed in the hope that it will be useful, but
14 WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 General Public License for more details.
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
23 Further information about the GNU GPL is available at:
24 http://www.gnu.org/copyleft/gpl.ja.html
27 // created by : Morten O. Alver 2003
29 // function : utility functions
33 // modified : - r.nagel 20.04.2006
34 // make the DateFormatter abstract and splitt the easyDate methode
35 // (now we cannot change the dateformat dynamicly, sorry)
36 package net.sf.jabref;
39 import java.awt.event.ActionEvent;
40 import java.awt.event.ActionListener;
43 import java.net.URISyntaxException;
44 import java.net.URLDecoder;
45 import java.nio.charset.Charset;
46 import java.nio.charset.CharsetEncoder;
47 import java.text.NumberFormat;
48 import java.text.SimpleDateFormat;
50 import java.util.List;
51 import java.util.regex.Matcher;
52 import java.util.regex.Pattern;
55 import javax.swing.undo.UndoableEdit;
57 import net.sf.jabref.export.layout.LayoutEntry;
58 import net.sf.jabref.export.layout.LayoutFormatter;
59 import net.sf.jabref.external.ExternalFileType;
60 import net.sf.jabref.external.ExternalFileTypeEntryEditor;
61 import net.sf.jabref.external.UnknownExternalFileType;
62 import net.sf.jabref.groups.AbstractGroup;
63 import net.sf.jabref.groups.KeywordGroup;
64 import net.sf.jabref.gui.AutoCompleter;
65 import net.sf.jabref.gui.FileListEntry;
66 import net.sf.jabref.gui.FileListEntryEditor;
67 import net.sf.jabref.gui.FileListTableModel;
68 import net.sf.jabref.imports.CiteSeerFetcher;
69 import net.sf.jabref.undo.NamedCompound;
70 import net.sf.jabref.undo.UndoableFieldChange;
72 import com.jgoodies.forms.builder.DefaultFormBuilder;
73 import com.jgoodies.forms.layout.FormLayout;
76 * Describe class <code>Util</code> here.
78 * @author <a href="mailto:"> </a>
84 * A static Object for date formatting. Please do not create the object
85 * here, because there are some references from the Globals class.....
88 private static SimpleDateFormat dateFormatter = null;
91 * Colors are defined here.
94 public static Color fieldsCol = new Color(180, 180, 200);
97 * Integer values for indicating result of duplicate check (for entries):
100 final static int TYPE_MISMATCH = -1, NOT_EQUAL = 0, EQUAL = 1, EMPTY_IN_ONE = 2,
103 final static NumberFormat idFormat;
106 idFormat = NumberFormat.getInstance();
107 idFormat.setMinimumIntegerDigits(8);
108 idFormat.setGroupingUsed(false);
111 public static void bool(boolean b) {
113 System.out.println("true");
115 System.out.println("false");
118 public static void pr(String s) {
119 System.out.println(s);
122 public static void pr_(String s) {
126 public static String nCase(String s) {
127 // Make first character of String uppercase, and the
130 return s.substring(0, 1).toUpperCase() + s.substring(1, s.length()).toLowerCase();
132 return s.toUpperCase();
136 public static String checkName(String s) {
137 // Append '.bib' to the string unless it ends with that.
138 if (s.length() < 4 || !s.substring(s.length() - 4).equalsIgnoreCase(".bib")) {
144 private static int idCounter = 0;
146 public synchronized static String createNeutralId() {
147 return idFormat.format(idCounter++);
151 * This method sets the location of a Dialog such that it is centered with
152 * regard to another window, but not outside the screen on the left and the
155 public static void placeDialog(java.awt.Dialog diag, java.awt.Container win) {
156 Dimension ds = diag.getSize(), df = win.getSize();
157 Point pf = win.getLocation();
158 diag.setLocation(new Point(Math.max(0, pf.x + (df.width - ds.width) / 2), Math.max(0, pf.y
159 + (df.height - ds.height) / 2)));
164 * This method translates a field or string from Bibtex notation, with
165 * possibly text contained in " " or { }, and string references,
166 * concatenated by '#' characters, into Bibkeeper notation, where string
167 * references are enclosed in a pair of '#' characters.
169 public static String parseField(String content) {
171 if (content.length() == 0)
174 String[] strings = content.split("#");
175 StringBuffer result = new StringBuffer();
176 for (int i = 0; i < strings.length; i++){
177 String s = strings[i].trim();
179 char c = s.charAt(0);
180 // String reference or not?
181 if (c == '{' || c == '"'){
182 result.append(shaveString(strings[i]));
184 // This part should normally be a string reference, but if it's
185 // a pure number, it is not.
186 String s2 = shaveString(s);
188 Integer.parseInt(s2);
189 // If there's no exception, it's a number.
191 } catch (NumberFormatException ex) {
192 // otherwise append with hashes...
193 result.append("#").append(s2).append("#");
198 return result.toString();
202 * Will return the publication date of the given bibtex entry in conformance
203 * to ISO 8601, i.e. either YYYY or YYYY-MM.
206 * @return will return the publication date of the entry or null if no year
209 public static String getPublicationDate(BibtexEntry entry) {
211 Object o = entry.getField("year");
215 String year = toFourDigitYear(o.toString());
217 o = entry.getField("month");
219 int month = Util.getMonthNumber(o.toString());
221 return year + "-" + (month + 1 < 10 ? "0" : "") + (month + 1);
227 public static String shaveString(String s) {
228 // returns the string, after shaving off whitespace at the beginning
229 // and end, and removing (at most) one pair of braces or " surrounding
234 int beg = 0, end = s.length();
235 // We start out assuming nothing will be removed.
236 boolean begok = false, endok = false;
238 if (beg < s.length()) {
240 if (Character.isWhitespace(ch))
250 ch = s.charAt(end - 1);
251 if (Character.isWhitespace(ch))
261 ch2 = s.charAt(end - 1);
262 if (((ch == '{') && (ch2 == '}')) || ((ch == '"') && (ch2 == '"'))) {
267 s = s.substring(beg, end);
272 * This method returns a String similar to the one passed in, except that it
273 * is molded into a form that is acceptable for bibtex.
275 * Watch-out that the returned string might be of length 0 afterwards.
280 public static String checkLegalKey(String key) {
283 StringBuffer newKey = new StringBuffer();
284 for (int i = 0; i < key.length(); i++) {
285 char c = key.charAt(i);
286 if (!Character.isWhitespace(c) && (c != '#') && (c != '{') && (c != '\\') && (c != '"')
287 && (c != '}') && (c != '~') && (c != ',') && (c != '^'))
291 // Replace non-english characters like umlauts etc. with a sensible
292 // letter or letter combination that bibtex can accept.
293 String newKeyS = replaceSpecialCharacters(newKey.toString());
299 * Replace non-english characters like umlauts etc. with a sensible letter
300 * or letter combination that bibtex can accept. The basis for replacement
301 * is the HashMap GLobals.UNICODE_CHARS.
303 public static String replaceSpecialCharacters(String s) {
304 for (Iterator i = Globals.UNICODE_CHARS.keySet().iterator(); i.hasNext();) {
305 String chr = (String) i.next(), replacer = (String) Globals.UNICODE_CHARS.get(chr);
306 // pr(chr+" "+replacer);
307 s = s.replaceAll(chr, replacer);
312 static public String _wrap2(String in, int wrapAmount) {
313 // The following line cuts out all whitespace and replaces them with
316 // in = in.replaceAll("[ ]+"," ").replaceAll("[\\t]+"," ");
317 // StringBuffer out = new StringBuffer(in);
318 StringBuffer out = new StringBuffer(in.replaceAll("[ \\t\\r]+", " "));
320 int p = in.length() - wrapAmount;
321 int lastInserted = -1;
323 p = out.lastIndexOf(" ", p);
324 if (p <= 0 || p <= 20)
326 int lbreak = out.indexOf("\n", p);
327 System.out.println(lbreak + " " + lastInserted);
328 if ((lbreak > p) && ((lastInserted >= 0) && (lbreak < lastInserted))) {
329 p = lbreak - wrapAmount;
331 out.insert(p, "\n\t");
336 return out.toString();
339 static public String wrap2(String in, int wrapAmount) {
340 return net.sf.jabref.imports.FieldContentParser.wrap(in, wrapAmount);
343 static public String __wrap2(String in, int wrapAmount) {
344 // The following line cuts out all whitespace except line breaks, and
346 // with single spaces. Line breaks are padded with a tab character:
347 StringBuffer out = new StringBuffer(in.replaceAll("[ \\t\\r]+", " "));
350 // int lastInserted = -1;
351 while (p < out.length()) {
352 int q = out.indexOf(" ", p + wrapAmount);
353 if ((q < 0) || (q >= out.length()))
355 int lbreak = out.indexOf("\n", p);
356 // System.out.println(lbreak);
357 if ((lbreak > p) && (lbreak < q)) {
359 int piv = lbreak + 1;
360 if ((out.length() > piv) && !(out.charAt(piv) == '\t'))
361 out.insert(piv, "\n\t");
364 // System.out.println(q+" "+out.length());
366 out.insert(q, "\n\t");
370 return out.toString();// .replaceAll("\n", "\n\t");
373 public static HashSet findDeliminatedWordsInField(BibtexDatabase db, String field,
374 String deliminator) {
375 HashSet res = new HashSet();
376 Iterator i = db.getKeySet().iterator();
377 while (i.hasNext()) {
378 BibtexEntry be = db.getEntryById(i.next().toString());
379 Object o = be.getField(field);
381 String fieldValue = o.toString().trim();
382 StringTokenizer tok = new StringTokenizer(fieldValue, deliminator);
383 while (tok.hasMoreTokens())
384 res.add(tok.nextToken().trim());
391 * Returns a HashMap containing all words used in the database in the given
392 * field type. Characters in
397 * a <code>BibtexDatabase</code> value
399 * a <code>String</code> value
401 * a <code>String</code> value
402 * @return a <code>HashSet</code> value
404 public static HashSet findAllWordsInField(BibtexDatabase db, String field, String remove) {
405 HashSet res = new HashSet();
407 Iterator i = db.getKeySet().iterator();
408 while (i.hasNext()) {
409 BibtexEntry be = db.getEntryById(i.next().toString());
410 Object o = be.getField(field);
412 tok = new StringTokenizer(o.toString(), remove, false);
413 while (tok.hasMoreTokens())
414 res.add(tok.nextToken());
421 * Takes a String array and returns a string with the array's elements
422 * delimited by a certain String.
425 * String array to convert.
427 * String to use as delimiter.
428 * @return Delimited String.
430 public static String stringArrayToDelimited(String[] strs, String delimiter) {
431 if ((strs == null) || (strs.length == 0))
433 if (strs.length == 1)
435 StringBuffer sb = new StringBuffer();
436 for (int i = 0; i < strs.length - 1; i++) {
438 sb.append(delimiter);
440 sb.append(strs[strs.length - 1]);
441 return sb.toString();
445 * Takes a delimited string, splits it and returns
448 * a <code>String</code> value
449 * @return a <code>String[]</code> value
451 public static String[] delimToStringArray(String names, String delimiter) {
454 return names.split(delimiter);
458 * Open a http/pdf/ps viewer for the given link string.
460 public static void openExternalViewer(MetaData metaData, String link, String fieldName)
463 if (fieldName.equals("ps") || fieldName.equals("pdf")) {
465 // Find the default directory for this field type:
466 String dir = metaData.getFileDirectory(fieldName);
468 File file = expandFilename(link, new String[] { dir, "." });
470 // Check that the file exists:
471 if ((file == null) || !file.exists()) {
472 throw new IOException(Globals.lang("File not found") + " (" + fieldName + "): '"
475 link = file.getCanonicalPath();
477 // Use the correct viewer even if pdf and ps are mixed up:
478 String[] split = file.getName().split("\\.");
479 if (split.length >= 2) {
480 if (split[split.length - 1].equalsIgnoreCase("pdf"))
482 else if (split[split.length - 1].equalsIgnoreCase("ps")
483 || (split.length >= 3 && split[split.length - 2].equalsIgnoreCase("ps")))
487 // Find the file type descriptor:
488 ExternalFileType type = Globals.prefs.getExternalFileTypeByExt(fieldName);
490 } else if (fieldName.equals("doi")) {
492 // Check to see if link field already contains a well formated URL
493 if (!link.startsWith("http://")) {
494 link = Globals.DOI_LOOKUP_PREFIX + link;
496 } else if (fieldName.equals("citeseerurl")) {
499 String canonicalLink = CiteSeerFetcher.generateCanonicalURL(link);
500 if (canonicalLink != null)
501 link = canonicalLink;
504 String cmdArray[] = new String[2];
505 if (fieldName.equals("url")) { // html
508 // First check if the url is enclosed in \\url{}. If so, remove
510 if (link.startsWith("\\url{") && link.endsWith("}"))
511 link = link.substring(5, link.length() - 1);
513 link = sanitizeUrl(link);
515 if (Globals.ON_MAC) {
516 String[] cmd = { "/usr/bin/open", "-a", Globals.prefs.get("htmlviewer"), link };
517 Process child = Runtime.getRuntime().exec(cmd);
518 } else if (Globals.ON_WIN) {
519 openFileOnWindows(link, false);
521 * cmdArray[0] = Globals.prefs.get("htmlviewer");
522 * cmdArray[1] = link; Process child =
523 * Runtime.getRuntime().exec( cmdArray[0] + " " +
527 cmdArray[0] = Globals.prefs.get("htmlviewer");
529 Process child = Runtime.getRuntime().exec(cmdArray);
532 } catch (IOException e) {
533 System.err.println("An error occured on the command: "
534 + Globals.prefs.get("htmlviewer") + " " + link);
536 } else if (fieldName.equals("ps")) {
538 if (Globals.ON_MAC) {
539 ExternalFileType type = Globals.prefs.getExternalFileTypeByExt("ps");
540 String viewer = type != null ? type.getOpenWith() : Globals.prefs.get("psviewer");
541 String[] cmd = { "/usr/bin/open", "-a", viewer, link };
542 Process child = Runtime.getRuntime().exec(cmd);
543 } else if (Globals.ON_WIN) {
544 openFileOnWindows(link, true);
546 * cmdArray[0] = Globals.prefs.get("psviewer"); cmdArray[1] =
547 * link; Process child = Runtime.getRuntime().exec(
548 * cmdArray[0] + " " + cmdArray[1]);
551 ExternalFileType type = Globals.prefs.getExternalFileTypeByExt("ps");
552 String viewer = type != null ? type.getOpenWith() : Globals.prefs.get("psviewer");
553 cmdArray[0] = viewer;
555 Process child = Runtime.getRuntime().exec(cmdArray);
557 } catch (IOException e) {
558 System.err.println("An error occured on the command: "
559 + Globals.prefs.get("psviewer") + " " + link);
561 } else if (fieldName.equals("pdf")) {
563 if (Globals.ON_MAC) {
564 ExternalFileType type = Globals.prefs.getExternalFileTypeByExt("pdf");
565 String viewer = type != null ? type.getOpenWith() : Globals.prefs.get("psviewer");
566 String[] cmd = { "/usr/bin/open", "-a", viewer, link };
567 Process child = Runtime.getRuntime().exec(cmd);
568 } else if (Globals.ON_WIN) {
569 openFileOnWindows(link, true);
571 * String[] spl = link.split("\\\\"); StringBuffer sb = new
572 * StringBuffer(); for (int i = 0; i < spl.length; i++) { if
573 * (i > 0) sb.append("\\"); if (spl[i].indexOf(" ") >= 0)
574 * spl[i] = "\"" + spl[i] + "\""; sb.append(spl[i]); }
575 * //pr(sb.toString()); link = sb.toString();
577 * String cmd = "cmd.exe /c start " + link;
579 * Process child = Runtime.getRuntime().exec(cmd);
582 ExternalFileType type = Globals.prefs.getExternalFileTypeByExt("pdf");
583 String viewer = type != null ? type.getOpenWith() : Globals.prefs.get("psviewer");
584 cmdArray[0] = viewer;
586 // Process child = Runtime.getRuntime().exec(cmdArray[0]+"
588 Process child = Runtime.getRuntime().exec(cmdArray);
590 } catch (IOException e) {
592 System.err.println("An error occured on the command: "
593 + Globals.prefs.get("pdfviewer") + " #" + link);
594 System.err.println(e.getMessage());
598 .println("Message: currently only PDF, PS and HTML files can be opened by double clicking");
604 * Opens a file on a Windows system, using its default viewer.
609 * true if it is a local file, not an URL.
610 * @throws IOException
612 public static void openFileOnWindows(String link, boolean localFile) throws IOException {
614 * if (localFile) { String[] spl = link.split("\\\\"); StringBuffer sb =
615 * new StringBuffer(); for (int i = 0; i < spl.length; i++) { if (i > 0)
616 * sb.append("\\"); if (spl[i].indexOf(" ") >= 0) spl[i] = "\"" + spl[i] +
617 * "\""; sb.append(spl[i]); } link = sb.toString(); }
619 link = link.replaceAll("&", "\"&\"").replaceAll(" ", "\" \"");
622 // http://sourceforge.net/tracker/index.php?func=detail&aid=1489454&group_id=92314&atid=600306
624 if (Globals.osName.startsWith("Windows 9")) {
625 cmd = "command.com /c start " + link;
627 cmd = "cmd.exe /c start " + link;
630 Runtime.getRuntime().exec(cmd);
634 * Opens a file on a Windows system, using the given application.
636 * @param link The file name.
637 * @param application Link to the app that opens the file.
638 * @throws IOException
640 public static void openFileWithApplicationOnWindows(String link, String application)
643 link = link.replaceAll("&", "\"&\"").replaceAll(" ", "\" \"");
645 Runtime.getRuntime().exec(application + " " + link);
649 * Open an external file, attempting to use the correct viewer for it.
652 * The MetaData for the database this file belongs to.
656 public static void openExternalFileAnyFormat(MetaData metaData, String link,
657 ExternalFileType fileType) throws IOException {
660 boolean httpLink = link.toLowerCase().startsWith("http");
662 // For other platforms we'll try to find the file type:
663 File file = new File(link);
665 // We try to check the extension for the file:
666 String name = file.getName();
667 int pos = name.lastIndexOf('.');
668 String extension = ((pos >= 0) && (pos < name.length() - 1)) ? name.substring(pos + 1)
669 .trim().toLowerCase() : null;
672 * if ((extension == null) || (extension.length() == 0)) { // No
673 * extension. What to do? throw new IOException(Globals.lang("No file
674 * extension. Could not find viewer for file.")); }
677 // Find the default directory for this field type, if any:
678 String dir = metaData.getFileDirectory(extension);
679 // Include the standard "file" directory:
680 String fileDir = metaData.getFileDirectory(GUIGlobals.FILE_FIELD);
682 // Include the directory of the bib file:
684 if (metaData.getFile() != null) {
685 String databaseDir = metaData.getFile().getParent();
686 dirs = new String[] { dir, fileDir, databaseDir };
689 dirs = new String[] { dir, fileDir };
692 File tmp = expandFilename(link, dirs);
697 // Check if we have arrived at a file type, and either an http link or an existing file:
698 if ((httpLink || file.exists()) && (fileType != null)) {
701 String filePath = httpLink ? link : file.getPath();
702 if (Globals.ON_MAC) {
703 String[] cmd = { "/usr/bin/open", "-a", fileType.getOpenWith(), filePath };
704 Runtime.getRuntime().exec(cmd);
705 } else if (Globals.ON_WIN) {
706 if ((fileType.getOpenWith() != null) && (fileType.getOpenWith().length() > 0)) {
707 // Application is specified. Use it:
708 openFileWithApplicationOnWindows(filePath, fileType.getOpenWith());
710 openFileOnWindows(filePath, true);
712 String[] cmdArray = new String[] { fileType.getOpenWith(), filePath };
713 Runtime.getRuntime().exec(cmdArray);
715 } catch (IOException e) {
717 /*e.printStackTrace();
718 System.err.println("An error occured on the command: " + fileType.getOpenWith()
720 System.err.println(e.getMessage());*/
724 // No file matched the name, or we didn't know the file type.
725 // Perhaps it is an URL thing.
727 // First check if it is enclosed in \\url{}. If so, remove
729 if (link.startsWith("\\url{") && link.endsWith("}"))
730 link = link.substring(5, link.length() - 1);
732 if (link.startsWith("doi:"))
733 link = Globals.DOI_LOOKUP_PREFIX + link;
735 link = sanitizeUrl(link);
737 if (Globals.ON_MAC) {
738 String[] cmd = { "/usr/bin/open", "-a", Globals.prefs.get("htmlviewer"), link };
739 Runtime.getRuntime().exec(cmd);
740 } else if (Globals.ON_WIN) {
741 openFileOnWindows(link, false);
743 String[] cmdArray = new String[] { Globals.prefs.get("htmlviewer"), link };
744 Runtime.getRuntime().exec(cmdArray);
750 public static void openExternalFileUnknown(JabRefFrame frame, BibtexEntry entry, MetaData metaData,
751 String link, UnknownExternalFileType fileType) throws IOException {
753 String cancelMessage = Globals.lang("Unable to open file.");
754 String[] options = new String[] {Globals.lang("Define '%0'", fileType.getName()),
755 Globals.lang("Change file type"), Globals.lang("Cancel")};
756 String defOption = options[0];
757 int answer = JOptionPane.showOptionDialog(frame, Globals.lang("This external link is of the type '%0', which is undefined. What do you want to do?",
759 Globals.lang("Undefined file type"), JOptionPane.YES_NO_CANCEL_OPTION,
760 JOptionPane.QUESTION_MESSAGE, null, options, defOption);
761 if (answer == JOptionPane.CANCEL_OPTION) {
762 frame.output(cancelMessage);
765 else if (answer == JOptionPane.YES_OPTION) {
766 // User wants to define the new file type. Show the dialog:
767 ExternalFileType newType = new ExternalFileType(fileType.getName(), "", "", "new");
768 ExternalFileTypeEntryEditor editor = new ExternalFileTypeEntryEditor(frame, newType);
769 editor.setVisible(true);
770 if (editor.okPressed()) {
771 // Get the old list of types, add this one, and update the list in prefs:
772 List<ExternalFileType> fileTypes = new ArrayList<ExternalFileType>();
773 ExternalFileType[] oldTypes = Globals.prefs.getExternalFileTypeSelection();
774 for (int i = 0; i < oldTypes.length; i++) {
775 fileTypes.add(oldTypes[i]);
777 fileTypes.add(newType);
778 Collections.sort(fileTypes);
779 Globals.prefs.setExternalFileTypes(fileTypes);
780 // Finally, open the file:
781 openExternalFileAnyFormat(metaData, link, newType);
784 frame.output(cancelMessage);
789 // User wants to change the type of this link.
790 // First get a model of all file links for this entry:
791 FileListTableModel tModel = new FileListTableModel();
792 String oldValue = (String)entry.getField(GUIGlobals.FILE_FIELD);
793 tModel.setContent(oldValue);
794 FileListEntry flEntry = null;
795 // Then find which one we are looking at:
796 for (int i=0; i<tModel.getRowCount(); i++) {
797 FileListEntry iEntry = tModel.getEntry(i);
798 if (iEntry.getLink().equals(link)) {
803 if (flEntry == null) {
804 // This shouldn't happen, so I'm not sure what to put in here:
805 throw new RuntimeException("Could not find the file list entry "+link+" in "+entry.toString());
808 FileListEntryEditor editor = new FileListEntryEditor(frame, flEntry, false, metaData);
809 editor.setVisible(true);
810 if (editor.okPressed()) {
811 // Store the changes and add an undo edit:
812 String newValue = tModel.getStringRepresentation();
813 UndoableFieldChange ce = new UndoableFieldChange(entry, GUIGlobals.FILE_FIELD,
815 entry.setField(GUIGlobals.FILE_FIELD, newValue);
816 frame.basePanel().undoManager.addEdit(ce);
817 frame.basePanel().markBaseChanged();
818 // Finally, open the link:
819 openExternalFileAnyFormat(metaData, flEntry.getLink(), flEntry.getType());
822 frame.output(cancelMessage);
828 * Make sure an URL is "portable", in that it doesn't contain bad characters
829 * that break the open command in some OSes.
831 * Old Version can be found in CVS version 114 of Util.java.
834 * The URL to sanitize.
835 * @return Sanitized URL
837 public static String sanitizeUrl(String link) {
839 link = link.replaceAll("\\+", "%2B");
842 link = URLDecoder.decode(link, "UTF-8");
843 } catch (UnsupportedEncodingException e) {
847 * Fix for: [ 1574773 ] sanitizeUrl() breaks ftp:// and file:///
849 * http://sourceforge.net/tracker/index.php?func=detail&aid=1574773&group_id=92314&atid=600306
852 return new URI(null, link, null).toASCIIString();
853 } catch (URISyntaxException e) {
859 * Searches the given directory and subdirectories for a pdf file with name
862 public static String findPdf(String key, String extension, String directory, OpenFileFilter off) {
863 // String filename = key + "."+extension;
866 * Simon Fischer's patch for replacing a regexp in keys before
867 * converting to filename:
869 * String regex = Globals.prefs.get("basenamePatternRegex"); if ((regex !=
870 * null) && (regex.trim().length() > 0)) { String replacement =
871 * Globals.prefs.get("basenamePatternReplacement"); key =
872 * key.replaceAll(regex, replacement); }
874 if (!directory.endsWith(System.getProperty("file.separator")))
875 directory += System.getProperty("file.separator");
876 String found = findInDir(key, directory, off);
878 return found.substring(directory.length());
883 public static Map<BibtexEntry, List<File>> findAssociatedFiles(Collection<BibtexEntry> entries, Collection<String> extensions, Collection<File> directories){
884 HashMap<BibtexEntry, List<File>> result = new HashMap<BibtexEntry, List<File>>();
886 // First scan directories
887 Set<File> filesWithExtension = findFiles(extensions, directories);
889 // Initialize Result-Set
890 for (BibtexEntry entry : entries){
891 result.put(entry, new ArrayList<File>());
896 for (File file : filesWithExtension){
898 String name = file.getName();
899 for (BibtexEntry entry : entries){
900 if (name.contains(entry.getCiteKey())){
901 result.get(entry).add(file);
910 public static Set<File> findFiles(Collection<String> extensions, Collection<File> directories) {
911 Set<File> result = new HashSet<File>();
913 for (File directory : directories){
914 result.addAll(findFiles(extensions, directory));
920 private static Collection<? extends File> findFiles(Collection<String> extensions, File directory) {
921 Set<File> result = new HashSet<File>();
923 File[] children = directory.listFiles();
924 if (children == null)
925 return result; // No permission?
927 for (File child : children){
928 if (child.isDirectory()) {
929 result.addAll(findFiles(extensions, child));
932 String extension = getFileExtension(child);
934 if (extension != null){
935 if (extensions.contains(extension)){
946 * Returns the extension of a file or null if the file does not have one (no . in name).
950 * @return The extension, trimmed and in lowercase.
952 public static String getFileExtension(File file) {
953 String name = file.getName();
954 int pos = name.lastIndexOf('.');
955 String extension = ((pos >= 0) && (pos < name.length() - 1)) ? name.substring(pos + 1)
956 .trim().toLowerCase() : null;
961 * New version of findPdf that uses findFiles.
963 * The search pattern will be read from the preferences.
965 * The [extension]-tags in this pattern will be replace by the given
966 * extension parameter.
969 public static String findPdf(BibtexEntry entry, String extension, String directory) {
970 return findPdf(entry, extension, new String[] { directory });
974 * Convenience method for findPDF. Can search multiple PDF directories.
976 public static String findPdf(BibtexEntry entry, String extension, String[] directories) {
978 String regularExpression;
979 if (Globals.prefs.getBoolean(JabRefPreferences.USE_REG_EXP_SEARCH_KEY)) {
980 regularExpression = Globals.prefs.get(JabRefPreferences.REG_EXP_SEARCH_EXPRESSION_KEY);
982 regularExpression = Globals.prefs
983 .get(JabRefPreferences.DEFAULT_REG_EXP_SEARCH_EXPRESSION_KEY);
985 regularExpression = regularExpression.replaceAll("\\[extension\\]", extension);
987 return findFile(entry, null, directories, regularExpression, true);
991 * Convenience menthod for findPDF. Searches for a file of the given type.
992 * @param entry The BibtexEntry to search for a link for.
993 * @param fileType The file type to search for.
994 * @return The link to the file found, or null if not found.
996 public static String findFile(BibtexEntry entry, ExternalFileType fileType, List extraDirs) {
998 List dirs = new ArrayList();
999 dirs.addAll(extraDirs);
1000 if (Globals.prefs.hasKey(fileType.getExtension()+"Directory")) {
1001 dirs.add(Globals.prefs.get(fileType.getExtension()+"Directory"));
1003 String [] directories = (String[])dirs.toArray(new String[dirs.size()]);
1004 return findPdf(entry, fileType.getExtension(), directories);
1008 * Searches the given directory and file name pattern for a file for the
1013 * http://sourceforge.net/tracker/index.php?func=detail&aid=1503410&group_id=92314&atid=600309
1016 * - Be able to find the associated PDF in a set of given directories.
1017 * - Be able to return a relative path or absolute path.
1019 * - Allow for flexible naming schemes in the PDFs.
1021 * Syntax scheme for file:
1023 * <li>* Any subDir</li>
1024 * <li>** Any subDir (recursiv)</li>
1025 * <li>[key] Key from bibtex file and database</li>
1026 * <li>.* Anything else is taken to be a Regular expression.</li>
1034 * A set of root directories to start the search from. Paths are
1035 * returned relative to these directories if relative is set to
1036 * true. These directories will not be expanded or anything. Use
1037 * the file attribute for this.
1042 * whether to return relative file paths or absolute ones
1044 * @return Will return the first file found to match the given criteria or
1045 * null if none was found.
1047 public static String findFile(BibtexEntry entry, BibtexDatabase database, String[] directory,
1048 String file, boolean relative) {
1050 for (int i = 0; i < directory.length; i++) {
1051 String result = findFile(entry, database, directory[i], file, relative);
1052 if (result != null) {
1060 * Removes optional square brackets from the string s
1065 public static String stripBrackets(String s) {
1066 int beginIndex = (s.startsWith("[") ? 1 : 0);
1067 int endIndex = (s.endsWith("]") ? s.length() - 1 : s.length());
1068 return s.substring(beginIndex, endIndex);
1071 public static ArrayList<String[]> parseMethodsCalls(String calls) throws RuntimeException {
1073 ArrayList<String[]> result = new ArrayList<String[]>();
1075 char[] c = calls.toCharArray();
1079 while (i < c.length) {
1082 if (Character.isJavaIdentifierStart(c[i])) {
1084 while (i < c.length && (Character.isJavaIdentifierPart(c[i]) || c[i] == '.')) {
1087 if (i < c.length && c[i] == '(') {
1089 String method = calls.substring(start, i);
1095 // Parameter is in format "xxx"
1102 boolean escaped = false;
1103 while (i + 1 < c.length &&
1104 !(!escaped && c[i] == '"' && c[i + 1] == ')')) {
1114 String param = calls.substring(startParam, i);
1115 result.add(new String[] { method, param });
1117 // Parameter is in format xxx
1121 while (i < c.length && c[i] != ')') {
1125 String param = calls.substring(startParam, i);
1127 result.add(new String[] { method, param });
1132 // Incorrecly terminated open brace
1133 result.add(new String[] { method });
1136 String method = calls.substring(start, i);
1137 result.add(new String[] { method });
1147 * Accepts a string like [author:toLowerCase("escapedstring"),toUpperCase],
1148 * whereas the first string signifies the bibtex-field to get while the
1149 * others are the names of layouters that will be applied.
1151 * @param fieldAndFormat
1156 public static String getFieldAndFormat(String fieldAndFormat, BibtexEntry entry,
1157 BibtexDatabase database) {
1159 fieldAndFormat = stripBrackets(fieldAndFormat);
1161 int colon = fieldAndFormat.indexOf(':');
1163 String beforeColon, afterColon;
1165 beforeColon = fieldAndFormat;
1168 beforeColon = fieldAndFormat.substring(0, colon);
1169 afterColon = fieldAndFormat.substring(colon + 1);
1171 beforeColon = beforeColon.trim();
1173 if (beforeColon.length() == 0) {
1177 String fieldValue = BibtexDatabase.getResolvedField(beforeColon, entry, database);
1179 if (fieldValue == null)
1182 if (afterColon == null || afterColon.length() == 0)
1186 LayoutFormatter[] formatters = LayoutEntry.getOptionalLayout(afterColon, "");
1187 for (int i = 0; i < formatters.length; i++) {
1188 fieldValue = formatters[i].format(fieldValue);
1190 } catch (Exception e) {
1191 throw new RuntimeException(e);
1198 * Convenience function for absolute search.
1200 * Uses findFile(BibtexEntry, BibtexDatabase, (String)null, String, false).
1202 public static String findFile(BibtexEntry entry, BibtexDatabase database, String file) {
1203 return findFile(entry, database, (String) null, file, false);
1207 * Internal Version of findFile, which also accepts a current directory to
1208 * base the search on.
1211 public static String findFile(BibtexEntry entry, BibtexDatabase database, String directory,
1212 String file, boolean relative) {
1215 if (directory == null) {
1216 root = new File(".");
1218 root = new File(directory);
1223 String found = findFile(entry, database, root, file);
1225 if (directory == null || !relative) {
1229 if (found != null) {
1232 * [ 1601651 ] PDF subdirectory - missing first character
1234 * http://sourceforge.net/tracker/index.php?func=detail&aid=1601651&group_id=92314&atid=600306
1236 // Changed by M. Alver 2007.01.04:
1237 // Remove first character if it is a directory separator character:
1238 String tmp = found.substring(root.getCanonicalPath().length());
1239 if ((tmp.length() > 1) && (tmp.charAt(0) == File.separatorChar))
1240 tmp = tmp.substring(1);
1242 //return found.substring(root.getCanonicalPath().length());
1243 } catch (IOException e) {
1251 * The actual work-horse. Will find absolute filepaths starting from the
1252 * given directory using the given regular expression string for search.
1254 protected static String findFile(BibtexEntry entry, BibtexDatabase database, File directory,
1257 if (file.startsWith("/")) {
1258 directory = new File(".");
1259 file = file.substring(1);
1262 // Escape handling...
1263 Matcher m = Pattern.compile("([^\\\\])\\\\([^\\\\])").matcher(file);
1264 StringBuffer s = new StringBuffer();
1266 m.appendReplacement(s, m.group(1) + "/" + m.group(2));
1269 file = s.toString();
1270 String[] fileParts = file.split("/");
1272 if (fileParts.length == 0)
1275 if (fileParts.length > 1) {
1277 for (int i = 0; i < fileParts.length - 1; i++) {
1279 String dirToProcess = fileParts[i];
1281 dirToProcess = expandBrackets(dirToProcess, entry, database);
1283 if (dirToProcess.matches("^.:$")) { // Windows Drive Letter
1284 directory = new File(dirToProcess + "/");
1287 if (dirToProcess.equals(".")) { // Stay in current directory
1290 if (dirToProcess.equals("..")) {
1291 directory = new File(directory.getParent());
1294 if (dirToProcess.equals("*")) { // Do for all direct subdirs
1296 File[] subDirs = directory.listFiles();
1297 if (subDirs == null)
1298 return null; // No permission?
1300 String restOfFileString = join(fileParts, "/", i + 1, fileParts.length);
1302 for (int sub = 0; sub < subDirs.length; sub++) {
1303 if (subDirs[sub].isDirectory()) {
1304 String result = findFile(entry, database, subDirs[sub],
1312 // Do for all direct and indirect subdirs
1313 if (dirToProcess.equals("**")) {
1314 List toDo = new LinkedList();
1315 toDo.add(directory);
1317 String restOfFileString = join(fileParts, "/", i + 1, fileParts.length);
1319 // Before checking the subdirs, we first check the current
1321 String result = findFile(entry, database, directory, restOfFileString);
1325 while (!toDo.isEmpty()) {
1327 // Get all subdirs of each of the elements found in toDo
1328 File[] subDirs = ((File) toDo.remove(0)).listFiles();
1329 if (subDirs == null) // No permission?
1332 toDo.addAll(Arrays.asList(subDirs));
1334 for (int sub = 0; sub < subDirs.length; sub++) {
1335 if (!subDirs[sub].isDirectory())
1337 result = findFile(entry, database, subDirs[sub], restOfFileString);
1342 // We already did the currentDirectory
1346 final Pattern toMatch = Pattern
1347 .compile(dirToProcess.replaceAll("\\\\\\\\", "\\\\"));
1349 File[] matches = directory.listFiles(new FilenameFilter() {
1350 public boolean accept(File arg0, String arg1) {
1351 return toMatch.matcher(arg1).matches();
1354 if (matches == null || matches.length == 0)
1357 directory = matches[0];
1359 if (!directory.exists())
1362 } // End process directory information
1364 // Last step check if the given file can be found in this directory
1365 String filenameToLookFor = expandBrackets(fileParts[fileParts.length - 1], entry, database);
1367 final Pattern toMatch = Pattern.compile("^"
1368 + filenameToLookFor.replaceAll("\\\\\\\\", "\\\\") + "$");
1370 File[] matches = directory.listFiles(new FilenameFilter() {
1371 public boolean accept(File arg0, String arg1) {
1372 return toMatch.matcher(arg1).matches();
1375 if (matches == null || matches.length == 0)
1379 return matches[0].getCanonicalPath();
1380 } catch (IOException e) {
1385 static Pattern squareBracketsPattern = Pattern.compile("\\[.*?\\]");
1388 * Takes a string that contains bracketed expression and expands each of
1389 * these using getFieldAndFormat.
1391 * Unknown Bracket expressions are silently dropped.
1393 * @param bracketString
1398 public static String expandBrackets(String bracketString, BibtexEntry entry,
1399 BibtexDatabase database) {
1400 Matcher m = squareBracketsPattern.matcher(bracketString);
1401 StringBuffer s = new StringBuffer();
1403 String replacement = getFieldAndFormat(m.group(), entry, database);
1404 if (replacement == null)
1406 m.appendReplacement(s, replacement);
1410 return s.toString();
1414 * Concatenate all strings in the array from index 'from' to 'to' (excluding
1415 * to) with the given separator.
1419 * String[] s = "ab/cd/ed".split("/"); join(s, "\\", 0, s.length) ->
1426 * Excluding strings[to]
1429 public static String join(String[] strings, String separator, int from, int to) {
1430 if (strings.length == 0 || from >= to)
1433 StringBuffer sb = new StringBuffer();
1434 for (int i = from; i < to - 1; i++) {
1435 sb.append(strings[i]).append(separator);
1437 return sb.append(strings[to - 1]).toString();
1441 * Converts a relative filename to an absolute one, if necessary. Returns
1442 * null if the file does not exist.
1444 * Will look in each of the given dirs starting from the beginning and
1445 * returning the first found file to match if any.
1447 public static File expandFilename(String name, String[] dir) {
1449 for (int i = 0; i < dir.length; i++) {
1450 if (dir[i] != null) {
1451 File result = expandFilename(name, dir[i]);
1452 if (result != null) {
1462 * Converts a relative filename to an absolute one, if necessary. Returns
1463 * null if the file does not exist.
1465 public static File expandFilename(String name, String dir) {
1466 // System.out.println("expandFilename: name="+name+"\t dir="+dir);
1468 if (name == null || name.length() == 0)
1471 file = new File(name);
1475 if (!file.exists() && (dir != null)) {
1476 if (dir.endsWith(System.getProperty("file.separator")))
1479 name = dir + System.getProperty("file.separator") + name;
1481 // System.out.println("expanded to: "+name);
1482 // if (name.startsWith("ftp"))
1484 file = new File(name);
1488 // Ok, try to fix / and \ problems:
1489 if (Globals.ON_WIN) {
1490 // workaround for catching Java bug in regexp replacer
1491 // and, why, why, why ... I don't get it - wegner 2006/01/22
1493 name = name.replaceAll("/", "\\\\");
1494 } catch (java.lang.StringIndexOutOfBoundsException exc) {
1495 System.err.println("An internal Java error was caused by the entry " + "\""
1499 name = name.replaceAll("\\\\", "/");
1500 // System.out.println("expandFilename: "+name);
1501 file = new File(name);
1509 private static String findInDir(String key, String dir, OpenFileFilter off) {
1510 File f = new File(dir);
1511 File[] all = f.listFiles();
1513 return null; // An error occured. We may not have
1514 // permission to list the files.
1516 int numFiles = all.length;
1518 for (int i = 0; i < numFiles; i++) {
1519 File curFile = all[i];
1521 if (curFile.isFile()) {
1522 String name = curFile.getName();
1523 if (name.startsWith(key + ".") && off.accept(name))
1524 return curFile.getPath();
1526 } else if (curFile.isDirectory()) {
1527 String found = findInDir(key, curFile.getPath(), off);
1536 * Checks if the two entries represent the same publication.
1544 public static boolean isDuplicate(BibtexEntry one, BibtexEntry two, float threshold) {
1546 // First check if they are of the same type - a necessary condition:
1547 if (one.getType() != two.getType())
1550 // The check if they have the same required fields:
1551 String[] fields = one.getType().getRequiredFields();
1557 req = compareFieldSet(fields, one, two);
1558 fields = one.getType().getOptionalFields();
1560 if (fields != null) {
1561 float opt = compareFieldSet(fields, one, two);
1562 return (2 * req + opt) / 3 >= threshold;
1564 return (req >= threshold);
1569 * Goes through all entries in the given database, and if at least one of
1570 * them is a duplicate of the given entry, as per
1571 * Util.isDuplicate(BibtexEntry, BibtexEntry), the duplicate is returned.
1572 * The search is terminated when the first duplicate is found.
1575 * The database to search.
1577 * The entry of which we are looking for duplicates.
1578 * @return The first duplicate entry found. null if no duplicates are found.
1580 public static BibtexEntry containsDuplicate(BibtexDatabase database, BibtexEntry entry) {
1581 Collection entries = database.getEntries();
1582 for (Iterator i = entries.iterator(); i.hasNext();) {
1583 BibtexEntry other = (BibtexEntry) i.next();
1584 if (isDuplicate(entry, other, Globals.duplicateThreshold))
1585 return other; // Duplicate found.
1587 return null; // No duplicate found.
1590 private static float compareFieldSet(String[] fields, BibtexEntry one, BibtexEntry two) {
1592 for (int i = 0; i < fields.length; i++) {
1593 // Util.pr(":"+compareSingleField(fields[i], one, two));
1594 if (compareSingleField(fields[i], one, two) == EQUAL) {
1596 // Util.pr(fields[i]);
1599 return ((float) res) / ((float) fields.length);
1602 private static int compareSingleField(String field, BibtexEntry one, BibtexEntry two) {
1603 String s1 = (String) one.getField(field), s2 = (String) two.getField(field);
1608 return EMPTY_IN_ONE;
1609 } else if (s2 == null)
1610 return EMPTY_IN_TWO;
1611 s1 = s1.toLowerCase();
1612 s2 = s2.toLowerCase();
1613 // Util.pr(field+": '"+s1+"' vs '"+s2+"'");
1614 if (field.equals("author") || field.equals("editor")) {
1615 // Specific for name fields.
1617 String[] aus1 = AuthorList.fixAuthor_lastNameFirst(s1).split(" and "), aus2 = AuthorList
1618 .fixAuthor_lastNameFirst(s2).split(" and "), au1 = aus1[0].split(","), au2 = aus2[0]
1621 // Can check number of authors, all authors or only the first.
1622 if ((aus1.length > 0) && (aus1.length == aus2.length)
1623 && au1[0].trim().equals(au2[0].trim()))
1628 if (s1.trim().equals(s2.trim()))
1636 public static double compareEntriesStrictly(BibtexEntry one, BibtexEntry two) {
1637 HashSet allFields = new HashSet();// one.getAllFields());
1638 Object[] o = one.getAllFields();
1639 for (int i = 0; i < o.length; i++)
1640 allFields.add(o[i]);
1641 o = two.getAllFields();
1642 for (int i = 0; i < o.length; i++)
1643 allFields.add(o[i]);
1645 for (Iterator fld = allFields.iterator(); fld.hasNext();) {
1646 String field = (String) fld.next();
1647 Object en = one.getField(field), to = two.getField(field);
1648 if ((en != null) && (to != null) && (en.equals(to)))
1650 else if ((en == null) && (to == null))
1653 if (score == allFields.size())
1654 return 1.01; // Just to make sure we can
1655 // use score>1 without
1658 return ((double) score) / allFields.size();
1662 * This methods assures all words in the given entry are recorded in their
1663 * respective Completers, if any.
1665 public static void updateCompletersForEntry(HashMap autoCompleters,
1668 for (Iterator j = autoCompleters.keySet().iterator(); j.hasNext();) {
1669 String field = (String) j.next();
1670 AutoCompleter comp = (AutoCompleter) autoCompleters.get(field);
1671 comp.addAll(be.getField(field));
1677 * Sets empty or non-existing owner fields of bibtex entries inside a List
1678 * to a specified default value. Timestamp field is also set. Preferences
1679 * are checked to see if these options are enabled.
1682 * List of bibtex entries
1684 public static void setAutomaticFields(List bibs, boolean overwriteOwner,
1685 boolean overwriteTimestamp) {
1686 String defaultOwner = Globals.prefs.get("defaultOwner");
1687 String timestamp = easyDateFormat();
1688 boolean globalSetOwner = Globals.prefs.getBoolean("useOwner"),
1689 globalSetTimeStamp = Globals.prefs.getBoolean("useTimeStamp");
1690 String timeStampField = Globals.prefs.get("timeStampField");
1691 // Iterate through all entries
1692 for (int i = 0; i < bibs.size(); i++) {
1693 // Get current entry
1694 BibtexEntry curEntry = (BibtexEntry) bibs.get(i);
1695 boolean setOwner = globalSetOwner &&
1696 (overwriteOwner || (curEntry.getField(BibtexFields.OWNER)==null));
1697 boolean setTimeStamp = globalSetTimeStamp &&
1698 (overwriteTimestamp || (curEntry.getField(timeStampField)==null));
1699 setAutomaticFields(curEntry, setOwner, defaultOwner, setTimeStamp, timeStampField,
1707 * Sets empty or non-existing owner fields of a bibtex entry to a specified
1708 * default value. Timestamp field is also set. Preferences are checked to
1709 * see if these options are enabled.
1712 * The entry to set fields for.
1713 * @param overwriteOwner
1714 * Indicates whether owner should be set if it is already set.
1715 * @param overwriteTimestamp
1716 * Indicates whether timestamp should be set if it is already set.
1718 public static void setAutomaticFields(BibtexEntry entry, boolean overwriteOwner,
1719 boolean overwriteTimestamp) {
1720 String defaultOwner = Globals.prefs.get("defaultOwner");
1721 String timestamp = easyDateFormat();
1722 String timeStampField = Globals.prefs.get("timeStampField");
1723 boolean setOwner = Globals.prefs.getBoolean("useOwner") &&
1724 (overwriteOwner || (entry.getField(BibtexFields.OWNER)==null));
1725 boolean setTimeStamp = Globals.prefs.getBoolean("useTimeStamp") &&
1726 (overwriteTimestamp || (entry.getField(timeStampField)==null));
1728 setAutomaticFields(entry, setOwner, defaultOwner, setTimeStamp, timeStampField, timestamp);
1731 private static void setAutomaticFields(BibtexEntry entry, boolean setOwner, String owner,
1732 boolean setTimeStamp, String timeStampField, String timeStamp) {
1734 // Set owner field if this option is enabled:
1736 // No or empty owner field?
1737 // if (entry.getField(Globals.OWNER) == null
1738 // || ((String) entry.getField(Globals.OWNER)).length() == 0) {
1739 // Set owner field to default value
1740 entry.setField(BibtexFields.OWNER, owner);
1745 entry.setField(timeStampField, timeStamp);
1754 * File Destination file
1755 * @param deleteIfExists
1756 * boolean Determines whether the copy goes on even if the file
1758 * @throws IOException
1759 * @return boolean Whether the copy succeeded, or was stopped due to the
1760 * file already existing.
1762 public static boolean copyFile(File source, File dest, boolean deleteIfExists)
1763 throws IOException {
1765 BufferedInputStream in = null;
1766 BufferedOutputStream out = null;
1768 // Check if the file already exists.
1769 if (dest.exists()) {
1770 if (!deleteIfExists)
1772 // else dest.delete();
1775 in = new BufferedInputStream(new FileInputStream(source));
1776 out = new BufferedOutputStream(new FileOutputStream(dest));
1779 while ((el = in.read()) >= 0) {
1782 } catch (IOException ex) {
1796 * This method is called at startup, and makes necessary adaptations to
1797 * preferences for users from an earlier version of Jabref.
1799 public static void performCompatibilityUpdate() {
1801 // Make sure "abstract" is not in General fields, because
1802 // Jabref 1.55 moves the abstract to its own tab.
1803 String genFields = Globals.prefs.get("generalFields");
1804 // pr(genFields+"\t"+genFields.indexOf("abstract"));
1805 if (genFields.indexOf("abstract") >= 0) {
1806 // pr(genFields+"\t"+genFields.indexOf("abstract"));
1808 if (genFields.equals("abstract"))
1810 else if (genFields.indexOf(";abstract;") >= 0) {
1811 newGen = genFields.replaceAll(";abstract;", ";");
1812 } else if (genFields.indexOf("abstract;") == 0) {
1813 newGen = genFields.replaceAll("abstract;", "");
1814 } else if (genFields.indexOf(";abstract") == genFields.length() - 9) {
1815 newGen = genFields.replaceAll(";abstract", "");
1819 Globals.prefs.put("generalFields", newGen);
1825 * Collect file links from the given set of fields, and add them to the list contained
1826 * in the field GUIGlobals.FILE_FIELD.
1827 * @param database The database to modify.
1828 * @param fields The fields to find links in.
1829 * @return A CompoundEdit specifying the undo operation for the whole operation.
1831 public static NamedCompound upgradePdfPsToFile(BibtexDatabase database, String[] fields) {
1832 NamedCompound ce = new NamedCompound(Globals.lang("Move external links to 'file' field"));
1833 for (Iterator i = database.getEntryMap().keySet().iterator(); i.hasNext();) {
1834 BibtexEntry entry = (BibtexEntry) database.getEntryMap().get(i.next());
1835 FileListTableModel tableModel = new FileListTableModel();
1836 // If there are already links in the file field, keep those on top:
1837 Object oldFileContent = entry.getField(GUIGlobals.FILE_FIELD);
1838 if (oldFileContent != null) {
1839 tableModel.setContent((String) oldFileContent);
1841 int oldRowCount = tableModel.getRowCount();
1842 for (int j = 0; j < fields.length; j++) {
1843 Object o = entry.getField(fields[j]);
1845 String s = (String) o;
1846 if (s.trim().length() > 0) {
1847 File f = new File(s);
1848 String extension = "";
1849 if ((s.lastIndexOf('.') >= 0) && (s.lastIndexOf('.') < s.length() - 1)) {
1850 extension = s.substring(s.lastIndexOf('.') + 1);
1852 FileListEntry flEntry = new FileListEntry(f.getName(), s,
1853 Globals.prefs.getExternalFileTypeByExt(fields[j]));
1854 tableModel.addEntry(tableModel.getRowCount(), flEntry);
1856 entry.clearField(fields[j]);
1857 ce.addEdit(new UndoableFieldChange(entry, fields[j], o, null));
1861 if (tableModel.getRowCount() != oldRowCount) {
1862 String newValue = tableModel.getStringRepresentation();
1863 entry.setField(GUIGlobals.FILE_FIELD, newValue);
1864 ce.addEdit(new UndoableFieldChange(entry, GUIGlobals.FILE_FIELD, oldFileContent, newValue));
1871 // -------------------------------------------------------------------------------
1874 * extends the filename with a default Extension, if no Extension '.x' could
1877 public static String getCorrectFileName(String orgName, String defaultExtension) {
1878 if (orgName == null)
1881 String back = orgName;
1882 int t = orgName.indexOf(".", 1); // hidden files Linux/Unix (?)
1884 back = back + "." + defaultExtension;
1890 * Quotes each and every character, e.g. '!' as !. Used for verbatim
1891 * display of arbitrary strings that may contain HTML entities.
1893 public static String quoteForHTML(String s) {
1894 StringBuffer sb = new StringBuffer();
1895 for (int i = 0; i < s.length(); ++i) {
1896 sb.append("&#" + (int) s.charAt(i) + ";");
1898 return sb.toString();
1901 public static String quote(String s, String specials, char quoteChar) {
1902 return quote(s, specials, quoteChar, 0);
1906 * Quote special characters.
1909 * The String which may contain special characters.
1911 * A String containing all special characters except the quoting
1912 * character itself, which is automatically quoted.
1914 * The quoting character.
1916 * The number of characters after which a linebreak is inserted
1917 * (this linebreak is undone by unquote()). Set to 0 to disable.
1918 * @return A String with every special character (including the quoting
1919 * character itself) quoted.
1921 public static String quote(String s, String specials, char quoteChar, int linewrap) {
1922 StringBuffer sb = new StringBuffer();
1926 for (int i = 0; i < s.length(); ++i) {
1928 isSpecial = specials.indexOf(c) >= 0 || c == quoteChar;
1931 && (++linelength >= linewrap || (isSpecial && linelength >= linewrap - 1))) {
1932 sb.append(quoteChar);
1937 sb.append(quoteChar);
1942 return sb.toString();
1946 * Unquote special characters.
1949 * The String which may contain quoted special characters.
1951 * The quoting character.
1952 * @return A String with all quoted characters unquoted.
1954 public static String unquote(String s, char quoteChar) {
1955 StringBuffer sb = new StringBuffer();
1957 boolean quoted = false;
1958 for (int i = 0; i < s.length(); ++i) {
1960 if (quoted) { // append literally...
1961 if (c != '\n') // ...unless newline
1964 } else if (c != quoteChar) {
1966 } else { // quote char
1970 return sb.toString();
1974 * Quote all regular expression meta characters in s, in order to search for
1977 public static String quoteMeta(String s) {
1978 // work around a bug: trailing backslashes have to be quoted
1980 int i = s.length() - 1;
1981 StringBuffer bs = new StringBuffer("");
1982 while ((i >= 0) && (s.charAt(i) == '\\')) {
1986 s = s.substring(0, i + 1);
1987 return "\\Q" + s.replaceAll("\\\\E", "\\\\E\\\\\\\\E\\\\Q") + "\\E" + bs.toString();
1991 * This method "tidies" up e.g. a keyword string, by alphabetizing the words
1992 * and removing all duplicates.
1994 public static String sortWordsAndRemoveDuplicates(String text) {
1996 String[] words = text.split(", ");
1997 SortedSet set = new TreeSet();
1998 for (int i = 0; i < words.length; i++)
2000 StringBuffer sb = new StringBuffer();
2001 for (Iterator i = set.iterator(); i.hasNext();) {
2002 sb.append(i.next());
2005 if (sb.length() > 2)
2006 sb.delete(sb.length() - 2, sb.length());
2007 String result = sb.toString();
2008 return result.length() > 2 ? result : "";
2012 * Warns the user of undesired side effects of an explicit
2013 * assignment/removal of entries to/from this group. Currently there are
2014 * four types of groups: AllEntriesGroup, SearchGroup - do not support
2015 * explicit assignment. ExplicitGroup - never modifies entries. KeywordGroup -
2016 * only this modifies entries upon assignment/removal. Modifications are
2017 * acceptable unless they affect a standard field (such as "author") besides
2018 * the "keywords" field.
2021 * The Component used as a parent when displaying a confirmation
2023 * @return true if the assignment has no undesired side effects, or the user
2024 * chose to perform it anyway. false otherwise (this indicates that
2025 * the user has aborted the assignment).
2027 public static boolean warnAssignmentSideEffects(AbstractGroup[] groups, BibtexEntry[] entries,
2028 BibtexDatabase db, Component parent) {
2029 Vector affectedFields = new Vector();
2030 for (int k = 0; k < groups.length; ++k) {
2031 if (groups[k] instanceof KeywordGroup) {
2032 KeywordGroup kg = (KeywordGroup) groups[k];
2033 String field = kg.getSearchField().toLowerCase();
2034 if (field.equals("keywords"))
2035 continue; // this is not undesired
2036 for (int i = 0, len = BibtexFields.numberOfPublicFields(); i < len; ++i) {
2037 if (field.equals(BibtexFields.getFieldName(i))) {
2038 affectedFields.add(field);
2044 if (affectedFields.size() == 0)
2045 return true; // no side effects
2047 // show a warning, then return
2048 StringBuffer message = // JZTODO lyrics...
2049 new StringBuffer("This action will modify the following field(s)\n"
2050 + "in at least one entry each:\n");
2051 for (int i = 0; i < affectedFields.size(); ++i)
2052 message.append(affectedFields.elementAt(i)).append("\n");
2053 message.append("This could cause undesired changes to "
2054 + "your entries, so it is\nrecommended that you change the grouping field "
2055 + "in your group\ndefinition to \"keywords\" or a non-standard name."
2056 + "\n\nDo you still want to continue?");
2057 int choice = JOptionPane.showConfirmDialog(parent, message, Globals.lang("Warning"),
2058 JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
2059 return choice != JOptionPane.NO_OPTION;
2061 // if (groups instanceof KeywordGroup) {
2062 // KeywordGroup kg = (KeywordGroup) groups;
2063 // String field = kg.getSearchField().toLowerCase();
2064 // if (field.equals("keywords"))
2065 // return true; // this is not undesired
2066 // for (int i = 0; i < GUIGlobals.ALL_FIELDS.length; ++i) {
2067 // if (field.equals(GUIGlobals.ALL_FIELDS[i])) {
2068 // // show a warning, then return
2069 // String message = Globals // JZTODO lyrics...
2071 // "This action will modify the \"%0\" field "
2072 // + "of your entries.\nThis could cause undesired changes to "
2073 // + "your entries, so it is\nrecommended that you change the grouping
2075 // + "in your group\ndefinition to \"keywords\" or a non-standard name."
2076 // + "\n\nDo you still want to continue?",
2078 // int choice = JOptionPane.showConfirmDialog(parent, message,
2079 // Globals.lang("Warning"), JOptionPane.YES_NO_OPTION,
2080 // JOptionPane.WARNING_MESSAGE);
2081 // return choice != JOptionPane.NO_OPTION;
2085 // return true; // found no side effects
2088 // ========================================================
2089 // lot of abreviations in medline
2090 // PKC etc convert to {PKC} ...
2091 // ========================================================
2092 static Pattern titleCapitalPattern = Pattern.compile("[A-Z]+");
2095 * Wrap all uppercase letters, or sequences of uppercase letters, in curly
2096 * braces. Ignore letters within a pair of # character, as these are part of
2097 * a string label that should not be modified.
2100 * The string to modify.
2101 * @return The resulting string after wrapping capitals.
2103 public static String putBracesAroundCapitals(String s) {
2105 boolean inString = false, isBracing = false, escaped = false;
2107 StringBuffer buf = new StringBuffer();
2108 for (int i = 0; i < s.length(); i++) {
2109 // Update variables based on special characters:
2110 int c = s.charAt(i);
2115 else if (!escaped && (c == '#'))
2116 inString = !inString;
2118 // See if we should start bracing:
2119 if ((inBrace == 0) && !isBracing && !inString && Character.isLetter((char) c)
2120 && Character.isUpperCase((char) c)) {
2126 // See if we should close a brace set:
2127 if (isBracing && !(Character.isLetter((char) c) && Character.isUpperCase((char) c))) {
2133 // Add the current character:
2134 buf.append((char) c);
2136 // Check if we are entering an escape sequence:
2137 if ((c == '\\') && !escaped)
2143 // Check if we have an unclosed brace:
2147 return buf.toString();
2150 * if (s.length() == 0) return s; // Protect against ArrayIndexOutOf....
2151 * StringBuffer buf = new StringBuffer();
2153 * Matcher mcr = titleCapitalPattern.matcher(s.substring(1)); while
2154 * (mcr.find()) { String replaceStr = mcr.group();
2155 * mcr.appendReplacement(buf, "{" + replaceStr + "}"); }
2156 * mcr.appendTail(buf); return s.substring(0, 1) + buf.toString();
2160 static Pattern bracedTitleCapitalPattern = Pattern.compile("\\{[A-Z]+\\}");
2163 * This method looks for occurences of capital letters enclosed in an
2164 * arbitrary number of pairs of braces, e.g. "{AB}" or "{{T}}". All of these
2165 * pairs of braces are removed.
2168 * The String to analyze.
2169 * @return A new String with braces removed.
2171 public static String removeBracesAroundCapitals(String s) {
2172 String previous = s;
2173 while ((s = removeSingleBracesAroundCapitals(s)).length() < previous.length()) {
2180 * This method looks for occurences of capital letters enclosed in one pair
2181 * of braces, e.g. "{AB}". All these are replaced by only the capitals in
2182 * between the braces.
2185 * The String to analyze.
2186 * @return A new String with braces removed.
2188 public static String removeSingleBracesAroundCapitals(String s) {
2189 Matcher mcr = bracedTitleCapitalPattern.matcher(s);
2190 StringBuffer buf = new StringBuffer();
2191 while (mcr.find()) {
2192 String replaceStr = mcr.group();
2193 mcr.appendReplacement(buf, replaceStr.substring(1, replaceStr.length() - 1));
2195 mcr.appendTail(buf);
2196 return buf.toString();
2200 * This method looks up what kind of external binding is used for the given
2201 * field, and constructs on OpenFileFilter suitable for browsing for an
2205 * The BibTeX field in question.
2206 * @return The file filter.
2208 public static OpenFileFilter getFileFilterForField(String fieldName) {
2209 String s = BibtexFields.getFieldExtras(fieldName);
2210 final String ext = "." + fieldName.toLowerCase();
2211 final OpenFileFilter off;
2212 if (s.equals("browseDocZip"))
2213 off = new OpenFileFilter(new String[] { ext, ext + ".gz", ext + ".bz2" });
2215 off = new OpenFileFilter(new String[] { ext });
2220 * This method can be used to display a "rich" error dialog which offers the
2221 * entire stack trace for an exception.
2226 public static void showQuickErrorDialog(JFrame parent, String title, Exception e) {
2227 // create and configure a text area - fill it with exception text.
2228 final JPanel pan = new JPanel(), details = new JPanel();
2229 final CardLayout crd = new CardLayout();
2231 final JTextArea textArea = new JTextArea();
2232 textArea.setFont(new Font("Sans-Serif", Font.PLAIN, 10));
2233 textArea.setEditable(false);
2234 StringWriter writer = new StringWriter();
2235 e.printStackTrace(new PrintWriter(writer));
2236 textArea.setText(writer.toString());
2237 JLabel lab = new JLabel(e.getMessage());
2238 JButton flip = new JButton(Globals.lang("Details"));
2240 FormLayout layout = new FormLayout("left:pref", "");
2241 DefaultFormBuilder builder = new DefaultFormBuilder(layout);
2242 builder.append(lab);
2244 builder.append(Box.createVerticalGlue());
2246 builder.append(flip);
2247 final JPanel simple = builder.getPanel();
2249 // stuff it in a scrollpane with a controlled size.
2250 JScrollPane scrollPane = new JScrollPane(textArea);
2251 scrollPane.setPreferredSize(new Dimension(350, 150));
2252 details.setLayout(new BorderLayout());
2253 details.add(scrollPane, BorderLayout.CENTER);
2255 flip.addActionListener(new ActionListener() {
2256 public void actionPerformed(ActionEvent event) {
2257 crd.show(pan, "details");
2260 pan.add(simple, "simple");
2261 pan.add(details, "details");
2262 // pass the scrollpane to the joptionpane.
2263 JOptionPane.showMessageDialog(parent, pan, title, JOptionPane.ERROR_MESSAGE);
2266 public static String wrapHTML(String s, final int lineWidth) {
2267 StringBuffer sb = new StringBuffer();
2268 StringTokenizer tok = new StringTokenizer(s);
2269 int charsLeft = lineWidth;
2270 while (tok.hasMoreTokens()) {
2271 String word = tok.nextToken();
2272 if (charsLeft == lineWidth) { // fresh line
2274 charsLeft -= word.length();
2275 if (charsLeft <= 0) {
2276 sb.append("<br>\n");
2277 charsLeft = lineWidth;
2279 } else { // continue previous line
2280 if (charsLeft < word.length() + 1) {
2281 sb.append("<br>\n");
2283 if (word.length() >= lineWidth - 1) {
2284 sb.append("<br>\n");
2285 charsLeft = lineWidth;
2288 charsLeft = lineWidth - word.length() - 1;
2291 sb.append(' ').append(word);
2292 charsLeft -= word.length() + 1;
2296 return sb.toString();
2300 * Creates a String containing the current date (and possibly time),
2301 * formatted according to the format set in preferences under the key
2302 * "timeStampFormat".
2304 * @return The date string.
2306 public static String easyDateFormat() {
2307 // Date today = new Date();
2308 return easyDateFormat(new Date());
2312 * Creates a readable Date string from the parameter date. The format is set
2313 * in preferences under the key "timeStampFormat".
2315 * @return The formatted date string.
2317 public static String easyDateFormat(Date date) {
2318 // first use, create an instance
2319 if (dateFormatter == null) {
2320 String format = Globals.prefs.get("timeStampFormat");
2321 dateFormatter = new SimpleDateFormat(format);
2323 return dateFormatter.format(date);
2326 public static void markEntry(BibtexEntry be, NamedCompound ce) {
2327 Object o = be.getField(BibtexFields.MARKED);
2328 if ((o != null) && (o.toString().indexOf(Globals.prefs.WRAPPED_USERNAME) >= 0))
2332 newValue = Globals.prefs.WRAPPED_USERNAME;
2334 StringBuffer sb = new StringBuffer(o.toString());
2336 sb.append(Globals.prefs.WRAPPED_USERNAME);
2337 newValue = sb.toString();
2339 ce.addEdit(new UndoableFieldChange(be, BibtexFields.MARKED, be
2340 .getField(BibtexFields.MARKED), newValue));
2341 be.setField(BibtexFields.MARKED, newValue);
2344 public static void unmarkEntry(BibtexEntry be, BibtexDatabase database, NamedCompound ce) {
2345 Object o = be.getField(BibtexFields.MARKED);
2347 String s = o.toString();
2348 if (s.equals("0")) {
2349 unmarkOldStyle(be, database, ce);
2354 StringBuffer sb = new StringBuffer();
2355 while ((hit = s.indexOf(Globals.prefs.WRAPPED_USERNAME, piv)) >= 0) {
2357 sb.append(s.substring(piv, hit));
2358 piv = hit + Globals.prefs.WRAPPED_USERNAME.length();
2360 if (piv < s.length() - 1) {
2361 sb.append(s.substring(piv));
2363 String newVal = sb.length() > 0 ? sb.toString() : null;
2364 ce.addEdit(new UndoableFieldChange(be, BibtexFields.MARKED, be
2365 .getField(BibtexFields.MARKED), newVal));
2366 be.setField(BibtexFields.MARKED, newVal);
2371 * An entry is marked with a "0", not in the new style with user names. We
2372 * want to unmark it as transparently as possible. Since this shouldn't
2373 * happen too often, we do it by scanning the "owner" fields of the entire
2374 * database, collecting all user names. We then mark the entry for all users
2375 * except the current one. Thus only the user who unmarks will see that it
2376 * is unmarked, and we get rid of the old-style marking.
2381 private static void unmarkOldStyle(BibtexEntry be, BibtexDatabase database, NamedCompound ce) {
2382 TreeSet owners = new TreeSet();
2383 for (Iterator i = database.getEntries().iterator(); i.hasNext();) {
2384 BibtexEntry entry = (BibtexEntry) i.next();
2385 Object o = entry.getField(BibtexFields.OWNER);
2388 // System.out.println("Owner: "+entry.getField(Globals.OWNER));
2390 owners.remove(Globals.prefs.get("defaultOwner"));
2391 StringBuffer sb = new StringBuffer();
2392 for (Iterator i = owners.iterator(); i.hasNext();) {
2394 sb.append(i.next().toString());
2397 String newVal = sb.toString();
2398 if (newVal.length() == 0)
2400 ce.addEdit(new UndoableFieldChange(be, BibtexFields.MARKED, be
2401 .getField(BibtexFields.MARKED), newVal));
2402 be.setField(BibtexFields.MARKED, newVal);
2406 public static boolean isMarked(BibtexEntry be) {
2407 Object fieldVal = be.getField(BibtexFields.MARKED);
2408 if (fieldVal == null)
2410 String s = (String) fieldVal;
2411 return (s.equals("0") || (s.indexOf(Globals.prefs.WRAPPED_USERNAME) >= 0));
2415 * Set a given field to a given value for all entries in a Collection. This
2416 * method DOES NOT update any UndoManager, but returns a relevant
2417 * CompoundEdit that should be registered by the caller.
2420 * The entries to set the field for.
2422 * The name of the field to set.
2424 * The value to set. This value can be null, indicating that the
2425 * field should be cleared.
2426 * @param overwriteValues
2427 * Indicate whether the value should be set even if an entry
2428 * already has the field set.
2429 * @return A CompoundEdit for the entire operation.
2431 public static UndoableEdit massSetField(Collection entries, String field, String text,
2432 boolean overwriteValues) {
2434 NamedCompound ce = new NamedCompound(Globals.lang("Set field"));
2435 for (Iterator i = entries.iterator(); i.hasNext();) {
2436 BibtexEntry entry = (BibtexEntry) i.next();
2437 Object oldVal = entry.getField(field);
2438 // If we are not allowed to overwrite values, check if there is a
2440 // value already for this entry:
2441 if (!overwriteValues && (oldVal != null) && (((String) oldVal).length() > 0))
2444 entry.setField(field, text);
2446 entry.clearField(field);
2447 ce.addEdit(new UndoableFieldChange(entry, field, oldVal, text));
2454 * Make a list of supported character encodings that can encode all
2455 * characters in the given String.
2458 * A String of characters that should be supported by the
2460 * @return A List of character encodings
2462 public static List findEncodingsForString(String characters) {
2463 List encodings = new ArrayList();
2464 for (int i = 0; i < Globals.ENCODINGS.length; i++) {
2465 CharsetEncoder encoder = Charset.forName(Globals.ENCODINGS[i]).newEncoder();
2466 if (encoder.canEncode(characters))
2467 encodings.add(Globals.ENCODINGS[i]);
2473 * Will convert a two digit year using the following scheme (describe at
2474 * http://www.filemaker.com/help/02-Adding%20and%20view18.html):
2476 * If a two digit year is encountered they are matched against the last 69
2477 * years and future 30 years.
2479 * For instance if it is the year 1992 then entering 23 is taken to be 1923
2480 * but if you enter 23 in 1993 then it will evaluate to 2023.
2483 * The year to convert to 4 digits.
2486 public static String toFourDigitYear(String year) {
2487 if (thisYear == 0) {
2488 thisYear = Calendar.getInstance().get(Calendar.YEAR);
2490 return toFourDigitYear(year, thisYear);
2493 public static int thisYear;
2496 * Will convert a two digit year using the following scheme (describe at
2497 * http://www.filemaker.com/help/02-Adding%20and%20view18.html):
2499 * If a two digit year is encountered they are matched against the last 69
2500 * years and future 30 years.
2502 * For instance if it is the year 1992 then entering 23 is taken to be 1923
2503 * but if you enter 23 in 1993 then it will evaluate to 2023.
2506 * The year to convert to 4 digits.
2509 public static String toFourDigitYear(String year, int thisYear) {
2510 if (year.length() != 2)
2513 int thisYearTwoDigits = thisYear % 100;
2514 int thisCentury = thisYear - thisYearTwoDigits;
2516 int yearNumber = Integer.parseInt(year);
2518 if (yearNumber == thisYearTwoDigits) {
2519 return String.valueOf(thisYear);
2523 if ((yearNumber + 100 - thisYearTwoDigits) % 100 > 30) {
2524 if (yearNumber < thisYearTwoDigits) {
2525 return String.valueOf(thisCentury + yearNumber);
2527 return String.valueOf(thisCentury - 100 + yearNumber);
2530 if (yearNumber < thisYearTwoDigits) {
2531 return String.valueOf(thisCentury + 100 + yearNumber);
2533 return String.valueOf(thisCentury + yearNumber);
2536 } catch (NumberFormatException e) {
2542 * Will return an integer indicating the month of the entry from 0 to 11.
2544 * -1 signals a unknown month string.
2546 * This method accepts three types of months given:
2547 * - Single and Double Digit months from 1 to 12 (01 to 12)
2548 * - 3 Digit BibTex strings (jan, feb, mar...)
2549 * - Full English Month identifiers.
2554 public static int getMonthNumber(String month) {
2556 month = month.replaceAll("#", "").toLowerCase();
2558 for (int i = 0; i < Globals.MONTHS.length; i++) {
2559 if (month.startsWith(Globals.MONTHS[i])) {
2565 return Integer.parseInt(month) - 1;
2566 } catch (NumberFormatException e) {
2573 * Encodes a two-dimensional String array into a single string, using ':' and
2574 * ';' as separators. The characters ':' and ';' are escaped with '\'.
2575 * @param values The String array.
2576 * @return The encoded String.
2578 public static String encodeStringArray(String[][] values) {
2579 StringBuilder sb = new StringBuilder();
2580 for (int i = 0; i < values.length; i++) {
2581 sb.append(encodeStringArray(values[i]));
2582 if (i < values.length-1)
2585 return sb.toString();
2589 * Encodes a String array into a single string, using ':' as separator.
2590 * The characters ':' and ';' are escaped with '\'.
2591 * @param entry The String array.
2592 * @return The encoded String.
2594 public static String encodeStringArray(String[] entry) {
2595 StringBuilder sb = new StringBuilder();
2596 for (int i = 0; i < entry.length; i++) {
2597 sb.append(encodeString(entry[i]));
2598 if (i < entry.length-1)
2602 return sb.toString();
2606 * Decodes an encoded double String array back into array form. The array
2607 * is assumed to be square, and delimited by the characters ';' (first dim) and
2609 * @param value The encoded String to be decoded.
2610 * @return The decoded String array.
2612 public static String[][] decodeStringDoubleArray(String value) {
2613 ArrayList<ArrayList<String>> newList = new ArrayList<ArrayList<String>>();
2614 StringBuilder sb = new StringBuilder();
2615 ArrayList<String> thisEntry = new ArrayList<String>();
2616 boolean escaped = false;
2617 for (int i=0; i<value.length(); i++) {
2618 char c = value.charAt(i);
2619 if (!escaped && (c == '\\')) {
2623 else if (!escaped && (c == ':')) {
2624 thisEntry.add(sb.toString());
2625 sb = new StringBuilder();
2627 else if (!escaped && (c == ';')) {
2628 thisEntry.add(sb.toString());
2629 sb = new StringBuilder();
2630 newList.add(thisEntry);
2631 thisEntry = new ArrayList<String>();
2636 if (sb.length() > 0)
2637 thisEntry.add(sb.toString());
2638 if (thisEntry.size() > 0)
2639 newList.add(thisEntry);
2641 // Convert to String[][]:
2642 String[][] res = new String[newList.size()][newList.get(0).size()];
2643 for (int i = 0; i < res.length; i++) {
2644 for (int j = 0; j < res[i].length; j++) {
2645 res[i][j] = newList.get(i).get(j);
2651 private static String encodeString(String s) {
2652 StringBuilder sb = new StringBuilder();
2653 for (int i=0; i<s.length(); i++) {
2654 char c = s.charAt(i);
2655 if ((c == ';') || (c == ':') || (c == '\\'))
2659 return sb.toString();
2663 * Static equals that can also return the right result when one of the
2667 * The object whose equals method is called if the first is not
2670 * The object passed to the first one if the first is not null.
2671 * @return <code>one == null ? two == null : one.equals(two);</code>
2673 public static boolean equals(Object one, Object two) {
2674 return one == null ? two == null : one.equals(two);