diff --git a/AUTHORS b/AUTHORS index ac5ce01a291..ff698991516 100644 --- a/AUTHORS +++ b/AUTHORS @@ -13,6 +13,7 @@ Andreas Amann Andreas Rudert Behrouz Javanmardi Bernd Kalbfuss +Bernhard Tempel Brian Quistorff Brian Van Essen captain123 @@ -49,6 +50,7 @@ Hannes Restel Igor Chernyavsky Igor Steinmacher Ingvar Jackal +Jan Frederik Maas Jan Kubovy Janosch Kutscherauer Jason Pickering diff --git a/CHANGELOG.md b/CHANGELOG.md index b9c7a6132f2..648d2289e89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ to [sourceforge feature requests](https://sourceforge.net/p/jabref/features/) by - Feature: New LabelPattern `[keywordsN]`, where N is optional. Returns the first N keywords. If no N is specified ("`[keywords]`"), all keywords are returned. Spaces are removed. - Update supported LookAndFeels - Show replaced journal abbreviations on console + - Integrated [GVK-Plugin](http://www.gbv.de/wikis/cls/Jabref-GVK-Plugin) - The three options to manage file references are moved to their own separated group in the Tools menu. ### Fixed diff --git a/scripts/generate-authors.sh b/scripts/generate-authors.sh index 51396fcd634..ee0d51e7d7a 100755 --- a/scripts/generate-authors.sh +++ b/scripts/generate-authors.sh @@ -51,6 +51,7 @@ cd "$(dirname "$(readlink -f "$BASH_SOURCE")")/.." John Zedlewski Samin Muhammad Ridwanul Karim Stefan Robert + Bernhard Tempel EOF # %aN = author name, %aE = author email diff --git a/src/main/java/net/sf/jabref/bibtex/EntryTypes.java b/src/main/java/net/sf/jabref/bibtex/EntryTypes.java index 25a8c92b5fa..d071583e381 100644 --- a/src/main/java/net/sf/jabref/bibtex/EntryTypes.java +++ b/src/main/java/net/sf/jabref/bibtex/EntryTypes.java @@ -74,11 +74,7 @@ private static void initBibtexEntryTypes() { * or null if it does not exist. */ public static EntryType getType(String name) { - EntryType entryType = ALL_TYPES.get(name.toLowerCase()); - if (entryType == null) { - return null; - } - return entryType; + return ALL_TYPES.get(name.toLowerCase()); } /** @@ -86,12 +82,7 @@ public static EntryType getType(String name) { * name of a type, or null if it does not exist. */ public static EntryType getStandardType(String name) { - EntryType entryType = STANDARD_TYPES.get(name.toLowerCase()); - if (entryType == null) { - return null; - } else { - return entryType; - } + return STANDARD_TYPES.get(name.toLowerCase()); } public static void addOrModifyCustomEntryType(CustomEntryType type) { diff --git a/src/main/java/net/sf/jabref/importer/fetcher/EntryFetchers.java b/src/main/java/net/sf/jabref/importer/fetcher/EntryFetchers.java index 1b5d2410294..41f1ff05bdf 100644 --- a/src/main/java/net/sf/jabref/importer/fetcher/EntryFetchers.java +++ b/src/main/java/net/sf/jabref/importer/fetcher/EntryFetchers.java @@ -31,6 +31,7 @@ public EntryFetchers() { entryFetchers.add(new DBLPFetcher()); entryFetchers.add(new DiVAtoBibTeXFetcher()); entryFetchers.add(new DOItoBibTeXFetcher()); + entryFetchers.add(new GVKFetcher()); entryFetchers.add(new IEEEXploreFetcher()); entryFetchers.add(new INSPIREFetcher()); entryFetchers.add(new ISBNtoBibTeXFetcher()); diff --git a/src/main/java/net/sf/jabref/importer/fetcher/GVKFetcher.java b/src/main/java/net/sf/jabref/importer/fetcher/GVKFetcher.java new file mode 100644 index 00000000000..136a576afa7 --- /dev/null +++ b/src/main/java/net/sf/jabref/importer/fetcher/GVKFetcher.java @@ -0,0 +1,186 @@ +/** + * License: GPLv2, but Jan Frederik Maas agreed to change license upon request + */ +package net.sf.jabref.importer.fetcher; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import javax.swing.JPanel; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.xml.sax.SAXException; + +import net.sf.jabref.importer.ImportInspector; +import net.sf.jabref.importer.OutputPrinter; +import net.sf.jabref.logic.l10n.Localization; +import net.sf.jabref.model.entry.BibtexEntry; + +import java.net.URLEncoder; + +/** + * Fetch or search from GVK http://gso.gbv.de/sru/DB=2.1/ + */ +public class GVKFetcher implements EntryFetcher { + + private static final Log LOGGER = LogFactory.getLog(GVKFetcher.class); + + HashMap searchKeys = new HashMap<>(); + + + public GVKFetcher() { + searchKeys.put("all", "pica.all%3D"); + searchKeys.put("tit", "pica.tit%3D"); + searchKeys.put("per", "pica.per%3D"); + searchKeys.put("thm", "pica.thm%3D"); + searchKeys.put("slw", "pica.slw%3D"); + searchKeys.put("txt", "pica.txt%3D"); + searchKeys.put("num", "pica.num%3D"); + searchKeys.put("kon", "pica.kon%3D"); + searchKeys.put("ppn", "pica.ppn%3D"); + searchKeys.put("bkl", "pica.bkl%3D"); + searchKeys.put("erj", "pica.erj%3D"); + } + + /** + * Necessary for JabRef + */ + @Override + public void stopFetching() { + // not supported + } + + @Override + public String getHelpPage() { + return "GVKHelp.html"; + } + + @Override + public JPanel getOptionsPanel() { + return null; + } + + @Override + public String getTitle() { + return "GVK (Gemeinsamer Verbundkatalog)"; + } + + @Override + public boolean processQuery(String query, ImportInspector dialog, OutputPrinter frame) { + String gvkQuery = ""; + + query = query.trim(); + + String[] qterms = query.split("\\s"); + + // Null abfangen! + if (qterms.length == 0) { + return false; + } + + // Jeden einzelnen Suchbegriff URL-Encodieren + for (int x = 0; x < qterms.length; x++) { + try { + qterms[x] = URLEncoder.encode(qterms[x], "UTF-8"); + } catch (UnsupportedEncodingException e) { + LOGGER.error("Unsupported encoding", e); + } + } + + if (searchKeys.containsKey(qterms[0])) { + gvkQuery = processComplexQuery(qterms); + } else { + gvkQuery = "pica.all%3D"; + gvkQuery = gvkQuery.concat(qterms[0]); + + for (int x = 1; x < qterms.length; x++) { + gvkQuery = gvkQuery.concat("%20"); + gvkQuery = gvkQuery.concat(qterms[x]); + } + } + + List bibs = fetchGVK(gvkQuery); + + for (BibtexEntry entry : bibs) { + dialog.addEntry(entry); + } + + if (bibs.size() == 0) { + frame.showMessage(Localization.lang("No references found")); + } + + return true; + } + + private String processComplexQuery(String[] s) { + String result = ""; + boolean lastWasKey = false; + + for (int x = 0; x < s.length; x++) { + if (searchKeys.containsKey(s[x])) { + if (!(x == 0)) { + result = result.concat("%20and%20" + searchKeys.get(s[x])); + } else { + result = searchKeys.get(s[x]); + } + lastWasKey = true; + } else { + if (!lastWasKey) { + result = result.concat("%20"); + } + String encoded = s[x]; + encoded = encoded.replaceAll(",", "%2C"); + encoded = encoded.replaceAll("\\?", "%3F"); + + result = result.concat(encoded); + lastWasKey = false; + } + } + return (result); + } + + private List fetchGVK(String query) { + List result; + + String urlPrefix = "http://sru.gbv.de/gvk?version=1.1&operation=searchRetrieve&query="; + String urlQuery = query; + String urlSuffix = "&maximumRecords=50&recordSchema=picaxml&sortKeys=Year%2C%2C1"; + + String searchstring = (urlPrefix + urlQuery + urlSuffix); + LOGGER.debug(searchstring); + try { + URI uri = null; + try { + uri = new URI(searchstring); + } catch (URISyntaxException e) { + LOGGER.error("URI malformed error", e); + return Collections.EMPTY_LIST; + } + URL url = uri.toURL(); + try (InputStream is = url.openStream()) { + result = (new GVKParser()).parseEntries(is); + } + } catch (IOException e) { + LOGGER.error("GVK plugin: An I/O exception occurred", e); + return Collections.EMPTY_LIST; + } catch (ParserConfigurationException e) { + LOGGER.error("GVK plugin: An internal parser error occurred", e); + return Collections.EMPTY_LIST; + } catch (SAXException e) { + LOGGER.error("An internal parser error occurred", e); + return Collections.EMPTY_LIST; + } + + return result; + } + +} diff --git a/src/main/java/net/sf/jabref/importer/fetcher/GVKParser.java b/src/main/java/net/sf/jabref/importer/fetcher/GVKParser.java new file mode 100644 index 00000000000..7d2d829d852 --- /dev/null +++ b/src/main/java/net/sf/jabref/importer/fetcher/GVKParser.java @@ -0,0 +1,502 @@ +/** + * License: GPLv2, but Jan Frederik Maas agreed to change license upon request + */ +package net.sf.jabref.importer.fetcher; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import net.sf.jabref.bibtex.EntryTypes; +import net.sf.jabref.importer.ImportFormatReader; +import net.sf.jabref.model.entry.BibtexEntry; +import net.sf.jabref.model.entry.IdGenerator; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import com.google.common.base.Strings; + +public class GVKParser { + + private static final Log LOGGER = LogFactory.getLog(GVKParser.class); + + + public List parseEntries(InputStream is) + throws ParserConfigurationException, SAXException, IOException { + DocumentBuilder dbuild = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document content = dbuild.parse(is); + return this.parseEntries(content); + } + + public List parseEntries(Document content) { + List result = new LinkedList<>(); + + // used for creating test cases + // XMLUtil.printDocument(content); + + // Namespace srwNamespace = Namespace.getNamespace("srw","http://www.loc.gov/zing/srw/"); + + // Schleife ueber allen Teilergebnissen + //Element root = content.getDocumentElement(); + Element root = (Element) content.getElementsByTagName("zs:searchRetrieveResponse").item(0); + Element srwrecords = getChild("zs:records", root); + if (srwrecords == null) { + // no records found -> return empty list + return result; + } + List records = getChildren("zs:record", srwrecords); + for (Element record : records) { + Element e = getChild("zs:recordData", record); + e = getChild("record", e); + result.add(parseEntry(e)); + } + return result; + } + + private BibtexEntry parseEntry(Element e) { + String author = null; + String editor = null; + String title = null; + String publisher = null; + String date = null; + String address = null; + String series = null; + String edition = null; + String isbn = null; + String issn = null; + String number = null; + String pagetotal = null; + String volume = null; + String pages = null; + String journal = null; + String ppn = null; + String booktitle = null; + String url = null; + String note = null; + + String quelle = ""; + String mak = ""; + String subtitle = ""; + + String entryType = "book"; // Default + + // Alle relevanten Informationen einsammeln + + List datafields = getChildren("datafield", e); + Iterator iter = datafields.iterator(); + while (iter.hasNext()) { + Element datafield = iter.next(); + + String tag = datafield.getAttribute("tag"); + LOGGER.debug("tag: " + tag); + + // mak + if (tag.equals("002@")) { + mak = getSubfield("0", datafield); + } + + //ppn + if (tag.equals("003@")) { + ppn = getSubfield("0", datafield); + } + + //author + if (tag.equals("028A")) { + String vorname = getSubfield("d", datafield); + String nachname = getSubfield("a", datafield); + + if (author != null) { + author = author.concat(" and "); + } else { + author = ""; + } + author = author.concat(vorname + " " + nachname); + } + //author (weiterer) + if (tag.equals("028B")) { + String vorname = getSubfield("d", datafield); + String nachname = getSubfield("a", datafield); + + if (author != null) { + author = author.concat(" and "); + } else { + author = ""; + } + author = author.concat(vorname + " " + nachname); + } + + //editor + if (tag.equals("028C")) { + String vorname = getSubfield("d", datafield); + String nachname = getSubfield("a", datafield); + + if (editor != null) { + editor = editor.concat(" and "); + } else { + editor = ""; + } + editor = editor.concat(vorname + " " + nachname); + } + + //title and subtitle + if (tag.equals("021A")) { + title = getSubfield("a", datafield); + subtitle = getSubfield("d", datafield); + } + + //publisher and address + if (tag.equals("033A")) { + publisher = getSubfield("n", datafield); + address = getSubfield("p", datafield); + } + + //date + if (tag.equals("011@")) { + date = getSubfield("a", datafield); + } + + //date, volume, number, pages (year bei Zeitschriften (evtl. redundant mit 011@)) + if (tag.equals("031A")) { + date = getSubfield("j", datafield); + volume = getSubfield("e", datafield); + number = getSubfield("a", datafield); + pages = getSubfield("h", datafield); + + } + + // 036D seems to contain more information than the other fields + // overwrite information using that field + // 036D also contains information normally found in 036E + if (tag.equals("036D")) { + // 021 might have been present + if (title != null) { + // convert old title (contained in "a" of 021A) to volume + if (title.startsWith("@")) { + // "@" indicates a number + title = title.substring(1); + } else { + // we nevertheless keep the old title data + } + number = title; + } + //title and subtitle + title = getSubfield("a", datafield); + subtitle = getSubfield("d", datafield); + volume = getSubfield("l", datafield); + } + + //series and number + if (tag.equals("036E")) { + series = getSubfield("a", datafield); + number = getSubfield("l", datafield); + String kor = getSubfield("b", datafield); + + if (kor != null) { + series = series + " / " + kor; + } + } + + //note + if (tag.equals("037A")) { + note = getSubfield("a", datafield); + } + + //edition + if (tag.equals("032@")) { + edition = getSubfield("a", datafield); + } + + //isbn + if (tag.equals("004A")) { + String isbn_10 = getSubfield("0", datafield); + String isbn_13 = getSubfield("A", datafield); + + if (isbn_10 != null) { + isbn = isbn_10; + } + + if (isbn_13 != null) { + isbn = isbn_13; + } + + } + + // Hochschulschriftenvermerk + // Bei einer Verlagsdissertation ist der Ort schon eingetragen + if (tag.equals("037C")) { + if (address == null) { + address = getSubfield("b", datafield); + address = removeSortCharacters(address); + } + + String st = getSubfield("a", datafield); + if (st != null) { + if (st.contains("Diss")) { + entryType = "phdthesis"; + } + } + } + + //journal oder booktitle + + /* Problematiken hier: Sowohl für Artikel in + * Zeitschriften als für Beiträge in Büchern + * wird 027D verwendet. Der Titel muß je nach + * Fall booktitle oder journal zugeordnet + * werden. Auch bei Zeitschriften werden hier + * ggf. Verlag und Ort angegeben (sind dann + * eigentlich überflüssig), während bei + * Buchbeiträgen Verlag und Ort wichtig sind + * (sonst in Kategorie 033A). + */ + if (tag.equals("027D")) { + journal = getSubfield("a", datafield); + booktitle = getSubfield("a", datafield); + address = getSubfield("p", datafield); + publisher = getSubfield("n", datafield); + } + + //pagetotal + if (tag.equals("034D")) { + pagetotal = getSubfield("a", datafield); + + // S, S. etc. entfernen + pagetotal = pagetotal.replaceAll(" S\\.?$", ""); + } + + // Behandlung von Konferenzen + if (tag.equals("030F")) { + address = getSubfield("k", datafield); + + if (!entryType.equals("proceedings")) { + subtitle = getSubfield("a", datafield); + } + + entryType = "proceedings"; + } + + // Wenn eine Verlagsdiss vorliegt + if (entryType.equals("phdthesis")) { + if (isbn != null) { + entryType = "book"; + } + } + + //Hilfskategorien zur Entscheidung @article + //oder @incollection; hier könnte man auch die + //ISBN herausparsen als Erleichterung für das + //Auffinden der Quelle, die über die + //SRU-Schnittstelle gelieferten Daten zur + //Quelle unvollständig sind (z.B. nicht Serie + //und Nummer angegeben werden) + if (tag.equals("039B")) { + quelle = getSubfield("8", datafield); + } + if (tag.equals("046R")) { + if (quelle.equals("") || (quelle == null)) { + quelle = getSubfield("a", datafield); + } + } + + // URLs behandeln + if (tag.equals("009P")) { + if (datafield.getAttribute("occurrence").equals("03") + || datafield.getAttribute("occurrence").equals("05")) { + if (url == null) { + url = getSubfield("a", datafield); + } + } + } + } + + // Abfangen von Nulleintraegen + if (quelle == null) { + quelle = ""; + } + + // Nichtsortierzeichen entfernen + if (author != null) { + author = removeSortCharacters(author); + } + if (editor != null) { + editor = removeSortCharacters(editor); + } + if (title != null) { + title = removeSortCharacters(title); + } + if (subtitle != null) { + subtitle = removeSortCharacters(subtitle); + } + + // Dokumenttyp bestimmen und Eintrag anlegen + + if (mak.startsWith("As")) { + entryType = "misc"; + + if (quelle.contains("ISBN")) { + entryType = "incollection"; + } + if (quelle.contains("ZDB-ID")) { + entryType = "article"; + } + } else if (mak.equals("")) { + entryType = "misc"; + } else if (mak.startsWith("O")) { + entryType = "online"; + } + + /* + * Wahrscheinlichkeit, dass ZDB-ID + * vorhanden ist, ist größer als ISBN bei + * Buchbeiträgen. Daher bei As?-Sätzen am besten immer + * dann @incollection annehmen, wenn weder ISBN noch + * ZDB-ID vorhanden sind. + */ + BibtexEntry result = new BibtexEntry(IdGenerator.next(), EntryTypes.getType(entryType)); + + // Zuordnung der Felder in Abhängigkeit vom Dokumenttyp + if (author != null) { + result.setField("author", ImportFormatReader.expandAuthorInitials(author)); + } + if (editor != null) { + result.setField("editor", ImportFormatReader.expandAuthorInitials(editor)); + } + if (title != null) { + result.setField("title", title); + } + if (!Strings.isNullOrEmpty(subtitle)) { + // ensure that first letter is an upper case letter + // there could be the edge case that the string is only one character long, therefore, this special treatment + // this is apache commons lang StringUtils.capitalize (https://commons.apache.org/proper/commons-lang/javadocs/api-release/org/apache/commons/lang3/StringUtils.html#capitalize%28java.lang.String%29), but we don't want to add an additional dependency ('org.apache.commons:commons-lang3:3.4') + String newSubtitle = Character.toString(Character.toUpperCase(subtitle.charAt(0))); + if (subtitle.length() > 1) { + newSubtitle += subtitle.substring(1); + } + result.setField("subtitle", newSubtitle); + } + if (publisher != null) { + result.setField("publisher", publisher); + } + if (date != null) { + result.setField("date", date); + } + if (address != null) { + result.setField("address", address); + } + if (series != null) { + result.setField("series", series); + } + if (edition != null) { + result.setField("edition", edition); + } + if (isbn != null) { + result.setField("isbn", isbn); + } + if (issn != null) { + result.setField("issn", issn); + } + if (number != null) { + result.setField("number", number); + } + if (pagetotal != null) { + result.setField("pagetotal", pagetotal); + } + if (pages != null) { + result.setField("pages", pages); + } + if (volume != null) { + result.setField("volume", volume); + } + if (journal != null) { + result.setField("journal", journal); + } + if (ppn != null) { + result.setField("ppn_GVK", ppn); + } + if (url != null) { + result.setField("url", url); + } + if (note != null) { + result.setField("note", note); + } + + if (entryType.equals("article")) { + if (journal != null) { + result.setField("journal", journal); + } + } else if (entryType.equals("incollection")) { + if (booktitle != null) { + result.setField("booktitle", booktitle); + } + } + + return result; + } + + private String getSubfield(String a, Element datafield) { + List liste = getChildren("subfield", datafield); + Iterator iter = liste.iterator(); + + while (iter.hasNext()) { + Element subfield = iter.next(); + if (subfield.getAttribute("code").equals(a)) { + return (subfield.getTextContent()); + } + } + return null; + } + + private Element getChild(String name, Element e) { + Element result = null; + + NodeList children = e.getChildNodes(); + + int j = children.getLength(); + for (int i = 0; i < j; i++) { + Node test = children.item(i); + if (test.getNodeType() == Node.ELEMENT_NODE) { + Element entry = (Element) test; + if (entry.getTagName().equals(name)) { + return entry; + } + } + } + return result; + } + + private List getChildren(String name, Element e) { + List result = new LinkedList<>(); + NodeList children = e.getChildNodes(); + + int j = children.getLength(); + for (int i = 0; i < j; i++) { + Node test = children.item(i); + if (test.getNodeType() == Node.ELEMENT_NODE) { + Element entry = (Element) test; + if (entry.getTagName().equals(name)) { + result.add(entry); + } + } + } + + return result; + } + + private String removeSortCharacters(String input) { + input = input.replaceAll("\\@", ""); + return input; + } + +} diff --git a/src/main/java/net/sf/jabref/logic/util/io/XMLUtil.java b/src/main/java/net/sf/jabref/logic/util/io/XMLUtil.java new file mode 100644 index 00000000000..5ed3d3e198d --- /dev/null +++ b/src/main/java/net/sf/jabref/logic/util/io/XMLUtil.java @@ -0,0 +1,43 @@ +package net.sf.jabref.logic.util.io; + +import java.io.StringWriter; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Document; + +/** + * Currently used for debugging only + */ +public class XMLUtil { + + private static final Log LOGGER = LogFactory.getLog(XMLUtil.class); + + + /** + * Prints out the document to standard out. Used to generate files for test cases. + */ + public static void printDocument(Document doc) { + // code by http://stackoverflow.com/a/10356322/873282 + try { + DOMSource domSource = new DOMSource(doc); + StringWriter writer = new StringWriter(); + StreamResult result = new StreamResult(writer); + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.transform(domSource, result); + System.out.println(writer.toString()); + } catch (TransformerException ex) { + LOGGER.error("", ex); + } + } + +} diff --git a/src/main/java/net/sf/jabref/model/entry/BibtexEntry.java b/src/main/java/net/sf/jabref/model/entry/BibtexEntry.java index 13254d24eae..7f7fc5e20fd 100644 --- a/src/main/java/net/sf/jabref/model/entry/BibtexEntry.java +++ b/src/main/java/net/sf/jabref/model/entry/BibtexEntry.java @@ -35,6 +35,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import com.google.common.base.Strings; + +import net.sf.jabref.bibtex.EntryTypes; import net.sf.jabref.model.database.BibtexDatabase; public class BibtexEntry { @@ -63,7 +66,7 @@ public BibtexEntry() { } public BibtexEntry(String id) { - this(id, BibtexEntryTypes.MISC); + this(id, EntryTypes.getBibtexEntryType("misc")); } public BibtexEntry(String id, EntryType type) { @@ -190,7 +193,7 @@ public String getField(String name) { public String getFieldOrAlias(String name) { String fieldValue = getField(name); - if ((fieldValue != null) && !fieldValue.isEmpty()) { + if (!Strings.isNullOrEmpty(fieldValue)) { return fieldValue; } @@ -280,10 +283,7 @@ public String getCiteKey() { } public boolean hasCiteKey() { - if ((getCiteKey() == null) || getCiteKey().isEmpty()) { - return false; - } - return true; + return !Strings.isNullOrEmpty(getCiteKey()); } /** @@ -421,9 +421,15 @@ public Object clone() { return clone; } + /** + * This returns a canonical BibTeX serialization. Special characters such as "{" or "&" are NOT escaped, but written + * as is + * + * Serializes all fields, even the JabRef internal ones. Does NOT serialize "KEY_FIELD" as field, but as key + */ @Override public String toString() { - return getType().getName() + ':' + getCiteKey(); + return CanonicalBibtexEntry.getCanonicalRepresentation(this); } public boolean isSearchHit() { diff --git a/src/main/java/net/sf/jabref/model/entry/CanonicalBibtexEntry.java b/src/main/java/net/sf/jabref/model/entry/CanonicalBibtexEntry.java new file mode 100644 index 00000000000..05db26a80bd --- /dev/null +++ b/src/main/java/net/sf/jabref/model/entry/CanonicalBibtexEntry.java @@ -0,0 +1,54 @@ +package net.sf.jabref.model.entry; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.SortedSet; +import java.util.StringJoiner; +import java.util.TreeSet; + +import com.google.common.base.Strings; + +public class CanonicalBibtexEntry { + + /** + * This returns a canonical BibTeX serialization. Special characters such as "{" or "&" are NOT escaped, but written + * as is + * + * Serializes all fields, even the JabRef internal ones. Does NOT serialize "KEY_FIELD" as field, but as key + */ + public static String getCanonicalRepresentation(BibtexEntry e) { + StringBuilder sb = new StringBuilder(); + + // generate first line: type and bibtex key + String citeKey = Strings.nullToEmpty(e.getCiteKey()); + sb.append(String.format("@%s{%s,\n", e.getType().getName().toLowerCase(Locale.US), citeKey)); + + // we have to introduce a new Map as fields are stored case-sensitive in JabRef (see https://github.com/koppor/jabref/issues/45). + Map mapFieldToValue = new HashMap<>(); + + // determine sorted fields -- all fields lower case + SortedSet sortedFields = new TreeSet<>(); + for (String fieldName : e.getFieldNames()) { + // JabRef stores the key in the field KEY_FIELD, which must not be serialized + if (!fieldName.equals(BibtexEntry.KEY_FIELD)) { + String lowerCaseFieldName = fieldName.toLowerCase(Locale.US); + sortedFields.add(lowerCaseFieldName); + mapFieldToValue.put(lowerCaseFieldName, e.getField(fieldName)); + } + } + + // generate field entries + StringJoiner sj = new StringJoiner(",\n", "", "\n"); + for (String fieldName : sortedFields) { + String line = String.format(" %s = {%s}", fieldName, mapFieldToValue.get(fieldName)); + sj.add(line); + } + sb.append(sj.toString()); + + // append the closing entry bracket + sb.append("}"); + return sb.toString(); + } + +} diff --git a/src/main/resources/help/en/GVKHelp.html b/src/main/resources/help/en/GVKHelp.html new file mode 100644 index 00000000000..e7e9fb1015a --- /dev/null +++ b/src/main/resources/help/en/GVKHelp.html @@ -0,0 +1,45 @@ + + + + + + +

Search the GVK (Common Union Catalogue)

+ +

The GVK search capability can be used to query the GVK (Common Union Catalogue) and automatically import references into JabRef.

+ +

To use this feature, choose Search -> Web search, and the search + interface will appear in the side pane. Select GVKFetcher in the dropdown menu.

+ +

You can simply enter words / names / years you want to search for, or you can specify search keys.

+ +

Supported keywords are

+
    +
  • all - all words. Not specifing a search key results in an "all" search
  • +
  • tit - title words
  • +
  • per - authors, editors, etc.
  • +
  • thm - topics
  • +
  • slw - key words
  • +
  • txt - tables of content
  • +
  • num - numbers, e.g. ISBN
  • +
  • kon - names of conferences
  • +
  • ppn - Pica Production Numbers of the GVK
  • +
  • bkl - Basisklassifikation-numbers
  • +
  • erj - year of publication
  • +
+ +

Notes

+
    +
  • queries can be combined with "and". The use of "and" is optional, though.
  • +
  • in many cases you can use the truncation sign "?"
  • +
  • spaces in person names are not supported - I am working on it. Please use the truncation sign ? after the first name for several given names. E.g. "per Maas,jan?"
  • +
+ +

Sample queries

+
    +
  • "marx kapital"
  • +
  • "per grodke and tit db2"
  • +
  • "per Maas,jan?"
  • +
+ + diff --git a/src/test/java/net/sf/jabref/bibtex/BibtexEntryAssert.java b/src/test/java/net/sf/jabref/bibtex/BibtexEntryAssert.java new file mode 100644 index 00000000000..6993e1eaf99 --- /dev/null +++ b/src/test/java/net/sf/jabref/bibtex/BibtexEntryAssert.java @@ -0,0 +1,69 @@ +package net.sf.jabref.bibtex; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; + +import org.junit.Assert; + +import net.sf.jabref.importer.ParserResult; +import net.sf.jabref.importer.fetcher.GVKParser; +import net.sf.jabref.importer.fileformat.BibtexParser; +import net.sf.jabref.model.entry.BibtexEntry; +import net.sf.jabref.model.entry.CanonicalBibtexEntry; + +public class BibtexEntryAssert { + + /** + * Reads a single entry from the resource using `getResourceAsStream` from the given class. The resource has to + * contain a single entry + * + * @param clazz the class where to call `getResourceAsStream` + * @param resourceName the resource to read + * @param entry the entry to compare with + */ + public static void assertEquals(Class clazz, String resourceName, BibtexEntry entry) + throws IOException { + Assert.assertNotNull(clazz); + Assert.assertNotNull(resourceName); + Assert.assertNotNull(entry); + try (InputStream shouldBeIs = clazz.getResourceAsStream(resourceName)) { + BibtexEntryAssert.assertEquals(shouldBeIs, entry); + } + + } + + /** + * Reads a bibtex database from the given InputStream. The result has to contain a single BibtexEntry. This entry is + * compared to the given entry + * + * @param shouldBeIs the inputStream reading the entry from + * @param entry the entry to compare with + */ + public static void assertEquals(InputStream shouldBeIs, BibtexEntry entry) + throws UnsupportedEncodingException, IOException { + Assert.assertNotNull(shouldBeIs); + Assert.assertNotNull(entry); + ParserResult result; + try (Reader reader = new InputStreamReader(shouldBeIs, "UTF-8")) { + BibtexParser parser = new BibtexParser(reader); + result = parser.parse(); + } + Assert.assertNotNull(result); + Assert.assertNotEquals(ParserResult.INVALID_FORMAT, result); + Assert.assertEquals(1, result.getDatabase().getEntryCount()); + BibtexEntry shouldBeEntry = result.getDatabase().getEntries().iterator().next(); + assertEquals(shouldBeEntry, entry); + } + + /** + * Compares to BibTeX entries using their canonical representation + */ + private static void assertEquals(BibtexEntry shouldBeEntry, BibtexEntry entry) { + // use the canonical string representation to compare the entries + Assert.assertEquals(CanonicalBibtexEntry.getCanonicalRepresentation(shouldBeEntry), + CanonicalBibtexEntry.getCanonicalRepresentation(entry)); + } +} diff --git a/src/test/java/net/sf/jabref/importer/fetcher/GVKParserTest.java b/src/test/java/net/sf/jabref/importer/fetcher/GVKParserTest.java new file mode 100644 index 00000000000..b10376e0484 --- /dev/null +++ b/src/test/java/net/sf/jabref/importer/fetcher/GVKParserTest.java @@ -0,0 +1,74 @@ +package net.sf.jabref.importer.fetcher; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.xml.parsers.ParserConfigurationException; + +import org.junit.Assert; +import org.junit.Test; +import org.xml.sax.SAXException; + +import net.sf.jabref.bibtex.BibtexEntryAssert; +import net.sf.jabref.model.entry.BibtexEntry; + +public class GVKParserTest { + + private void doTest(String xmlName, int expectedSize, List resourceNames) + throws ParserConfigurationException, SAXException, IOException { + try (InputStream is = GVKParser.class.getResourceAsStream(xmlName)) { + GVKParser parser = new GVKParser(); + List entries = parser.parseEntries(is); + Assert.assertNotNull(entries); + Assert.assertEquals(expectedSize, entries.size()); + int i = 0; + for (String resourceName : resourceNames) { + BibtexEntryAssert.assertEquals(GVKParser.class, resourceName, entries.get(i)); + i++; + } + } + } + + @Test + public void emptyResult() throws Exception { + doTest("gvk_empty_result_becaue_of_bad_query.xml", 0, Collections.EMPTY_LIST); + } + + @Test + public void resultFor797485368() throws Exception { + doTest("gvk_result_for_797485368.xml", 1, Arrays.asList(new String[] {"gvk_result_for_797485368.bib"})); + } + + @Test + public void GMP() throws Exception { + doTest("gvk_gmp.xml", 2, Arrays.asList(new String[] {"gvk_gmp.1.bib", "gvk_gmp.2.bib"})); + } + + @Test + public void subTitleTest() throws Exception { + try (InputStream is = GVKParser.class.getResourceAsStream("gvk_artificial_subtitle_test.xml")) { + GVKParser parser = new GVKParser(); + List entries = parser.parseEntries(is); + Assert.assertNotNull(entries); + Assert.assertEquals(5, entries.size()); + + BibtexEntry entry = entries.get(0); + Assert.assertEquals(null, entry.getField("subtitle")); + + entry = entries.get(1); + Assert.assertEquals("C", entry.getField("subtitle")); + + entry = entries.get(2); + Assert.assertEquals("Word", entry.getField("subtitle")); + + entry = entries.get(3); + Assert.assertEquals("Word1 word2", entry.getField("subtitle")); + + entry = entries.get(4); + Assert.assertEquals("Word1 word2", entry.getField("subtitle")); + } + } +} diff --git a/src/test/java/net/sf/jabref/model/entry/BibtexEntryTests.java b/src/test/java/net/sf/jabref/model/entry/BibtexEntryTests.java index ae9b92d688b..7ec9813f74a 100644 --- a/src/test/java/net/sf/jabref/model/entry/BibtexEntryTests.java +++ b/src/test/java/net/sf/jabref/model/entry/BibtexEntryTests.java @@ -7,6 +7,7 @@ import net.sf.jabref.Globals; import net.sf.jabref.JabRefPreferences; +import net.sf.jabref.bibtex.EntryTypes; import net.sf.jabref.importer.fileformat.BibtexParser; import java.util.ArrayList; @@ -21,6 +22,10 @@ public void setup() { @Test public void testDefaultConstructor() { BibtexEntry entry = new BibtexEntry(); + // we have to use `getType("misc")` in the case of biblatex mode + Assert.assertEquals(EntryTypes.getType("misc"), entry.getType()); + Assert.assertNotNull(entry.getId()); + Assert.assertNull(entry.getField("author")); } @Test @@ -90,4 +95,19 @@ public void hasAllRequiredFields() { e.setField("year", "2015"); Assert.assertTrue(e.hasAllRequiredFields(null)); } + + @Test + public void isNullOrEmptyCiteKey() { + BibtexEntry e = new BibtexEntry("id", BibtexEntryTypes.ARTICLE); + Assert.assertFalse(e.hasCiteKey()); + e.setField(BibtexEntry.KEY_FIELD, ""); + Assert.assertFalse(e.hasCiteKey()); + e.setField(BibtexEntry.KEY_FIELD, null); + Assert.assertFalse(e.hasCiteKey()); + e.setField(BibtexEntry.KEY_FIELD, "key"); + Assert.assertTrue(e.hasCiteKey()); + e.clearField(BibtexEntry.KEY_FIELD); + Assert.assertFalse(e.hasCiteKey()); + } + } diff --git a/src/test/java/net/sf/jabref/model/entry/CanonicalBibtexEntryTest.java b/src/test/java/net/sf/jabref/model/entry/CanonicalBibtexEntryTest.java new file mode 100644 index 00000000000..f4a44733c2a --- /dev/null +++ b/src/test/java/net/sf/jabref/model/entry/CanonicalBibtexEntryTest.java @@ -0,0 +1,23 @@ +package net.sf.jabref.model.entry; + +import org.junit.Assert; +import org.junit.Test; + +public class CanonicalBibtexEntryTest { + + /** + * Simple test for the canonical format + */ + @Test + public void canonicalRepresentation() { + BibtexEntry e = new BibtexEntry("id", BibtexEntryTypes.ARTICLE); + e.setField(BibtexEntry.KEY_FIELD, "key"); + e.setField("author", "abc"); + e.setField("title", "def"); + e.setField("journal", "hij"); + String canonicalRepresentation = CanonicalBibtexEntry.getCanonicalRepresentation(e); + Assert.assertEquals("@article{key,\n author = {abc},\n journal = {hij},\n title = {def}\n}", + canonicalRepresentation); + } + +} diff --git a/src/test/resources/net/sf/jabref/importer/fetcher/gvk_artificial_subtitle_test.xml b/src/test/resources/net/sf/jabref/importer/fetcher/gvk_artificial_subtitle_test.xml new file mode 100644 index 00000000000..2451e984f85 --- /dev/null +++ b/src/test/resources/net/sf/jabref/importer/fetcher/gvk_artificial_subtitle_test.xml @@ -0,0 +1,75 @@ + + +1.1 +1 + + +picaxml +xml + + + + + + + +1 + + +picaxml +xml + + + + c + + + +2 + + +picaxml +xml + + + + word + + + +3 + + +picaxml +xml + + + + word1 word2 + + + +4 + + +picaxml +xml + + + + Word1 word2 + + + +5 + + + +1.1 +pica.all=797485368 +50 +xml +picaxml +Year,,1 + + \ No newline at end of file diff --git a/src/test/resources/net/sf/jabref/importer/fetcher/gvk_empty_result_becaue_of_bad_query.xml b/src/test/resources/net/sf/jabref/importer/fetcher/gvk_empty_result_becaue_of_bad_query.xml new file mode 100644 index 00000000000..58a1c44df98 --- /dev/null +++ b/src/test/resources/net/sf/jabref/importer/fetcher/gvk_empty_result_becaue_of_bad_query.xml @@ -0,0 +1,13 @@ + + +1.1 +0 + +1.1 +pica.all=797485368 Zitierlink +50 +xml +picaxml +Year,,1 + + \ No newline at end of file diff --git a/src/test/resources/net/sf/jabref/importer/fetcher/gvk_gmp.1.bib b/src/test/resources/net/sf/jabref/importer/fetcher/gvk_gmp.1.bib new file mode 100644 index 00000000000..6adf1682c64 --- /dev/null +++ b/src/test/resources/net/sf/jabref/importer/fetcher/gvk_gmp.1.bib @@ -0,0 +1,13 @@ +@Book{, + Title = {GMP-Regelwerke für Arzneimittel}, + Publisher = {Maas & Peither, GMP-Verl.}, + Address = {Schopfheim}, + Edition = {7., aktualisierte Aufl.}, + Note = {Text teilw. dt., teilw. engl.}, + Number = {7}, + Series = {Kleiner GMP-Berater}, + Date = {2014}, + ISBN = {9783943267914}, + Pagetotal = {310}, + Ppn_GVK = {785759913} +} diff --git a/src/test/resources/net/sf/jabref/importer/fetcher/gvk_gmp.2.bib b/src/test/resources/net/sf/jabref/importer/fetcher/gvk_gmp.2.bib new file mode 100644 index 00000000000..1a678c0eddf --- /dev/null +++ b/src/test/resources/net/sf/jabref/importer/fetcher/gvk_gmp.2.bib @@ -0,0 +1,13 @@ +@Book{, + Title = {GMP-Regelwerke für computergestützte Systeme}, + Publisher = {Maas & Peither, GMP-Verl}, + Address = {Schopfheim}, + Edition = {4., aktual. Aufl.}, + Note = {Text teilw. dt. und engl}, + Number = {Bd. 5}, + Series = {Kleiner GMP-Berater}, + Date = {2014}, + ISBN = {9783958070011}, + Pagetotal = {142}, + Ppn_GVK = {810635399} +} diff --git a/src/test/resources/net/sf/jabref/importer/fetcher/gvk_gmp.xml b/src/test/resources/net/sf/jabref/importer/fetcher/gvk_gmp.xml new file mode 100644 index 00000000000..d5b96f19b7b --- /dev/null +++ b/src/test/resources/net/sf/jabref/importer/fetcher/gvk_gmp.xml @@ -0,0 +1,566 @@ + + +1.1 +2 + + +picaxml +xml + + + + 20 + + + 2001:14-05-14 + + + 0084:07-11-14 + 14:03:00.000 + + + 0084:20-10-14 + + + utf8 + + + 0 + + + Aau0z + + + 785759913 + + + 9783943267914 + spiralgeh. : EUR 20.87 + DE + + + 9783943267914 + + + 1050792157 + + + DNB + 1050792157 + + + 14A22 + + + OCoLC + 879880337 + + + DNB + 1050792157 + + + http://d-nb.info/1050792157/04 + DE-101 + application/pdf + 2014-05-28 + 04 + 2 + DNB + 2 + + + http://deposit.d-nb.de/cgi-bin/dokserv?id=4659669&prov=M&dok_var=1&dok_ext=htm + Verlag: MVB + text/html + 2014-05-28 + 01 + 2 + DNB + 2 + + + ger + eng + + + 2014 + + + XA-DE + + + GMP-Regelwerke für Arzneimittel + + + 7., aktualisierte Aufl. + + + Schopfheim + Maas & Peither, GMP-Verl. + + + 310 S. + + + 14 cm + + + Kleiner GMP-Berater + 7 + + + 700 + 496510126 + Kleiner GMP-Berater + Schopfheim + Maas & Peither, GMP-Verl + 2000- + 21989291 + 7 + + + Text teilw. dt., teilw. engl. + + + 528183451 + Tuv1 + Arzneimittel- und Wirkstoffherstellungsverordnung + gnd/7568112-2 + + + DE-101 + + + 104616016 + Tgv1 + Europ�ische Union + gnd/4131753-1 + + + 104161949 + Tsv1 + GMP-Regeln + gnd/4113765-6 + + + 104962720 + Tsv1 + Produktinformation + gnd/4227582-9 + + + DE-101 + + + c + Deutschland + + + 22/ger + 615.19002184 + + + 23sdnb + 610 + 340 + + + 23sdnb + 610 + + + 610 + 340 + DNB + + + 106416995 + 86.56 + Gesundheitsrecht + Lebensmittelrecht + + + 106423304 + 44.40 + Pharmazie + Pharmazeutika + + + 15,3 + + + pha + + + 20 + PICA + + + 20-10-14 + 10:29:15.000 + + + 20-10-14 + 1014 + 0084 + + + utf8 + + + 1458093670 + + + 15-07-14 + k + + + 1452-1627 + 00 + + + 84$014521627 + + + +1 + + +picaxml +xml + + + + 20,293 + + + 3293:04-12-14 + + + 2004:19-11-15 + 02:28:21.000 + + + 3293:04-12-14 + + + utf8 + + + 0 + + + Aau + + + 810635399 + + + 9783958070011 + + + GBV + 810635399 + + + ger + eng + eng + + + 2014 + + + XA-DE + + + GMP-Regelwerke für computergestützte Systeme + + + 4., aktual. Aufl. + + + Schopfheim + Maas & Peither, GMP-Verl + + + 142 S. + + + Kleiner GMP-Berater + Bd. 5 + + + 500 + 496510126 + Kleiner GMP-Berater + Schopfheim + Maas & Peither, GMP-Verl + 2000- + 21989291 + Bd. 5 + + + Text teilw. dt. und engl + + + 106200690 + Tsv1 + Pharmazeutische Industrie + gnd/4045696-1 + + + 104451939 + Tsv1 + Betriebliches Informationssystem + gnd/4069386-7 + + + 104467525 + Tsv1 + Computersicherheit + gnd/4274324-2 + + + 104212543 + Tsv1 + Softwareschutz + gnd/4131649-6 + + + DE-101 + + + 23sdnb + 610 + + + 22/ger + 658.4038011 + + + 23sdnb + 650 + 004 + + + 650 + 004 + DNB + + + 15,3 + + + pha + + + 20 + PICA + + + 17-11-15 + 12:06:23.000 + + + 17-11-15 + 1014 + 0084 + + + utf8 + + + 1468429450 + + + 11-12-14 + k + + + 1452-3227 + 00 + + + 84$014523227 + + + 293 + PICA + + + 04-12-14 + 14:49:02.000 + + + 04-12-14 + 5601 + 3293/0333 + + + utf8 + + + 1508486832 + + + 04-12-14 + zf + + + KSF/FHH + MED 2200 251(4):1 + s + 00 + + + fi 2014/0607 + 00 + + + 960$02115956 + + + 04-12-14 + 14:49:02.000 + + + 04-12-14 + 5601 + 3293/0333 + + + utf8 + + + 1513287834 + + + 04-12-14 + zf + + + KSF/FHH + MED 2200 251(4):2 + u + 00 + + + fi 2014/0608 + 00 + + + 960$02115913 + + + 04-12-14 + 14:49:02.000 + + + 04-12-14 + 5601 + 3293/0333 + + + utf8 + + + 1513287842 + + + 04-12-14 + zf + + + KSF/FHH + MED 2200 251(4):3 + u + 00 + + + fi 2014/0609 + 00 + + + 960$02115964 + + + 04-12-14 + 14:49:02.000 + + + 04-12-14 + 5601 + 3293/0333 + + + utf8 + + + 1513287850 + + + 04-12-14 + zf + + + KSF/FHH + MED 2200 251(4):4 + u + 00 + + + fi 2014/0610 + 00 + + + 960$02115921 + + + 04-12-14 + 14:49:02.000 + + + 04-12-14 + 5601 + 3293/0333 + + + utf8 + + + 1513287869 + + + 04-12-14 + zf + + + KSF/FHH + MED 2200 251(4):5 + u + 00 + + + fi 2014/0611 + 00 + + + 960$0211593X + + + +2 + + + +1.1 +pica.all=GMP-Regelwerke für Arzneimittel +50 +xml +picaxml +Year,,1 + + + diff --git a/src/test/resources/net/sf/jabref/importer/fetcher/gvk_result_for_797485368.bib b/src/test/resources/net/sf/jabref/importer/fetcher/gvk_result_for_797485368.bib new file mode 100644 index 00000000000..f76dc7f1d53 --- /dev/null +++ b/src/test/resources/net/sf/jabref/importer/fetcher/gvk_result_for_797485368.bib @@ -0,0 +1,12 @@ +@Book{, + Title = {GMP-Berater}, + Publisher = {Maas und Peither GMP Verl.}, + Address = {Schopfheim}, + Number = {L - N}, + Volume = {Bd. I}, + Date = {2014}, + Editor = {Anita Maas}, + Pagetotal = {Losebl.-Ausg.}, + Ppn_GVK = {797485368}, + Subtitle = {Nachschlagewerk für Pharmaindustrie und Lieferanten. Die praxisorientierte Loseblattsammlung für die Gute Herstellungspraxis (Good Manufacturing Practice)} +} diff --git a/src/test/resources/net/sf/jabref/importer/fetcher/gvk_result_for_797485368.xml b/src/test/resources/net/sf/jabref/importer/fetcher/gvk_result_for_797485368.xml new file mode 100644 index 00000000000..7c2b3783937 --- /dev/null +++ b/src/test/resources/net/sf/jabref/importer/fetcher/gvk_result_for_797485368.xml @@ -0,0 +1,131 @@ + + +1.1 +1 + + +picaxml +xml + + + + 31 + + + 0027:26-09-14 + + + 0027:26-09-14 + 09:39:19.000 + + + 0027:26-09-14 + + + utf8 + + + 0 + + + Afu + + + 797485368 + + + GBV + 797485368 + + + ger + + + 2014 + + + XA-DE + + + @L - N + + + Anita + Maas + + + Schopfheim + Maas und Peither GMP Verl. + + + Losebl.-Ausg. + + + GMP-Berater + Nachschlagewerk für Pharmaindustrie und Lieferanten. Die praxisorientierte Loseblattsammlung für die Gute Herstellungspraxis (Good Manufacturing Practice) + hrsg. von Anita Maas ... + Bd. I + + + I.2014 + 327753609 + GMP-Berater + Nachschlagewerk für Pharmaindustrie und Lieferanten. Die praxisorientierte Loseblattsammlung für die Gute Herstellungspraxis (Good Manufacturing Practice) + Maas + Anita + Schopfheim + Maas und Peither GMP Verl. + 2000- + Bd. I + _I 2014 + + + 31 + PICA + + + 31-03-15 + 16:54:04.000 + + + 31-03-15 + 7681 + 0027 + + + utf8 + + + 150373661X + + + 26-09-14 + z + + + J 36/PT2 + PHA:RG:5500:Maa:I:2000 + f + 00 + + + U501697/2 + 00 + + + 27$027697738 + + + +1 + + + +1.1 +pica.all=797485368 +50 +xml +picaxml +Year,,1 + + \ No newline at end of file