package org.jabref.logic.bibtex; import java.io.IOException; import java.io.Writer; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.Set; import java.util.TreeSet; import java.util.function.Predicate; import org.jabref.logic.TypedBibEntry; import org.jabref.logic.util.OS; import org.jabref.model.EntryTypes; import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.EntryType; import org.jabref.model.entry.InternalBibtexFields; import org.jabref.model.strings.StringUtil; public class BibEntryWriter { private final LatexFieldFormatter fieldFormatter; private final boolean write; public BibEntryWriter(LatexFieldFormatter fieldFormatter, boolean write) { this.fieldFormatter = fieldFormatter; this.write = write; } public void write(BibEntry entry, Writer out, BibDatabaseMode bibDatabaseMode) throws IOException { write(entry, out, bibDatabaseMode, false); } /** * Writes the given BibEntry using the given writer * * @param entry The entry to write * @param out The writer to use * @param bibDatabaseMode The database mode (bibtex or biblatex) * @param reformat Should the entry be in any case, even if no change occurred? */ public void write(BibEntry entry, Writer out, BibDatabaseMode bibDatabaseMode, Boolean reformat) throws IOException { // if the entry has not been modified, write it as it was if (!reformat && !entry.hasChanged()) { out.write(entry.getParsedSerialization()); return; } writeUserComments(entry, out); out.write(OS.NEWLINE); writeRequiredFieldsFirstRemainingFieldsSecond(entry, out, bibDatabaseMode); out.write(OS.NEWLINE); } private void writeUserComments(BibEntry entry, Writer out) throws IOException { String userComments = entry.getUserComments(); if (!userComments.isEmpty()) { out.write(userComments + OS.NEWLINE); } } public void writeWithoutPrependedNewlines(BibEntry entry, Writer out, BibDatabaseMode bibDatabaseMode) throws IOException { // if the entry has not been modified, write it as it was if (!entry.hasChanged()) { out.write(entry.getParsedSerialization().trim()); return; } writeRequiredFieldsFirstRemainingFieldsSecond(entry, out, bibDatabaseMode); } /** * Write fields in the order of requiredFields, optionalFields and other fields, but does not sort the fields. * * @param entry * @param out * @throws IOException */ private void writeRequiredFieldsFirstRemainingFieldsSecond(BibEntry entry, Writer out, BibDatabaseMode bibDatabaseMode) throws IOException { // Write header with type and bibtex-key. TypedBibEntry typedEntry = new TypedBibEntry(entry, bibDatabaseMode); out.write('@' + typedEntry.getTypeForDisplay() + '{'); writeKeyField(entry, out); Set<String> written = new HashSet<>(); written.add(BibEntry.KEY_FIELD); int indentation = getLengthOfLongestFieldName(entry); EntryType type = EntryTypes.getTypeOrDefault(entry.getType(), bibDatabaseMode); // Write required fields first. List<String> fields = type.getRequiredFieldsFlat(); if (fields != null) { for (String value : fields) { writeField(entry, out, value, indentation); written.add(value); } } // Then optional fields. fields = type.getOptionalFields(); if (fields != null) { for (String value : fields) { if (!written.contains(value)) { // If field appears both in req. and opt. don't repeat. writeField(entry, out, value, indentation); written.add(value); } } } // Then write remaining fields in alphabetic order. Set<String> remainingFields = new TreeSet<>(); for (String key : entry.getFieldNames()) { boolean writeIt = write ? InternalBibtexFields.isWriteableField(key) : InternalBibtexFields.isDisplayableField(key); if (!written.contains(key) && writeIt) { remainingFields.add(key); } } for (String field : remainingFields) { writeField(entry, out, field, indentation); } // Finally, end the entry. out.write('}'); } private void writeKeyField(BibEntry entry, Writer out) throws IOException { String keyField = StringUtil.shaveString(entry.getCiteKeyOptional().orElse("")); out.write(keyField + ',' + OS.NEWLINE); } /** * Write a single field, if it has any content. * * @param entry the entry to write * @param out the target of the write * @param name The field name * @throws IOException In case of an IO error */ private void writeField(BibEntry entry, Writer out, String name, int indentation) throws IOException { Optional<String> field = entry.getField(name); // only write field if is is not empty // field.ifPresent does not work as an IOException may be thrown if (field.isPresent() && !field.get().trim().isEmpty()) { out.write(" " + getFieldDisplayName(name, indentation)); try { out.write(fieldFormatter.format(field.get(), name)); out.write(',' + OS.NEWLINE); } catch (InvalidFieldValueException ex) { throw new IOException("Error in field '" + name + "': " + ex.getMessage()); } } } private int getLengthOfLongestFieldName(BibEntry entry) { Predicate<String> isNotBibtexKey = field -> !BibEntry.KEY_FIELD.equals(field); return entry.getFieldNames().stream().filter(isNotBibtexKey).mapToInt(String::length).max().orElse(0); } /** * Get display version of a entry field. * <p> * BibTeX is case-insensitive therefore there is no difference between: * howpublished, HOWPUBLISHED, HowPublished, etc. * <p> * The was a long discussion about how JabRef should write the fields. * See https://github.com/JabRef/jabref/issues/116 * <p> * The team decided to do the biblatex way and use lower case for the field names. * * @param field The name of the field. * @return The display version of the field name. */ private String getFieldDisplayName(String field, int intendation) { String actualField = field; if (actualField.isEmpty()) { // hard coded "UNKNOWN" is assigned to a field without any name actualField = "UNKNOWN"; } return actualField.toLowerCase(Locale.ROOT) + StringUtil.repeatSpaces(intendation - actualField.length()) + " = "; } }