package org.jabref.logic.layout;
import java.io.File;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.jabref.logic.formatter.bibtexfields.HtmlToLatexFormatter;
import org.jabref.logic.formatter.bibtexfields.UnicodeToLatexFormatter;
import org.jabref.logic.layout.format.AuthorAbbreviator;
import org.jabref.logic.layout.format.AuthorAndToSemicolonReplacer;
import org.jabref.logic.layout.format.AuthorAndsCommaReplacer;
import org.jabref.logic.layout.format.AuthorAndsReplacer;
import org.jabref.logic.layout.format.AuthorFirstAbbrLastCommas;
import org.jabref.logic.layout.format.AuthorFirstAbbrLastOxfordCommas;
import org.jabref.logic.layout.format.AuthorFirstFirst;
import org.jabref.logic.layout.format.AuthorFirstFirstCommas;
import org.jabref.logic.layout.format.AuthorFirstLastCommas;
import org.jabref.logic.layout.format.AuthorFirstLastOxfordCommas;
import org.jabref.logic.layout.format.AuthorLF_FF;
import org.jabref.logic.layout.format.AuthorLF_FFAbbr;
import org.jabref.logic.layout.format.AuthorLastFirst;
import org.jabref.logic.layout.format.AuthorLastFirstAbbrCommas;
import org.jabref.logic.layout.format.AuthorLastFirstAbbrOxfordCommas;
import org.jabref.logic.layout.format.AuthorLastFirstAbbreviator;
import org.jabref.logic.layout.format.AuthorLastFirstCommas;
import org.jabref.logic.layout.format.AuthorLastFirstOxfordCommas;
import org.jabref.logic.layout.format.AuthorNatBib;
import org.jabref.logic.layout.format.AuthorOrgSci;
import org.jabref.logic.layout.format.Authors;
import org.jabref.logic.layout.format.CompositeFormat;
import org.jabref.logic.layout.format.CreateBibORDFAuthors;
import org.jabref.logic.layout.format.CreateDocBookAuthors;
import org.jabref.logic.layout.format.CurrentDate;
import org.jabref.logic.layout.format.DOICheck;
import org.jabref.logic.layout.format.DOIStrip;
import org.jabref.logic.layout.format.DateFormatter;
import org.jabref.logic.layout.format.Default;
import org.jabref.logic.layout.format.EntryTypeFormatter;
import org.jabref.logic.layout.format.FileLink;
import org.jabref.logic.layout.format.FirstPage;
import org.jabref.logic.layout.format.FormatPagesForHTML;
import org.jabref.logic.layout.format.FormatPagesForXML;
import org.jabref.logic.layout.format.GetOpenOfficeType;
import org.jabref.logic.layout.format.HTMLChars;
import org.jabref.logic.layout.format.HTMLParagraphs;
import org.jabref.logic.layout.format.IfPlural;
import org.jabref.logic.layout.format.Iso690FormatDate;
import org.jabref.logic.layout.format.Iso690NamesAuthors;
import org.jabref.logic.layout.format.JournalAbbreviator;
import org.jabref.logic.layout.format.LastPage;
import org.jabref.logic.layout.format.LatexToUnicodeFormatter;
import org.jabref.logic.layout.format.NameFormatter;
import org.jabref.logic.layout.format.NoSpaceBetweenAbbreviations;
import org.jabref.logic.layout.format.NotFoundFormatter;
import org.jabref.logic.layout.format.Number;
import org.jabref.logic.layout.format.Ordinal;
import org.jabref.logic.layout.format.RTFChars;
import org.jabref.logic.layout.format.RemoveBrackets;
import org.jabref.logic.layout.format.RemoveBracketsAddComma;
import org.jabref.logic.layout.format.RemoveLatexCommandsFormatter;
import org.jabref.logic.layout.format.RemoveTilde;
import org.jabref.logic.layout.format.RemoveWhitespace;
import org.jabref.logic.layout.format.Replace;
import org.jabref.logic.layout.format.RisAuthors;
import org.jabref.logic.layout.format.RisKeywords;
import org.jabref.logic.layout.format.RisMonth;
import org.jabref.logic.layout.format.ToLowerCase;
import org.jabref.logic.layout.format.ToUpperCase;
import org.jabref.logic.layout.format.WrapContent;
import org.jabref.logic.layout.format.WrapFileLinks;
import org.jabref.logic.layout.format.XMLChars;
import org.jabref.logic.openoffice.OOPreFormatter;
import org.jabref.model.database.BibDatabase;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.strings.StringUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
class LayoutEntry {
private static final Log LOGGER = LogFactory.getLog(LayoutEntry.class);
private List<LayoutFormatter> option;
// Formatter to be run after other formatters:
private LayoutFormatter postFormatter;
private String text;
private List<LayoutEntry> layoutEntries;
private final int type;
private final List<String> invalidFormatter = new ArrayList<>();
private final LayoutFormatterPreferences prefs;
public LayoutEntry(StringInt si, LayoutFormatterPreferences prefs) {
this.prefs = prefs;
type = si.i;
switch (type) {
case LayoutHelper.IS_LAYOUT_TEXT:
text = si.s;
break;
case LayoutHelper.IS_SIMPLE_FIELD:
text = si.s.trim();
break;
case LayoutHelper.IS_OPTION_FIELD:
doOptionField(si.s);
break;
case LayoutHelper.IS_FIELD_START:
case LayoutHelper.IS_FIELD_END:
default:
break;
}
}
public LayoutEntry(List<StringInt> parsedEntries, int layoutType, LayoutFormatterPreferences prefs) {
this.prefs = prefs;
List<LayoutEntry> tmpEntries = new ArrayList<>();
String blockStart = parsedEntries.get(0).s;
String blockEnd = parsedEntries.get(parsedEntries.size() - 1).s;
if (!blockStart.equals(blockEnd)) {
LOGGER.warn("Field start and end entry must be equal.");
}
type = layoutType;
text = blockEnd;
List<StringInt> blockEntries = null;
for (StringInt parsedEntry : parsedEntries.subList(1, parsedEntries.size() - 1)) {
switch (parsedEntry.i) {
case LayoutHelper.IS_FIELD_START:
case LayoutHelper.IS_GROUP_START:
blockEntries = new ArrayList<>();
blockStart = parsedEntry.s;
break;
case LayoutHelper.IS_FIELD_END:
case LayoutHelper.IS_GROUP_END:
if (blockStart.equals(parsedEntry.s)) {
blockEntries.add(parsedEntry);
int groupType = parsedEntry.i == LayoutHelper.IS_GROUP_END ? LayoutHelper.IS_GROUP_START :
LayoutHelper.IS_FIELD_START;
LayoutEntry le = new LayoutEntry(blockEntries, groupType, prefs);
tmpEntries.add(le);
blockEntries = null;
} else {
LOGGER.warn("Nested field entries are not implemented!");
}
break;
case LayoutHelper.IS_LAYOUT_TEXT:
case LayoutHelper.IS_SIMPLE_FIELD:
case LayoutHelper.IS_OPTION_FIELD:
default:
// Do nothing
break;
}
if (blockEntries == null) {
tmpEntries.add(new LayoutEntry(parsedEntry, prefs));
} else {
blockEntries.add(parsedEntry);
}
}
layoutEntries = new ArrayList<>(tmpEntries);
for (LayoutEntry layoutEntry : layoutEntries) {
invalidFormatter.addAll(layoutEntry.getInvalidFormatters());
}
}
public void setPostFormatter(LayoutFormatter formatter) {
this.postFormatter = formatter;
}
public String doLayout(BibEntry bibtex, BibDatabase database) {
switch (type) {
case LayoutHelper.IS_LAYOUT_TEXT:
return text;
case LayoutHelper.IS_SIMPLE_FIELD:
String value = bibtex.getResolvedFieldOrAlias(text, database).orElse("");
// If a post formatter has been set, call it:
if (postFormatter != null) {
value = postFormatter.format(value);
}
return value;
case LayoutHelper.IS_FIELD_START:
case LayoutHelper.IS_GROUP_START:
return handleFieldOrGroupStart(bibtex, database);
case LayoutHelper.IS_FIELD_END:
case LayoutHelper.IS_GROUP_END:
return "";
case LayoutHelper.IS_OPTION_FIELD:
return handleOptionField(bibtex, database);
case LayoutHelper.IS_ENCODING_NAME:
// Printing the encoding name is not supported in entry layouts, only
// in begin/end layouts. This prevents breakage if some users depend
// on a field called "encoding". We simply return this field instead:
return bibtex.getResolvedFieldOrAlias("encoding", database).orElse(null);
default:
return "";
}
}
private String handleOptionField(BibEntry bibtex, BibDatabase database) {
String fieldEntry;
if (BibEntry.TYPE_HEADER.equals(text)) {
fieldEntry = bibtex.getType();
} else if (BibEntry.OBSOLETE_TYPE_HEADER.equals(text)) {
LOGGER.warn("'" + BibEntry.OBSOLETE_TYPE_HEADER
+ "' is an obsolete name for the entry type. Please update your layout to use '"
+ BibEntry.TYPE_HEADER + "' instead.");
fieldEntry = bibtex.getType();
} else {
// changed section begin - arudert
// resolve field (recognized by leading backslash) or text
fieldEntry = text.startsWith("\\") ? bibtex
.getResolvedFieldOrAlias(text.substring(1), database)
.orElse("") : BibDatabase.getText(text, database);
// changed section end - arudert
}
if (option != null) {
for (LayoutFormatter anOption : option) {
fieldEntry = anOption.format(fieldEntry);
}
}
// If a post formatter has been set, call it:
if (postFormatter != null) {
fieldEntry = postFormatter.format(fieldEntry);
}
return fieldEntry;
}
private String handleFieldOrGroupStart(BibEntry bibtex, BibDatabase database) {
Optional<String> field;
if (type == LayoutHelper.IS_GROUP_START) {
field = bibtex.getResolvedFieldOrAlias(text, database);
} else if (text.matches(".*(;|(\\&+)).*")) {
// split the strings along &, && or ; for AND formatter
String[] parts = text.split("\\s*(;|(\\&+))\\s*");
field = Optional.empty();
for (String part : parts) {
field = bibtex.getResolvedFieldOrAlias(part, database);
if (!field.isPresent()) {
break;
}
}
} else {
// split the strings along |, || for OR formatter
String[] parts = text.split("\\s*(\\|+)\\s*");
field = Optional.empty();
for (String part : parts) {
field = bibtex.getResolvedFieldOrAlias(part, database);
if (field.isPresent()) {
break;
}
}
}
if ((!field.isPresent()) || ((type == LayoutHelper.IS_GROUP_START)
&& field.get().equalsIgnoreCase(LayoutHelper.getCurrentGroup()))) {
return null;
} else {
if (type == LayoutHelper.IS_GROUP_START) {
LayoutHelper.setCurrentGroup(field.get());
}
StringBuilder sb = new StringBuilder(100);
String fieldText;
boolean previousSkipped = false;
for (int i = 0; i < layoutEntries.size(); i++) {
fieldText = layoutEntries.get(i).doLayout(bibtex, database);
if (fieldText == null) {
if ((i + 1) < layoutEntries.size()) {
if (layoutEntries.get(i + 1).doLayout(bibtex, database).trim().isEmpty()) {
i++;
previousSkipped = true;
continue;
}
}
} else {
// if previous was skipped --> remove leading line
// breaks
if (previousSkipped) {
int eol = 0;
while ((eol < fieldText.length())
&& ((fieldText.charAt(eol) == '\n') || (fieldText.charAt(eol) == '\r'))) {
eol++;
}
if (eol < fieldText.length()) {
sb.append(fieldText.substring(eol));
}
} else {
sb.append(fieldText);
}
}
previousSkipped = false;
}
return sb.toString();
}
}
/**
* Do layout for general formatters (no bibtex-entry fields).
*
* @param databaseContext
* Bibtex Database
* @return
*/
public String doLayout(BibDatabaseContext databaseContext, Charset encoding) {
switch (type) {
case LayoutHelper.IS_LAYOUT_TEXT:
return text;
case LayoutHelper.IS_SIMPLE_FIELD:
throw new UnsupportedOperationException("bibtex entry fields not allowed in begin or end layout");
case LayoutHelper.IS_FIELD_START:
case LayoutHelper.IS_GROUP_START:
throw new UnsupportedOperationException("field and group starts not allowed in begin or end layout");
case LayoutHelper.IS_FIELD_END:
case LayoutHelper.IS_GROUP_END:
throw new UnsupportedOperationException("field and group ends not allowed in begin or end layout");
case LayoutHelper.IS_OPTION_FIELD:
String field = BibDatabase.getText(text, databaseContext.getDatabase());
if (option != null) {
for (LayoutFormatter anOption : option) {
field = anOption.format(field);
}
}
// If a post formatter has been set, call it:
if (postFormatter != null) {
field = postFormatter.format(field);
}
return field;
case LayoutHelper.IS_ENCODING_NAME:
return encoding.displayName();
case LayoutHelper.IS_FILENAME:
return databaseContext.getDatabaseFile().map(File::getName).orElse("");
case LayoutHelper.IS_FILEPATH:
return databaseContext.getDatabaseFile().map(File::getPath).orElse("");
default:
break;
}
return "";
}
private void doOptionField(String s) {
List<String> v = StringUtil.tokenizeToList(s, "\n");
if (v.size() == 1) {
text = v.get(0);
} else {
text = v.get(0).trim();
option = getOptionalLayout(v.get(1));
// See if there was an undefined formatter:
for (LayoutFormatter anOption : option) {
if (anOption instanceof NotFoundFormatter) {
String notFound = ((NotFoundFormatter) anOption).getNotFound();
invalidFormatter.add(notFound);
}
}
}
}
private LayoutFormatter getLayoutFormatterByName(String name) throws Exception {
switch (name) {
case "HTMLToLatexFormatter": // For backward compatibility
case "HtmlToLatex":
return new HtmlToLatexFormatter();
case "UnicodeToLatexFormatter": // For backward compatibility
case "UnicodeToLatex":
return new UnicodeToLatexFormatter();
case "OOPreFormatter":
return new OOPreFormatter();
case "AuthorAbbreviator":
return new AuthorAbbreviator();
case "AuthorAndToSemicolonReplacer":
return new AuthorAndToSemicolonReplacer();
case "AuthorAndsCommaReplacer":
return new AuthorAndsCommaReplacer();
case "AuthorAndsReplacer":
return new AuthorAndsReplacer();
case "AuthorFirstAbbrLastCommas":
return new AuthorFirstAbbrLastCommas();
case "AuthorFirstAbbrLastOxfordCommas":
return new AuthorFirstAbbrLastOxfordCommas();
case "AuthorFirstFirst":
return new AuthorFirstFirst();
case "AuthorFirstFirstCommas":
return new AuthorFirstFirstCommas();
case "AuthorFirstLastCommas":
return new AuthorFirstLastCommas();
case "AuthorFirstLastOxfordCommas":
return new AuthorFirstLastOxfordCommas();
case "AuthorLastFirst":
return new AuthorLastFirst();
case "AuthorLastFirstAbbrCommas":
return new AuthorLastFirstAbbrCommas();
case "AuthorLastFirstAbbreviator":
return new AuthorLastFirstAbbreviator();
case "AuthorLastFirstAbbrOxfordCommas":
return new AuthorLastFirstAbbrOxfordCommas();
case "AuthorLastFirstCommas":
return new AuthorLastFirstCommas();
case "AuthorLastFirstOxfordCommas":
return new AuthorLastFirstOxfordCommas();
case "AuthorLF_FF":
return new AuthorLF_FF();
case "AuthorLF_FFAbbr":
return new AuthorLF_FFAbbr();
case "AuthorNatBib":
return new AuthorNatBib();
case "AuthorOrgSci":
return new AuthorOrgSci();
case "CompositeFormat":
return new CompositeFormat();
case "CreateBibORDFAuthors":
return new CreateBibORDFAuthors();
case "CreateDocBookAuthors":
return new CreateDocBookAuthors();
case "CurrentDate":
return new CurrentDate();
case "DateFormatter":
return new DateFormatter();
case "DOICheck":
return new DOICheck();
case "DOIStrip":
return new DOIStrip();
case "EntryTypeFormatter":
return new EntryTypeFormatter();
case "FirstPage":
return new FirstPage();
case "FormatPagesForHTML":
return new FormatPagesForHTML();
case "FormatPagesForXML":
return new FormatPagesForXML();
case "GetOpenOfficeType":
return new GetOpenOfficeType();
case "HTMLChars":
return new HTMLChars();
case "HTMLParagraphs":
return new HTMLParagraphs();
case "Iso690FormatDate":
return new Iso690FormatDate();
case "Iso690NamesAuthors":
return new Iso690NamesAuthors();
case "JournalAbbreviator":
return new JournalAbbreviator(prefs.getJournalAbbreviationLoader(),
prefs.getJournalAbbreviationPreferences());
case "LastPage":
return new LastPage();
case "FormatChars": // For backward compatibility
case "LatexToUnicode":
return new LatexToUnicodeFormatter();
case "NameFormatter":
return new NameFormatter();
case "NoSpaceBetweenAbbreviations":
return new NoSpaceBetweenAbbreviations();
case "Ordinal":
return new Ordinal();
case "RemoveBrackets":
return new RemoveBrackets();
case "RemoveBracketsAddComma":
return new RemoveBracketsAddComma();
case "RemoveLatexCommands":
return new RemoveLatexCommandsFormatter();
case "RemoveTilde":
return new RemoveTilde();
case "RemoveWhitespace":
return new RemoveWhitespace();
case "RisKeywords":
return new RisKeywords();
case "RisMonth":
return new RisMonth();
case "RTFChars":
return new RTFChars();
case "ToLowerCase":
return new ToLowerCase();
case "ToUpperCase":
return new ToUpperCase();
case "XMLChars":
return new XMLChars();
case "Default":
return new Default();
case "FileLink":
return new FileLink(prefs.getFileLinkPreferences());
case "Number":
return new Number();
case "RisAuthors":
return new RisAuthors();
case "Authors":
return new Authors();
case "IfPlural":
return new IfPlural();
case "Replace":
return new Replace();
case "WrapContent":
return new WrapContent();
case "WrapFileLinks":
return new WrapFileLinks(prefs.getFileLinkPreferences());
default:
return new NotFoundFormatter(name);
}
}
/**
* Return an array of LayoutFormatters found in the given formatterName
* string (in order of appearance).
*
*/
private List<LayoutFormatter> getOptionalLayout(String formatterName) {
List<List<String>> formatterStrings = parseMethodsCalls(formatterName);
List<LayoutFormatter> results = new ArrayList<>(formatterStrings.size());
Map<String, String> userNameFormatter = NameFormatter.getNameFormatters(prefs.getNameFormatterPreferences());
for (List<String> strings : formatterStrings) {
String nameFormatterName = strings.get(0).trim();
// Check if this is a name formatter defined by this export filter:
Optional<String> contents = prefs.getCustomExportNameFormatter(nameFormatterName);
if (contents.isPresent()) {
NameFormatter nf = new NameFormatter();
nf.setParameter(contents.get());
results.add(nf);
continue;
}
// Try to load from formatters in formatter folder
try {
LayoutFormatter f = getLayoutFormatterByName(nameFormatterName);
// If this formatter accepts an argument, check if we have one, and
// set it if so:
if ((f instanceof ParamLayoutFormatter) && (strings.size() >= 2)) {
((ParamLayoutFormatter) f).setArgument(strings.get(1));
}
results.add(f);
continue;
} catch (Exception ex) {
LOGGER.info("Problem with formatter", ex);
}
// Then check whether this is a user defined formatter
String formatterParameter = userNameFormatter.get(nameFormatterName);
if (formatterParameter != null) {
NameFormatter nf = new NameFormatter();
nf.setParameter(formatterParameter);
results.add(nf);
continue;
}
results.add(new NotFoundFormatter(nameFormatterName));
}
return results;
}
public List<String> getInvalidFormatters() {
return invalidFormatter;
}
public static List<List<String>> parseMethodsCalls(String calls) {
List<List<String>> result = new ArrayList<>();
char[] c = calls.toCharArray();
int i = 0;
while (i < c.length) {
int start = i;
if (Character.isJavaIdentifierStart(c[i])) {
i++;
while ((i < c.length) && (Character.isJavaIdentifierPart(c[i]) || (c[i] == '.'))) {
i++;
}
if ((i < c.length) && (c[i] == '(')) {
String method = calls.substring(start, i);
// Skip the brace
i++;
int bracelevel = 0;
if (i < c.length) {
if (c[i] == '"') {
// Parameter is in format "xxx"
// Skip "
i++;
int startParam = i;
i++;
boolean escaped = false;
while (((i + 1) < c.length)
&& !(!escaped && (c[i] == '"') && (c[i + 1] == ')') && (bracelevel == 0))) {
if (c[i] == '\\') {
escaped = !escaped;
} else if (c[i] == '(') {
bracelevel++;
} else if (c[i] == ')') {
bracelevel--;
} else {
escaped = false;
}
i++;
}
String param = calls.substring(startParam, i);
result.add(Arrays.asList(method, param));
} else {
// Parameter is in format xxx
int startParam = i;
while ((i < c.length) && (!((c[i] == ')') && (bracelevel == 0)))) {
if (c[i] == '(') {
bracelevel++;
} else if (c[i] == ')') {
bracelevel--;
}
i++;
}
String param = calls.substring(startParam, i);
result.add(Arrays.asList(method, param));
}
} else {
// Incorrectly terminated open brace
result.add(Collections.singletonList(method));
}
} else {
String method = calls.substring(start, i);
result.add(Collections.singletonList(method));
}
}
i++;
}
return result;
}
}