package org.jabref.logic.exporter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.jabref.JabRefMain;
import org.jabref.logic.layout.Layout;
import org.jabref.logic.layout.LayoutFormatterPreferences;
import org.jabref.logic.layout.LayoutHelper;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Base class for export formats based on templates.
*/
public class ExportFormat implements IExportFormat {
private static final String LAYOUT_PREFIX = "/resource/layout/";
private static final Log LOGGER = LogFactory.getLog(ExportFormat.class);
private String displayName;
private String consoleName;
private String lfFileName;
private String directory;
private String extension;
private Charset encoding; // If this value is set, it will be used to override
// the default encoding for the getCurrentBasePanel.
private LayoutFormatterPreferences layoutPreferences;
private SavePreferences savePreferences;
private boolean customExport;
/**
* Initialize another export format based on templates stored in dir with
* layoutFile lfFilename.
*
* @param displayName Name to display to the user.
* @param consoleName Name to call this format in the console.
* @param lfFileName Name of the main layout file.
* @param directory Directory in which to find the layout file.
* @param extension Should contain the . (for instance .txt).
*/
public ExportFormat(String displayName, String consoleName, String lfFileName, String directory, String extension) {
this.displayName = displayName;
this.consoleName = consoleName;
this.lfFileName = lfFileName;
this.directory = directory;
this.extension = extension;
}
/**
* Initialize another export format based on templates stored in dir with
* layoutFile lfFilename.
*
* @param displayName Name to display to the user.
* @param consoleName Name to call this format in the console.
* @param lfFileName Name of the main layout file.
* @param directory Directory in which to find the layout file.
* @param extension Should contain the . (for instance .txt).
* @param layoutPreferences Preferences for layout
* @param savePreferences Preferences for saving
*/
public ExportFormat(String displayName, String consoleName, String lfFileName, String directory, String extension,
LayoutFormatterPreferences layoutPreferences, SavePreferences savePreferences) {
this(displayName, consoleName, lfFileName, directory, extension);
this.layoutPreferences = layoutPreferences;
this.savePreferences = savePreferences;
}
/**
* Empty default constructor for subclasses
*/
protected ExportFormat() {
// intentionally empty
}
/**
* Indicate whether this is a custom export. A custom export looks for its
* layout files using a normal file path, while a built-in export looks in
* the classpath.
*
* @param custom true to indicate a custom export format.
*/
public void setCustomExport(boolean custom) {
this.customExport = custom;
}
/**
* @see IExportFormat#getConsoleName()
*/
@Override
public String getConsoleName() {
return consoleName;
}
/**
* @see IExportFormat#getDisplayName()
*/
@Override
public String getDisplayName() {
return displayName;
}
/**
* Set an encoding which will be used in preference to the default value
* obtained from the basepanel.
*
* @param encoding The name of the encoding to use.
*/
public void setEncoding(Charset encoding) {
this.encoding = encoding;
}
/**
* @see IExportFormat#getExtension()
*/
@Override
public String getExtension() {
return extension;
}
/**
* This method should return a reader from which the given layout file can
* be read.
* <p>
* <p>
* Subclasses of ExportFormat are free to override and provide their own
* implementation.
*
* @param filename the filename
* @return a newly created reader
* @throws IOException if the reader could not be created
*/
private Reader getReader(String filename) throws IOException {
// If this is a custom export, just use the given filename:
String dir;
if (customExport) {
dir = "";
} else {
dir = LAYOUT_PREFIX + (directory == null ? "" : directory + '/');
}
// Attempt to get a Reader for the file path given, either by
// loading it as a resource (from within JAR), or as a normal file. If
// unsuccessful (e.g. file not found), an IOException is thrown.
String name = dir + filename;
Reader reader;
// Try loading as a resource first. This works for files inside the JAR:
URL reso = JabRefMain.class.getResource(name);
// If that did not work, try loading as a normal file URL:
try {
if (reso == null) {
File f = new File(name);
reader = new FileReader(f);
} else {
reader = new InputStreamReader(reso.openStream());
}
} catch (FileNotFoundException ex) {
throw new IOException("Cannot find layout file: '" + name + "'.");
}
return reader;
}
@Override
public void performExport(final BibDatabaseContext databaseContext, final String file,
final Charset encoding, List<BibEntry> entries) throws Exception {
Objects.requireNonNull(databaseContext);
Objects.requireNonNull(entries);
if (entries.isEmpty()) { // Do not export if no entries to export -- avoids exports with only template text
return;
}
Path outFile = Paths.get(file);
SaveSession ss = null;
if (this.encoding != null) {
try {
ss = new FileSaveSession(this.encoding, false);
} catch (SaveException ex) {
// Perhaps the overriding encoding doesn't work?
// We will fall back on the default encoding.
LOGGER.warn("Cannot get save session.", ex);
}
}
if (ss == null) {
ss = new FileSaveSession(encoding, false);
}
try (VerifyingWriter ps = ss.getWriter()) {
Layout beginLayout = null;
// Check if this export filter has bundled name formatters:
// Add these to the preferences, so all layouts have access to the custom name formatters:
readFormatterFile();
List<String> missingFormatters = new ArrayList<>(1);
// Print header
try (Reader reader = getReader(lfFileName + ".begin.layout")) {
LayoutHelper layoutHelper = new LayoutHelper(reader, layoutPreferences);
beginLayout = layoutHelper.getLayoutFromText();
} catch (IOException ex) {
// If an exception was cast, export filter doesn't have a begin
// file.
}
// Write the header
if (beginLayout != null) {
ps.write(beginLayout.doLayout(databaseContext, encoding));
missingFormatters.addAll(beginLayout.getMissingFormatters());
}
/*
* Write database entries; entries will be sorted as they appear on the
* screen, or sorted by author, depending on Preferences. We also supply
* the Set entries - if we are to export only certain entries, it will
* be non-null, and be used to choose entries. Otherwise, it will be
* null, and be ignored.
*/
List<BibEntry> sorted = BibDatabaseWriter.getSortedEntries(databaseContext, entries, savePreferences);
// Load default layout
Layout defLayout;
LayoutHelper layoutHelper;
try (Reader reader = getReader(lfFileName + ".layout")) {
layoutHelper = new LayoutHelper(reader,layoutPreferences);
defLayout = layoutHelper.getLayoutFromText();
}
if (defLayout != null) {
missingFormatters.addAll(defLayout.getMissingFormatters());
if (!missingFormatters.isEmpty()) {
LOGGER.warn(missingFormatters);
}
}
Map<String, Layout> layouts = new HashMap<>();
Layout layout;
ExportFormats.entryNumber = 0;
for (BibEntry entry : sorted) {
ExportFormats.entryNumber++; // Increment entry counter.
// Get the layout
String type = entry.getType();
if (layouts.containsKey(type)) {
layout = layouts.get(type);
} else {
try (Reader reader = getReader(lfFileName + '.' + type + ".layout")) {
// We try to get a type-specific layout for this entry.
layoutHelper = new LayoutHelper(reader, layoutPreferences);
layout = layoutHelper.getLayoutFromText();
layouts.put(type, layout);
if (layout != null) {
missingFormatters.addAll(layout.getMissingFormatters());
}
} catch (IOException ex) {
// The exception indicates that no type-specific layout
// exists, so we
// go with the default one.
layout = defLayout;
}
}
// Write the entry
if (layout != null) {
ps.write(layout.doLayout(entry, databaseContext.getDatabase()));
}
}
// Print footer
// changed section - begin (arudert)
Layout endLayout = null;
try (Reader reader = getReader(lfFileName + ".end.layout")) {
layoutHelper = new LayoutHelper(reader, layoutPreferences);
endLayout = layoutHelper.getLayoutFromText();
} catch (IOException ex) {
// If an exception was thrown, export filter doesn't have an end
// file.
}
// Write footer
if (endLayout != null) {
ps.write(endLayout.doLayout(databaseContext, this.encoding));
missingFormatters.addAll(endLayout.getMissingFormatters());
}
// Clear custom name formatters:
layoutPreferences.clearCustomExportNameFormatters();
if (!missingFormatters.isEmpty()) {
StringBuilder sb = new StringBuilder("The following formatters could not be found: ");
sb.append(String.join(", ", missingFormatters));
LOGGER.warn(sb);
}
finalizeSaveSession(ss, outFile);
}
}
@Override
public void performExport(final BibDatabaseContext databaseContext, Path file, final Charset encoding,
List<BibEntry> entries) throws Exception {
performExport(databaseContext, file.getFileName().toString(), encoding, entries);
}
/**
* See if there is a name formatter file bundled with this export format. If so, read
* all the name formatters so they can be used by the filter layouts.
*
*/
private void readFormatterFile() {
File formatterFile = new File(lfFileName + ".formatters");
if (formatterFile.exists()) {
try (Reader in = new FileReader(formatterFile)) {
// Ok, we found and opened the file. Read all contents:
StringBuilder sb = new StringBuilder();
int c;
while ((c = in.read()) != -1) {
sb.append((char) c);
}
String[] lines = sb.toString().split("\n");
// Go through each line:
for (String line1 : lines) {
String line = line1.trim();
// Do not deal with empty lines:
if (line.isEmpty()) {
continue;
}
int index = line.indexOf(':'); // TODO: any need to accept escaped colons here?
if ((index > 0) && ((index + 1) < line.length())) {
String formatterName = line.substring(0, index);
String contents = line.substring(index + 1);
layoutPreferences.putCustomExportNameFormatter(formatterName, contents);
}
}
} catch (IOException ex) {
// TODO: show error message here?
LOGGER.warn("Problem opening formatter file.", ex);
}
}
}
public void finalizeSaveSession(final SaveSession ss, Path file) throws SaveException, IOException {
ss.getWriter().flush();
ss.getWriter().close();
if (!ss.getWriter().couldEncodeAll()) {
LOGGER.warn("Could not encode...");
}
ss.commit(file);
}
}