package org.jabref.logic.importer.fileformat;
import java.io.BufferedReader;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.datatype.XMLGregorianCalendar;
import org.jabref.logic.importer.Importer;
import org.jabref.logic.importer.ParserResult;
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.util.FileExtensions;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.FieldName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Importer for the BibTeXML format.
* <p>
* check here for details on the format
* http://bibtexml.sourceforge.net/
*/
public class BibTeXMLImporter extends Importer {
private static final Log LOGGER = LogFactory.getLog(BibTeXMLImporter.class);
private static final Pattern START_PATTERN = Pattern.compile("<(bibtex:)?file .*");
private static final List<String> IGNORED_METHODS = Arrays.asList("getClass", "getAnnotate", "getContents",
"getPrice", "getSize", "getChapter");
@Override
public String getName() {
return "BibTeXML";
}
@Override
public FileExtensions getExtensions() {
return FileExtensions.BIBTEXML;
}
@Override
public String getDescription() {
return "Importer for the BibTeXML format.";
}
@Override
public boolean isRecognizedFormat(BufferedReader reader) throws IOException {
// Our strategy is to look for the "<bibtex:file *" line.
String str;
while ((str = reader.readLine()) != null) {
if (START_PATTERN.matcher(str).find()) {
return true;
}
}
return false;
}
@Override
public ParserResult importDatabase(BufferedReader reader) throws IOException {
Objects.requireNonNull(reader);
List<BibEntry> bibItems = new ArrayList<>();
try {
JAXBContext context = JAXBContext.newInstance("org.jabref.logic.importer.fileformat.bibtexml");
Unmarshaller unmarshaller = context.createUnmarshaller();
File file = (File) unmarshaller.unmarshal(reader);
List<Entry> entries = file.getEntry();
Map<String, String> fields = new HashMap<>();
for (Entry entry : entries) {
BibEntry bibEntry = new BibEntry();
if (entry.getArticle() != null) {
bibEntry.setType("article");
parse(entry.getArticle(), fields);
} else if (entry.getBook() != null) {
bibEntry.setType("book");
parse(entry.getBook(), fields);
} else if (entry.getBooklet() != null) {
bibEntry.setType("booklet");
parse(entry.getBooklet(), fields);
} else if (entry.getConference() != null) {
bibEntry.setType("conference");
parse(entry.getConference(), fields);
} else if (entry.getInbook() != null) {
bibEntry.setType("inbook");
parseInbook(entry.getInbook(), fields);
} else if (entry.getIncollection() != null) {
bibEntry.setType("incollection");
Incollection incollection = entry.getIncollection();
if (incollection.getChapter() != null) {
fields.put(FieldName.CHAPTER, String.valueOf(incollection.getChapter()));
}
parse(incollection, fields);
} else if (entry.getInproceedings() != null) {
bibEntry.setType("inproceedings");
parse(entry.getInproceedings(), fields);
} else if (entry.getManual() != null) {
bibEntry.setType("manual");
parse(entry.getManual(), fields);
} else if (entry.getMastersthesis() != null) {
bibEntry.setType("mastersthesis");
parse(entry.getMastersthesis(), fields);
} else if (entry.getMisc() != null) {
bibEntry.setType("misc");
parse(entry.getMisc(), fields);
} else if (entry.getPhdthesis() != null) {
bibEntry.setType("phdthesis");
parse(entry.getPhdthesis(), fields);
} else if (entry.getProceedings() != null) {
bibEntry.setType("proceedings");
parse(entry.getProceedings(), fields);
} else if (entry.getTechreport() != null) {
bibEntry.setType("techreport");
parse(entry.getTechreport(), fields);
} else if (entry.getUnpublished() != null) {
bibEntry.setType("unpublished");
parse(entry.getUnpublished(), fields);
}
if (entry.getId() != null) {
bibEntry.setCiteKey(entry.getId());
}
bibEntry.setField(fields);
bibItems.add(bibEntry);
}
} catch (JAXBException e) {
LOGGER.error("Error with XML parser configuration", e);
return ParserResult.fromError(e);
}
return new ParserResult(bibItems);
}
/**
* We use a generic method and not work on the real classes, because they all have the same behaviour. They call all get methods
* that are needed and use the return value. So this will prevent writing similar methods for every type.
* <p>
* In this method, all <Code>get</Code> methods that entryType has will be used and their value will be put to fields,
* if it is not null. So for example if entryType has the method <Code>getAbstract</Code>, then
* "abstract" will be put as key to fields and the value of <Code>getAbstract</Code> will be put as value to fields.
* Some <Code>get</Code> methods shouldn't be mapped to fields, so <Code>getClass</Code> for example will be skipped.
*
* @param entryType This can be all possible BibTeX types. It contains all fields of the entry and their values.
* @param fields A map where the name and the value of all fields that the entry contains will be put.
*/
private <T> void parse(T entryType, Map<String, String> fields) {
Method[] declaredMethods = entryType.getClass().getDeclaredMethods();
for (Method method : declaredMethods) {
try {
if (method.getName().equals("getYear")) {
putYear(fields, (XMLGregorianCalendar) method.invoke(entryType));
continue;
} else if (method.getName().equals("getNumber")) {
putNumber(fields, (BigInteger) method.invoke(entryType));
continue;
} else if (isMethodToIgnore(method.getName())) {
continue;
} else if (method.getName().startsWith("get")) {
putIfValueNotNull(fields, method.getName().replace("get", ""), (String) method.invoke(entryType));
}
} catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException e) {
LOGGER.error("Could not invoke method", e);
}
}
}
/**
* Returns whether the value of the given method name should be mapped or whether the method can be ignored.
*
* @param methodName The name of the method as String
* @return true if the method can be ignored, else false
*/
private boolean isMethodToIgnore(String methodName) {
return IGNORED_METHODS.contains(methodName);
}
/**
* Inbook needs a special Treatment, because <Code>inbook.getContent()</Code> returns a list of <Code>JAXBElements</Code>.
* The other types have just <Code>get</Code> methods, which return the values as Strings.
*/
private void parseInbook(Inbook inbook, Map<String, String> fields) {
List<JAXBElement<?>> content = inbook.getContent();
for (JAXBElement<?> element : content) {
String localName = element.getName().getLocalPart();
Object elementValue = element.getValue();
if (elementValue instanceof String) {
String value = (String) elementValue;
putIfValueNotNull(fields, localName, value);
} else if (elementValue instanceof BigInteger) {
BigInteger value = (BigInteger) elementValue;
if (FieldName.NUMBER.equals(localName)) {
fields.put(FieldName.NUMBER, String.valueOf(value));
} else if (FieldName.CHAPTER.equals(localName)) {
fields.put(FieldName.CHAPTER, String.valueOf(value));
}
} else if (elementValue instanceof XMLGregorianCalendar) {
XMLGregorianCalendar value = (XMLGregorianCalendar) elementValue;
if (FieldName.YEAR.equals(localName)) {
putYear(fields, value);
} else {
LOGGER.info("Unexpected field was found");
}
} else {
LOGGER.info("Unexpected field was found");
}
}
}
private void putYear(Map<String, String> fields, XMLGregorianCalendar year) {
if (year != null) {
fields.put(FieldName.YEAR, String.valueOf(year));
}
}
private void putNumber(Map<String, String> fields, BigInteger number) {
if (number != null) {
fields.put(FieldName.NUMBER, String.valueOf(number));
}
}
private void putIfValueNotNull(Map<String, String> fields, String key, String value) {
if (value != null) {
fields.put(key, value);
}
}
}