package org.jabref.logic.layout.format;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.jabref.logic.layout.AbstractParamLayoutFormatter;
import org.jabref.model.entry.FileFieldParser;
import org.jabref.model.entry.LinkedFile;
import org.jabref.model.util.FileHelper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* 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 static final Log LOGGER = LogFactory.getLog(WrapFileLinks.class);
private static final int STRING = 0;
private static final int ITERATION_COUNT = 1;
private static final int FILE_PATH = 2;
private static final int FILE_TYPE = 3;
private static final int FILE_EXTENSION = 4;
private static final int FILE_DESCRIPTION = 5;
private static final int RELATIVE_FILE_PATH = 6;
// Define which escape sequences give what results:
private static final Map<Character, Integer> ESCAPE_SEQ = new HashMap<>();
static {
WrapFileLinks.ESCAPE_SEQ.put('i', WrapFileLinks.ITERATION_COUNT);
WrapFileLinks.ESCAPE_SEQ.put('p', WrapFileLinks.FILE_PATH);
WrapFileLinks.ESCAPE_SEQ.put('r', WrapFileLinks.RELATIVE_FILE_PATH);
WrapFileLinks.ESCAPE_SEQ.put('f', WrapFileLinks.FILE_TYPE);
WrapFileLinks.ESCAPE_SEQ.put('x', WrapFileLinks.FILE_EXTENSION);
WrapFileLinks.ESCAPE_SEQ.put('d', WrapFileLinks.FILE_DESCRIPTION);
}
private final Map<String, String> replacements = new HashMap<>();
private final FileLinkPreferences prefs;
private String fileType;
private List<FormatEntry> format;
public WrapFileLinks(FileLinkPreferences fileLinkPreferences) {
this.prefs = fileLinkPreferences;
}
/**
* 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.
*/
private static List<FormatEntry> parseFormatString(String format) {
List<FormatEntry> l = new ArrayList<>();
StringBuilder sb = new StringBuilder();
boolean escaped = false;
for (int i = 0; i < format.length(); i++) {
char c = format.charAt(i);
if (escaped) {
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('\\');
} else if (WrapFileLinks.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(WrapFileLinks.ESCAPE_SEQ.get(c)));
} else {
// Unknown escape sequence.
sb.append('\\');
sb.append(c);
}
} else {
// Check if we are at the start of an escape sequence:
if (c == '\\') {
escaped = true;
} else {
sb.append(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;
}
@Override
public void setArgument(String arg) {
List<String> parts = AbstractParamLayoutFormatter.parseArgument(arg);
format = parseFormatString(parts.get(0));
if ((parts.size() > 1) && !parts.get(1).trim().isEmpty()) {
fileType = parts.get(1);
}
if (parts.size() > 2) {
for (int i = 2; i < (parts.size() - 1); i += 2) {
replacements.put(parts.get(i), parts.get(i + 1));
}
}
}
@Override
public String format(String field) {
if (field == null) {
return "";
}
StringBuilder sb = new StringBuilder();
// Build the list containing the links:
List<LinkedFile> fileList = FileFieldParser.parse(field);
int piv = 1; // counter for relevant iterations
for (LinkedFile flEntry : fileList) {
// Use this entry if we don't discriminate on types, or if the type fits:
if ((fileType == null) || flEntry.getFileType().equalsIgnoreCase(fileType)) {
for (FormatEntry entry : format) {
switch (entry.getType()) {
case STRING:
sb.append(entry.getString());
break;
case ITERATION_COUNT:
sb.append(piv);
break;
case FILE_PATH:
List<String> dirs;
// 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 ((prefs.getFileDirForDatabase() == null) || prefs.getFileDirForDatabase().isEmpty()) {
dirs = prefs.getGeneratedDirForDatabase();
} else {
dirs = prefs.getFileDirForDatabase();
}
String pathString = flEntry.findIn(dirs.stream().map(Paths::get).collect(Collectors.toList()))
.map(path -> path.toAbsolutePath().toString())
.orElse(flEntry.getLink());
sb.append(replaceStrings(pathString));
break;
case RELATIVE_FILE_PATH:
/*
* 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:
FileHelper.getFileExtension(flEntry.getLink())
.ifPresent(extension -> sb.append(replaceStrings(extension)));
break;
case FILE_TYPE:
sb.append(replaceStrings(flEntry.getFileType()));
break;
case FILE_DESCRIPTION:
sb.append(replaceStrings(flEntry.getDescription()));
break;
default:
break;
}
}
piv++; // update counter
}
}
return sb.toString();
}
private String replaceStrings(String text) {
String result = text;
for (Map.Entry<String, String> stringStringEntry : replacements.entrySet()) {
String to = stringStringEntry.getValue();
result = result.replaceAll(stringStringEntry.getKey(), to);
}
return result;
}
/**
* 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.
*/
static class FormatEntry {
private final int type;
private String string;
public FormatEntry(int type) {
this.type = type;
}
public FormatEntry(String value) {
this.type = WrapFileLinks.STRING;
this.string = value;
}
public int getType() {
return type;
}
public String getString() {
return string;
}
}
}