package org.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 org.jabref.logic.importer.fileformat.bibtexml.Article;
import org.jabref.logic.importer.fileformat.bibtexml.Book;
import org.jabref.logic.importer.fileformat.bibtexml.Booklet;
import org.jabref.logic.importer.fileformat.bibtexml.Conference;
import org.jabref.logic.importer.fileformat.bibtexml.Entry;
import org.jabref.logic.importer.fileformat.bibtexml.File;
import org.jabref.logic.importer.fileformat.bibtexml.Inbook;
import org.jabref.logic.importer.fileformat.bibtexml.Incollection;
import org.jabref.logic.importer.fileformat.bibtexml.Inproceedings;
import org.jabref.logic.importer.fileformat.bibtexml.Manual;
import org.jabref.logic.importer.fileformat.bibtexml.Mastersthesis;
import org.jabref.logic.importer.fileformat.bibtexml.Misc;
import org.jabref.logic.importer.fileformat.bibtexml.Phdthesis;
import org.jabref.logic.importer.fileformat.bibtexml.Proceedings;
import org.jabref.logic.importer.fileformat.bibtexml.Techreport;
import org.jabref.logic.importer.fileformat.bibtexml.Unpublished;
import org.jabref.model.database.BibDatabaseContext;
import org.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());
}
}