package net.sf.jabref.export.layout.format; import net.sf.jabref.export.layout.AbstractParamLayoutFormatter; import net.sf.jabref.gui.FileListTableModel; import net.sf.jabref.gui.FileListEntry; import net.sf.jabref.Globals; import net.sf.jabref.GUIGlobals; import net.sf.jabref.Util; import java.util.*; import java.io.File; import java.io.IOException; /** * This formatter iterates over all file links, or all file links of a specified * type, outputting a format string given as the first argument. The format string * can contain a number of escape sequences indicating file link information to * be inserted into the string. * <p/> * This formatter can take an optional second argument specifying the name of a file * type. If specified, the iteration will only include those files with a file type * matching the given name (case-insensitively). If specified as an empty argument, * all file links will be included. * <p/> * After the second argument, pairs of additional arguments can be added in order to * specify regular expression replacements to be done upon the inserted link information * before insertion into the output string. A non-paired argument will be ignored. * In order to specify replacements without filtering on file types, use an empty second * argument. * <p/> * <p/> * <p/> * The escape sequences for embedding information are as follows: * <p/> * \i : This inserts the iteration index (starting from 1), and can be useful if * the output list of files should be enumerated. * \p : This inserts the file path of the file link. Relative links below your file directory * will be expanded to their absolute path. * \r : This inserts the file path without expanding relative links. * \f : This inserts the name of the file link's type. * \x : This inserts the file's extension, if any. * \d : This inserts the file link's description, if any. * <p/> * For instance, an entry could contain a file link to the file "/home/john/report.pdf" * of the "PDF" type with description "John's final report". * <p/> * Using the WrapFileLinks formatter with the following argument: * <p/> * \format[WrapFileLinks(\i. \d (\p))]{\file} * <p/> * would give the following output: * 1. John's final report (/home/john/report.pdf) * <p/> * If the entry contained a second file link to the file "/home/john/draft.txt" of the * "Text file" type with description 'An early "draft"', the output would be as follows: * 1. John's final report (/home/john/report.pdf) * 2. An early "draft" (/home/john/draft.txt) * <p/> * If the formatter was called with a second argument, the list would be filtered. * For instance: * \format[WrapFileLinks(\i. \d (\p),text file)]{\file} * <p/> * would show only the text file: * 1. An early "draft" (/home/john/draft.txt) * <p/> * If we wanted this output to be part of an XML styled output, the quotes in the * file description could cause problems. Adding two additional arguments to translate * the quotes into XML characters solves this: * \format[WrapFileLinks(\i. \d (\p),text file,",")]{\file} * <p/> * would give the following output: * 1. An early "draft" (/home/john/draft.txt) * * Additional pairs of replacements can be added. */ public class WrapFileLinks extends AbstractParamLayoutFormatter { private String fileType = null; private List<FormatEntry> format = null; private Map<String, String> replacements = new HashMap<String, String>(); public void setArgument(String arg) { String[] parts = parseArgument(arg); format = parseFormatString(parts[0]); if ((parts.length > 1) && (parts[1].trim().length() > 0)) fileType = parts[1]; if (parts.length > 2) { for (int i = 2; i < parts.length-1; i+=2) { replacements.put(parts[i], parts[i+1]); } } } public String format(String field) { StringBuilder sb = new StringBuilder(); // Build the table model containing the links: FileListTableModel tableModel = new FileListTableModel(); if (field == null) return ""; tableModel.setContent(field); int piv = 1; // counter for relevant iterations for (int i = 0; i < tableModel.getRowCount(); i++) { FileListEntry flEntry = tableModel.getEntry(i); // Use this entry if we don't discriminate on types, or if the type fits: if ((fileType == null) || flEntry.getType().getName().toLowerCase().equals(fileType)) { for (FormatEntry entry : format) { switch (entry.getType()) { case STRING: sb.append(entry.getString()); break; case ITERATION_COUNT: sb.append(String.valueOf(piv)); break; case FILE_PATH: if (flEntry.getLink() == null) break; String dir; // We need to resolve the file directory from the database's metadata, // but that is not available from a formatter. Therefore, as an // ugly hack, the export routine has set a global variable before // starting the export, which contains the database's file directory: if (Globals.prefs.fileDirForDatabase != null) dir = Globals.prefs.fileDirForDatabase; else dir = Globals.prefs.get(GUIGlobals.FILE_FIELD + "Directory"); File f = Util.expandFilename(flEntry.getLink(), new String[]{dir}); /* * Stumbled over this while investigating * * https://sourceforge.net/tracker/index.php?func=detail&aid=1469903&group_id=92314&atid=600306 */ if (f != null) { try { sb.append(replaceStrings(f.getCanonicalPath()));//f.toURI().toString(); } catch (IOException ex) { ex.printStackTrace(); sb.append(replaceStrings(f.getPath())); } } else { sb.append(replaceStrings(flEntry.getLink())); } break; case RELATIVE_FILE_PATH: if (flEntry.getLink() == null) break; /* * Stumbled over this while investigating * * https://sourceforge.net/tracker/index.php?func=detail&aid=1469903&group_id=92314&atid=600306 */ sb.append(replaceStrings(flEntry.getLink()));//f.toURI().toString(); break; case FILE_EXTENSION: if (flEntry.getLink() == null) break; int index = flEntry.getLink().lastIndexOf('.'); if ((index >= 0) && (index < flEntry.getLink().length() - 1)) sb.append(replaceStrings(flEntry.getLink().substring(index + 1))); break; case FILE_TYPE: sb.append(replaceStrings(flEntry.getType().getName())); break; case FILE_DESCRIPTION: sb.append(replaceStrings(flEntry.getDescription())); break; } } piv++; // update counter } } return sb.toString(); } protected String replaceStrings(String text) { for (Iterator<String> i=replacements.keySet().iterator(); i.hasNext();) { String from = i.next(); String to = replacements.get(from); text = text.replaceAll(from, to); } return text; } // Define codes for the various escape sequences that can be inserted: public static final int STRING = 0, ITERATION_COUNT = 1, FILE_PATH = 2, FILE_TYPE = 3, FILE_EXTENSION = 4, FILE_DESCRIPTION = 5, RELATIVE_FILE_PATH = 6; // Define which escape sequences give what results: final static Map<Character, Integer> ESCAPE_SEQ = new HashMap<Character, Integer>(); static { ESCAPE_SEQ.put('i', ITERATION_COUNT); ESCAPE_SEQ.put('p', FILE_PATH); ESCAPE_SEQ.put('r', RELATIVE_FILE_PATH); ESCAPE_SEQ.put('f', FILE_TYPE); ESCAPE_SEQ.put('x', FILE_EXTENSION); ESCAPE_SEQ.put('d', FILE_DESCRIPTION); } /** * Parse a format string and return a list of FormatEntry objects. The format * string is basically marked up with "\i" marking that the iteration number should * be inserted, and with "\p" marking that the file path of the current iteration * should be inserted, plus additional markers. * * @param format The marked-up string. * @return the resulting format entries. */ public List<FormatEntry> parseFormatString(String format) { List<FormatEntry> l = new ArrayList<FormatEntry>(); StringBuilder sb = new StringBuilder(); boolean escaped = false; for (int i = 0; i < format.length(); i++) { char c = format.charAt(i); if (!escaped) { // Check if we are at the start of an escape sequence: if (c == '\\') escaped = true; else sb.append(c); } else { escaped = false; // we know we'll be out of escape mode after this // Check if this escape sequence is meaningful: if (c == '\\') { // Escaped backslash: means that we add a backslash: sb.append(c); } else if (ESCAPE_SEQ.containsKey(c)) { // Ok, we have the code. Add the previous string (if any) and // the entry indicated by the escape sequence: if (sb.length() > 0) { l.add(new FormatEntry(sb.toString())); // Clear the buffer: sb = new StringBuilder(); } l.add(new FormatEntry(ESCAPE_SEQ.get(c))); } else { // Unknown escape sequence. sb.append('\\'); sb.append(c); //System.out.println("Error: unknown escape sequence: \\"+String.valueOf(c)); } } } // Finished scanning the string. If we collected text at the end, add an entry for it: if (sb.length() > 0) { l.add(new FormatEntry(sb.toString())); } return l; } /** * This class defines the building blocks of a parsed format strings. Each FormatEntry * represents either a literal string or a piece of information pertaining to the file * link to be exported or to the iteration through a series of file links. For literal * strings this class encapsulates the literal itself, while for other types of information, * only a type code is provided, and the subclass needs to fill in the proper information * based on the file link to be exported or the iteration status. */ protected class FormatEntry { private int type; private String string = null; public FormatEntry(int type) { this.type = type; } public FormatEntry(String value) { this.type = STRING; this.string = value; } public int getType() { return type; } public String getString() { return string; } } }