forked from JabRef/jabref
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rewrite Bibtexml exporter (JabRef#2017)
* rewrite exporter with jaxb parser * add test * fix codacy and some code refactorings * delete old layout files * adress comments
- Loading branch information
1 parent
7ce95d0
commit 371788c
Showing
42 changed files
with
799 additions
and
44 deletions.
There are no files selected for viewing
246 changes: 246 additions & 0 deletions
246
src/main/java/net/sf/jabref/logic/exporter/BibTeXMLExportFormat.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
89 changes: 89 additions & 0 deletions
89
src/test/java/net/sf/jabref/logic/exporter/BibTeXMLExporterTestFiles.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
Oops, something went wrong.