Skip to content

Commit

Permalink
Rewrite Bibtexml exporter (JabRef#2017)
Browse files Browse the repository at this point in the history
* rewrite exporter with jaxb parser

* add test

* fix codacy and some code refactorings

* delete old layout files

* adress comments
  • Loading branch information
tschechlovdev authored and zesaro committed Oct 27, 2016
1 parent 7ce95d0 commit 371788c
Show file tree
Hide file tree
Showing 42 changed files with 799 additions and 44 deletions.
246 changes: 246 additions & 0 deletions src/main/java/net/sf/jabref/logic/exporter/BibTeXMLExportFormat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package net.sf.jabref.logic.exporter;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;

import net.sf.jabref.logic.importer.fileformat.bibtexml.Article;
import net.sf.jabref.logic.importer.fileformat.bibtexml.Book;
import net.sf.jabref.logic.importer.fileformat.bibtexml.Booklet;
import net.sf.jabref.logic.importer.fileformat.bibtexml.Conference;
import net.sf.jabref.logic.importer.fileformat.bibtexml.Entry;
import net.sf.jabref.logic.importer.fileformat.bibtexml.File;
import net.sf.jabref.logic.importer.fileformat.bibtexml.Inbook;
import net.sf.jabref.logic.importer.fileformat.bibtexml.Incollection;
import net.sf.jabref.logic.importer.fileformat.bibtexml.Inproceedings;
import net.sf.jabref.logic.importer.fileformat.bibtexml.Manual;
import net.sf.jabref.logic.importer.fileformat.bibtexml.Mastersthesis;
import net.sf.jabref.logic.importer.fileformat.bibtexml.Misc;
import net.sf.jabref.logic.importer.fileformat.bibtexml.Phdthesis;
import net.sf.jabref.logic.importer.fileformat.bibtexml.Proceedings;
import net.sf.jabref.logic.importer.fileformat.bibtexml.Techreport;
import net.sf.jabref.logic.importer.fileformat.bibtexml.Unpublished;
import net.sf.jabref.model.database.BibDatabaseContext;
import net.sf.jabref.model.entry.BibEntry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
* Export format for the BibTeXML format.
*/
public class BibTeXMLExportFormat extends ExportFormat {

private static final String BIBTEXML_NAMESPACE_URI = "http://bibtexml.sf.net/";
private static final Locale ENGLISH = Locale.ENGLISH;
private static final Log LOGGER = LogFactory.getLog(BibTeXMLExportFormat.class);
private JAXBContext context;


public BibTeXMLExportFormat() {
super("BibTeXML", "bibtexml", null, null, ".xml");
}

@Override
public void performExport(final BibDatabaseContext databaseContext, final String resultFile, final Charset encoding,
List<BibEntry> entries) throws SaveException {
Objects.requireNonNull(databaseContext);
Objects.requireNonNull(entries);
if (entries.isEmpty()) { // Only export if entries exist
return;
}

File file = new File();
for (BibEntry bibEntry : entries) {
Entry entry = new Entry();

bibEntry.getCiteKeyOptional().ifPresent(citeKey -> entry.setId(citeKey));

String type = bibEntry.getType().toLowerCase(ENGLISH);
switch (type) {
case "article":
parse(new Article(), bibEntry, entry);
break;
case "book":
parse(new Book(), bibEntry, entry);
break;
case "booklet":
parse(new Booklet(), bibEntry, entry);
break;
case "conference":
parse(new Conference(), bibEntry, entry);
break;
case "inbook":
parseInbook(new Inbook(), bibEntry, entry);
break;
case "incollection":
parse(new Incollection(), bibEntry, entry);
break;
case "inproceedings":
parse(new Inproceedings(), bibEntry, entry);
break;
case "mastersthesis":
parse(new Mastersthesis(), bibEntry, entry);
break;
case "manual":
parse(new Manual(), bibEntry, entry);
break;
case "misc":
parse(new Misc(), bibEntry, entry);
break;
case "phdthesis":
parse(new Phdthesis(), bibEntry, entry);
break;
case "proceedings":
parse(new Proceedings(), bibEntry, entry);
break;
case "techreport":
parse(new Techreport(), bibEntry, entry);
break;
case "unpublished":
parse(new Unpublished(), bibEntry, entry);
break;
default:
LOGGER.warn("unexpected type appeared");
break;
}
file.getEntry().add(entry);
}
createMarshallerAndWriteToFile(file, resultFile);
}

private void createMarshallerAndWriteToFile(File file, String resultFile) throws SaveException {
try {
if (context == null) {
context = JAXBContext.newInstance(File.class);
}
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);

marshaller.marshal(file, new java.io.File(resultFile));
} catch (JAXBException e) {
throw new SaveException(e);
}
}

/**
* Contains same logic as the {@link parse()} method, but inbook needs a special treatment, because
* the contents of inbook are stored in a List of JAXBElements. So we first need to create
* a JAXBElement for every field and then add it to the content list.
*/
private void parseInbook(Inbook inbook, BibEntry bibEntry, Entry entry) {
Map<String, String> fieldMap = bibEntry.getFieldMap();
for (Map.Entry<String, String> entryField : fieldMap.entrySet()) {
String value = entryField.getValue();
String key = entryField.getKey();
if ("year".equals(key)) {
XMLGregorianCalendar calendar;
try {
calendar = DatatypeFactory.newInstance().newXMLGregorianCalendar(value);

JAXBElement<XMLGregorianCalendar> year = new JAXBElement<>(
new QName(BIBTEXML_NAMESPACE_URI, "year"), XMLGregorianCalendar.class, calendar);
inbook.getContent().add(year);
} catch (DatatypeConfigurationException e) {
LOGGER.error("A configuration error occured");
}
} else if ("number".equals(key)) {
JAXBElement<BigInteger> number = new JAXBElement<>(new QName(BIBTEXML_NAMESPACE_URI, "number"),
BigInteger.class, new BigInteger(value));
inbook.getContent().add(number);
} else {
JAXBElement<String> element = new JAXBElement<>(new QName(BIBTEXML_NAMESPACE_URI, key), String.class,
value);
inbook.getContent().add(element);
}
}

//set the entryType to the entry
entry.setInbook(inbook);
}

/**
* Generic method that gets an instance of an entry type (article, book, booklet ...). It also
* gets one bibEntry. Then the method checks all fields of the entry and then for all fields the method
* uses the set method of the entry type with the fieldname. So for example if a bib entry has the field
* author and the value for it is "Max Mustermann" and the given type is an article, then this method
* will invoke <Code>article.setAuthor("Max Mustermann")</Code>. <br>
* <br>
* The second part of this method is that the entry type will be set to the entry. So e.g., if the type is
* article then <Code>entry.setArticle(article)</Code> will be invoked.
*
* @param entryType The type parameterized type of the entry.
* @param bibEntry The bib entry, which fields will be set to the entryType.
* @param entry The bibtexml entry. The entryType will be set to this entry.
*/
private <T> void parse(T entryType, BibEntry bibEntry, Entry entry) {
List<Method> declaredSetMethods = getListOfSetMethods(entryType);
Map<String, String> fieldMap = bibEntry.getFieldMap();
for (Map.Entry<String, String> entryField : fieldMap.entrySet()) {
String value = entryField.getValue();
String key = entryField.getKey();
for (Method method : declaredSetMethods) {
String methodNameWithoutSet = method.getName().replace("set", "").toLowerCase(ENGLISH);
try {

if ("year".equals(key) && key.equals(methodNameWithoutSet)) {
try {

XMLGregorianCalendar calendar = DatatypeFactory.newInstance()
.newXMLGregorianCalendar(value);
method.invoke(entryType, calendar);
} catch (DatatypeConfigurationException e) {
LOGGER.error("A configuration error occured");
}
break;
} else if ("number".equals(key) && key.equals(methodNameWithoutSet)) {
method.invoke(entryType, new BigInteger(value));
break;
} else if (key.equals(methodNameWithoutSet)) {
method.invoke(entryType, value);
break;
}
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
LOGGER.error("Could not invoke method", e);
}
}

//set the entryType to the entry
List<Method> entryMethods = getListOfSetMethods(entry);
for (Method method : entryMethods) {
String methodWithoutSet = method.getName().replace("set", "");
String simpleClassName = entryType.getClass().getSimpleName();

if (methodWithoutSet.equals(simpleClassName)) {
try {
method.invoke(entry, entryType);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
LOGGER.warn("Could not set the type to the entry");
}
}
}
}
}

private <T> List<Method> getListOfSetMethods(T entryType) {
return Arrays.asList(entryType.getClass().getDeclaredMethods()).stream()
.filter(method -> method.getName().startsWith("set")).collect(Collectors.toList());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ public static void initAllExports(Map<String, ExportFormat> customFormats,
savePreferences));
ExportFormats.putFormat(new ExportFormat("DIN 1505", "din1505", "din1505winword", "din1505", ".rtf",
layoutPreferences, savePreferences));
ExportFormats.putFormat(
new ExportFormat("BibTeXML", "bibtexml", "bibtexml", null, ".xml", layoutPreferences, savePreferences));
ExportFormats.putFormat(
new ExportFormat("BibO RDF", "bibordf", "bibordf", null, ".rdf", layoutPreferences, savePreferences));
ExportFormats.putFormat(new ExportFormat(Localization.lang("HTML table"), "tablerefs", "tablerefs", "tablerefs",
Expand All @@ -56,6 +54,7 @@ public static void initAllExports(Map<String, ExportFormat> customFormats,
ExportFormats.putFormat(
new ExportFormat("MIS Quarterly", "misq", "misq", "misq", ".rtf", layoutPreferences, savePreferences));

ExportFormats.putFormat(new BibTeXMLExportFormat());
ExportFormats.putFormat(new OpenOfficeDocumentCreator());
ExportFormats.putFormat(new OpenDocumentSpreadsheetCreator());
ExportFormats.putFormat(new MSBibExportFormat());
Expand Down
5 changes: 0 additions & 5 deletions src/main/resources/resource/layout/bibtexml.begin.layout

This file was deleted.

1 change: 0 additions & 1 deletion src/main/resources/resource/layout/bibtexml.end.layout

This file was deleted.

36 changes: 0 additions & 36 deletions src/main/resources/resource/layout/bibtexml.layout

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package net.sf.jabref.logic.exporter;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import net.sf.jabref.logic.importer.fileformat.BibtexImporter;
import net.sf.jabref.model.database.BibDatabaseContext;
import net.sf.jabref.model.entry.BibEntry;
import net.sf.jabref.preferences.JabRefPreferences;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import org.xmlunit.builder.Input;
import org.xmlunit.builder.Input.Builder;
import org.xmlunit.diff.DefaultNodeMatcher;
import org.xmlunit.diff.ElementSelectors;
import org.xmlunit.matchers.CompareMatcher;


@RunWith(Parameterized.class)
public class BibTeXMLExporterTestFiles {

public BibDatabaseContext databaseContext;
public Charset charset;
public File tempFile;
public BibTeXMLExportFormat bibtexmlExportFormat;
public BibtexImporter testImporter;

@Parameter
public String filename;
public Path resourceDir;

@Rule
public TemporaryFolder testFolder = new TemporaryFolder();


@Parameters(name = "{0}")
public static Collection<String> fileNames() throws IOException, URISyntaxException {
try (Stream<Path> stream = Files.list(Paths.get(BibTeXMLExporterTestFiles.class.getResource("").toURI()))) {
return stream.map(n -> n.getFileName().toString()).filter(n -> n.endsWith(".bib"))
.filter(n -> n.startsWith("BibTeXML")).collect(Collectors.toList());
}
}

@Before
public void setUp() throws Exception {
resourceDir = Paths.get(BibTeXMLExporterTestFiles.class.getResource("").toURI());
databaseContext = new BibDatabaseContext();
charset = StandardCharsets.UTF_8;
bibtexmlExportFormat = new BibTeXMLExportFormat();
tempFile = testFolder.newFile();
testImporter = new BibtexImporter(JabRefPreferences.getInstance().getImportFormatPreferences());
}

@Test
public final void testPerformExport() throws IOException, SaveException {
String xmlFileName = filename.replace(".bib", ".xml");
Path importFile = resourceDir.resolve(filename);
String tempFilename = tempFile.getCanonicalPath();

List<BibEntry> entries = testImporter.importDatabase(importFile, StandardCharsets.UTF_8).getDatabase()
.getEntries();

bibtexmlExportFormat.performExport(databaseContext, tempFile.getPath(), charset, entries);

Builder control = Input.from(Files.newInputStream(resourceDir.resolve(xmlFileName)));
Builder test = Input.from(Files.newInputStream(Paths.get(tempFilename)));

Assert.assertThat(test, CompareMatcher.isSimilarTo(control)
.withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)).throwComparisonFailure());
}
}
Loading

0 comments on commit 371788c

Please sign in to comment.