package org.jabref.logic.exporter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.jabref.logic.bibtex.LatexFieldFormatterPreferences;
import org.jabref.logic.bibtex.comparator.BibtexStringComparator;
import org.jabref.logic.bibtex.comparator.CrossRefEntryComparator;
import org.jabref.logic.bibtex.comparator.FieldComparator;
import org.jabref.logic.bibtex.comparator.FieldComparatorStack;
import org.jabref.logic.bibtex.comparator.IdComparator;
import org.jabref.model.EntryTypes;
import org.jabref.model.FieldChange;
import org.jabref.model.bibtexkeypattern.GlobalBibtexKeyPattern;
import org.jabref.model.cleanup.FieldFormatterCleanups;
import org.jabref.model.database.BibDatabase;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.database.BibDatabaseMode;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.BibtexString;
import org.jabref.model.entry.CustomEntryType;
import org.jabref.model.entry.EntryType;
import org.jabref.model.metadata.MetaData;
import org.jabref.model.metadata.SaveOrderConfig;
public abstract class BibDatabaseWriter<E extends SaveSession> {
private static final Pattern REFERENCE_PATTERN = Pattern.compile("(#[A-Za-z]+#)"); // Used to detect string references in strings
private final SaveSessionFactory<E> saveSessionFactory;
private E session;
public BibDatabaseWriter(SaveSessionFactory<E> saveSessionFactory) {
this.saveSessionFactory = saveSessionFactory;
}
public interface SaveSessionFactory<E extends SaveSession> {
E createSaveSession(Charset encoding, Boolean makeBackup) throws SaveException;
}
private static List<FieldChange> applySaveActions(List<BibEntry> toChange, MetaData metaData) {
List<FieldChange> changes = new ArrayList<>();
Optional<FieldFormatterCleanups> saveActions = metaData.getSaveActions();
saveActions.ifPresent(actions -> {
// save actions defined -> apply for every entry
for (BibEntry entry : toChange) {
changes.addAll(actions.applySaveActions(entry));
}
});
return changes;
}
public static List<FieldChange> applySaveActions(BibEntry entry, MetaData metaData) {
return applySaveActions(Arrays.asList(entry), metaData);
}
private static List<Comparator<BibEntry>> getSaveComparators(SavePreferences preferences, MetaData metaData) {
List<Comparator<BibEntry>> comparators = new ArrayList<>();
Optional<SaveOrderConfig> saveOrder = getSaveOrder(preferences, metaData);
// Take care, using CrossRefEntry-Comparator, that referred entries occur after referring
// ones. This is a necessary requirement for BibTeX to be able to resolve referenced entries correctly.
comparators.add(new CrossRefEntryComparator());
if (! saveOrder.isPresent()) {
// entries will be sorted based on their internal IDs
comparators.add(new IdComparator());
} else {
// use configured sorting strategy
comparators.add(new FieldComparator(saveOrder.get().sortCriteria[0]));
comparators.add(new FieldComparator(saveOrder.get().sortCriteria[1]));
comparators.add(new FieldComparator(saveOrder.get().sortCriteria[2]));
comparators.add(new FieldComparator(BibEntry.KEY_FIELD));
}
return comparators;
}
/*
* We have begun to use getSortedEntries() for both database save operations
* and non-database save operations. In a non-database save operation
* (such as the exportDatabase call), we do not wish to use the
* global preference of saving in standard order.
*/
public static List<BibEntry> getSortedEntries(BibDatabaseContext bibDatabaseContext, List<BibEntry> entriesToSort,
SavePreferences preferences) {
Objects.requireNonNull(bibDatabaseContext);
Objects.requireNonNull(entriesToSort);
//if no meta data are present, simply return in original order
if (bibDatabaseContext.getMetaData() == null) {
List<BibEntry> result = new LinkedList<>();
result.addAll(entriesToSort);
return result;
}
List<Comparator<BibEntry>> comparators = BibDatabaseWriter.getSaveComparators(preferences,
bibDatabaseContext.getMetaData());
FieldComparatorStack<BibEntry> comparatorStack = new FieldComparatorStack<>(comparators);
List<BibEntry> sorted = new ArrayList<>();
sorted.addAll(entriesToSort);
Collections.sort(sorted, comparatorStack);
return sorted;
}
private static Optional<SaveOrderConfig> getSaveOrder(SavePreferences preferences, MetaData metaData) {
/* three options:
* 1. original order
* 2. order specified in metaData
* 3. order specified in preferences
*/
if (preferences.isSaveInOriginalOrder()) {
return Optional.empty();
}
if (preferences.getTakeMetadataSaveOrderInAccount()) {
return metaData.getSaveOrderConfig();
}
return Optional.ofNullable(preferences.getSaveOrder());
}
/**
* Saves the complete database.
*/
public E saveDatabase(BibDatabaseContext bibDatabaseContext, SavePreferences preferences)
throws SaveException {
return savePartOfDatabase(bibDatabaseContext, bibDatabaseContext.getDatabase().getEntries(), preferences);
}
/**
* Saves the database, including only the specified entries.
*/
public E savePartOfDatabase(BibDatabaseContext bibDatabaseContext,
List<BibEntry> entries, SavePreferences preferences) throws SaveException {
session = saveSessionFactory.createSaveSession(preferences.getEncodingOrDefault(), preferences.getMakeBackup());
Optional<String> sharedDatabaseIDOptional = bibDatabaseContext.getDatabase().getSharedDatabaseID();
if (sharedDatabaseIDOptional.isPresent()) {
writeDatabaseID(sharedDatabaseIDOptional.get());
}
// Map to collect entry type definitions that we must save along with entries using them.
Map<String, EntryType> typesToWrite = new TreeMap<>();
// Some file formats write something at the start of the file (like the encoding)
if (preferences.getSaveType() != SavePreferences.DatabaseSaveType.PLAIN_BIBTEX) {
writePrelogue(bibDatabaseContext, preferences.getEncoding());
}
// Write preamble if there is one.
writePreamble(bibDatabaseContext.getDatabase().getPreamble().orElse(""));
// Write strings if there are any.
writeStrings(bibDatabaseContext.getDatabase(), preferences.isReformatFile(),
preferences.getLatexFieldFormatterPreferences());
// Write database entries.
List<BibEntry> sortedEntries = getSortedEntries(bibDatabaseContext, entries, preferences);
List<FieldChange> saveActionChanges = applySaveActions(sortedEntries, bibDatabaseContext.getMetaData());
session.addFieldChanges(saveActionChanges);
for (BibEntry entry : sortedEntries) {
// Check if we must write the type definition for this
// entry, as well. Our criterion is that all non-standard
// types (*not* all customized standard types) must be written.
if (!EntryTypes.getStandardType(entry.getType(), bibDatabaseContext.getMode()).isPresent()) {
// If user-defined entry type, then add it
// Otherwise (getType returns empty optional) it is a completely unknown entry type, so ignore it
EntryTypes.getType(entry.getType(), bibDatabaseContext.getMode()).ifPresent(
entryType -> typesToWrite.put(entryType.getName(), entryType));
}
writeEntry(entry, bibDatabaseContext.getMode(), preferences.isReformatFile(),
preferences.getLatexFieldFormatterPreferences());
}
if (preferences.getSaveType() != SavePreferences.DatabaseSaveType.PLAIN_BIBTEX) {
// Write meta data.
writeMetaData(bibDatabaseContext.getMetaData(), preferences.getGlobalCiteKeyPattern());
// Write type definitions, if any:
writeEntryTypeDefinitions(typesToWrite);
}
//finally write whatever remains of the file, but at least a concluding newline
writeEpilogue(bibDatabaseContext.getDatabase().getEpilog());
try {
session.getWriter().close();
} catch (IOException e) {
throw new SaveException(e);
}
return session;
}
protected abstract void writePrelogue(BibDatabaseContext bibDatabaseContext, Charset encoding) throws SaveException;
protected abstract void writeEntry(BibEntry entry, BibDatabaseMode mode, Boolean isReformatFile,
LatexFieldFormatterPreferences latexFieldFormatterPreferences) throws SaveException;
protected abstract void writeEpilogue(String epilogue) throws SaveException;
/**
* Writes all data to the specified writer, using each object's toString() method.
*/
protected void writeMetaData(MetaData metaData, GlobalBibtexKeyPattern globalCiteKeyPattern) throws SaveException {
Objects.requireNonNull(metaData);
Map<String, String> serializedMetaData = MetaDataSerializer.getSerializedStringMap(metaData,
globalCiteKeyPattern);
for (Map.Entry<String, String> metaItem : serializedMetaData.entrySet()) {
writeMetaDataItem(metaItem);
}
}
protected abstract void writeMetaDataItem(Map.Entry<String, String> metaItem) throws SaveException;
protected abstract void writePreamble(String preamble) throws SaveException;
protected abstract void writeDatabaseID(String sharedDatabaseID) throws SaveException;
/**
* Write all strings in alphabetical order, modified to produce a safe (for
* BibTeX) order of the strings if they reference each other.
*
* @param database The database whose strings we should write.
*/
private void writeStrings(BibDatabase database, Boolean reformatFile,
LatexFieldFormatterPreferences latexFieldFormatterPreferences) throws SaveException {
List<BibtexString> strings = database.getStringKeySet().stream().map(database::getString).collect(
Collectors.toList());
strings.sort(new BibtexStringComparator(true));
// First, make a Map of all entries:
Map<String, BibtexString> remaining = new HashMap<>();
int maxKeyLength = 0;
for (BibtexString string : strings) {
remaining.put(string.getName(), string);
maxKeyLength = Math.max(maxKeyLength, string.getName().length());
}
for (BibtexString.Type t : BibtexString.Type.values()) {
boolean isFirstStringInType = true;
for (BibtexString bs : strings) {
if (remaining.containsKey(bs.getName()) && (bs.getType() == t)) {
writeString(bs, isFirstStringInType, remaining, maxKeyLength, reformatFile,
latexFieldFormatterPreferences);
isFirstStringInType = false;
}
}
}
}
protected void writeString(BibtexString bibtexString, boolean isFirstString, Map<String, BibtexString> remaining, int maxKeyLength,
Boolean reformatFile, LatexFieldFormatterPreferences latexFieldFormatterPreferences)
throws SaveException {
// First remove this from the "remaining" list so it can't cause problem with circular refs:
remaining.remove(bibtexString.getName());
// Then we go through the string looking for references to other strings. If we find references
// to strings that we will write, but still haven't, we write those before proceeding. This ensures
// that the string order will be acceptable for BibTeX.
String content = bibtexString.getContent();
Matcher m;
while ((m = REFERENCE_PATTERN.matcher(content)).find()) {
String foundLabel = m.group(1);
int restIndex = content.indexOf(foundLabel) + foundLabel.length();
content = content.substring(restIndex);
String label = foundLabel.substring(1, foundLabel.length() - 1);
// If the label we found exists as a key in the "remaining" Map, we go on and write it now:
if (remaining.containsKey(label)) {
BibtexString referred = remaining.get(label);
writeString(referred, isFirstString, remaining, maxKeyLength, reformatFile,
latexFieldFormatterPreferences);
}
}
writeString(bibtexString, isFirstString, maxKeyLength, reformatFile, latexFieldFormatterPreferences);
}
protected abstract void writeString(BibtexString bibtexString, boolean isFirstString, int maxKeyLength,
Boolean reformatFile, LatexFieldFormatterPreferences latexFieldFormatterPreferences)
throws SaveException;
protected void writeEntryTypeDefinitions(Map<String, EntryType> types) throws SaveException {
for (EntryType type : types.values()) {
if (type instanceof CustomEntryType) {
writeEntryTypeDefinition((CustomEntryType) type);
}
}
}
protected abstract void writeEntryTypeDefinition(CustomEntryType customType) throws SaveException;
protected SaveSession getActiveSession() {
return session;
}
}